Kaynağa Gözat

feat: reuse tabs across multi-run — no more tab bloat

- tabRegistry preserved across resetState (like seenCodes/accounts)
- New reuseOrCreateTab() helper: navigates existing tab or creates new one
- All steps use reuseOrCreateTab: VPS panel, auth page, mail
- Mail tabs: only activate if alive, don't re-navigate (preserves session)
- Auth tab: reused between step 2→6→8, navigated to new URLs
- VPS tab: reused across runs, re-injected on navigation
unknown 4 ay önce
ebeveyn
işleme
1286130023

+ 85 - 79
background.js

@@ -39,13 +39,14 @@ async function setState(updates) {
 
 async function resetState() {
   console.log(LOG_PREFIX, 'Resetting all state');
-  // Preserve seenCodes and accounts across resets
-  const prev = await chrome.storage.session.get(['seenCodes', 'accounts']);
+  // Preserve seenCodes, accounts, and tabRegistry across resets
+  const prev = await chrome.storage.session.get(['seenCodes', 'accounts', 'tabRegistry']);
   await chrome.storage.session.clear();
   await chrome.storage.session.set({
     ...DEFAULT_STATE,
     seenCodes: prev.seenCodes || [],
     accounts: prev.accounts || [],
+    tabRegistry: prev.tabRegistry || {},
   });
 }
 
@@ -140,6 +141,61 @@ function flushCommand(source, tabId) {
   }
 }
 
+// ============================================================
+// Reuse or create tab
+// ============================================================
+
+async function reuseOrCreateTab(source, url, options = {}) {
+  const alive = await isTabAlive(source);
+  if (alive) {
+    const tabId = await getTabId(source);
+    // Navigate existing tab to new URL (or just activate if same URL)
+    await chrome.tabs.update(tabId, { url, active: true });
+    console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}), navigated to ${url.slice(0, 60)}`);
+
+    // Wait for page load
+    await new Promise(resolve => {
+      const listener = (tid, info) => {
+        if (tid === tabId && info.status === 'complete') {
+          chrome.tabs.onUpdated.removeListener(listener);
+          resolve();
+        }
+      };
+      chrome.tabs.onUpdated.addListener(listener);
+    });
+
+    // Mark as not ready — content script will re-inject and send READY
+    const registry = await getTabRegistry();
+    if (registry[source]) registry[source].ready = false;
+    await setState({ tabRegistry: registry });
+
+    return tabId;
+  }
+
+  // Create new tab
+  const tab = await chrome.tabs.create({ url, active: true });
+  console.log(LOG_PREFIX, `Created new tab ${source} (${tab.id})`);
+
+  // If dynamic injection needed (VPS panel), inject scripts after load
+  if (options.inject) {
+    await new Promise(resolve => {
+      const listener = (tabId, info) => {
+        if (tabId === tab.id && info.status === 'complete') {
+          chrome.tabs.onUpdated.removeListener(listener);
+          resolve();
+        }
+      };
+      chrome.tabs.onUpdated.addListener(listener);
+    });
+    await chrome.scripting.executeScript({
+      target: { tabId: tab.id },
+      files: options.inject,
+    });
+  }
+
+  return tab.id;
+}
+
 // ============================================================
 // Send command to content script (with readiness check)
 // ============================================================
@@ -526,35 +582,11 @@ async function resumeAutoRun() {
 // ============================================================
 
 async function executeStep1(state) {
-  const vpsUrl = state.vpsUrl;
-  if (!vpsUrl) {
+  if (!state.vpsUrl) {
     throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.');
   }
-
-  // Ensure VPS panel tab is open
-  const alive = await isTabAlive('vps-panel');
-  if (!alive) {
-    await addLog(`Step 1: Opening VPS panel: ${vpsUrl.slice(0, 60)}...`);
-    const tab = await chrome.tabs.create({ url: vpsUrl, active: true });
-    // Dynamically inject content scripts since VPS URL is configurable
-    // Wait for page to load, then inject
-    await new Promise(resolve => {
-      const listener = (tabId, info) => {
-        if (tabId === tab.id && info.status === 'complete') {
-          chrome.tabs.onUpdated.removeListener(listener);
-          resolve();
-        }
-      };
-      chrome.tabs.onUpdated.addListener(listener);
-    });
-    await chrome.scripting.executeScript({
-      target: { tabId: tab.id },
-      files: ['content/utils.js', 'content/vps-panel.js'],
-    });
-  } else {
-    const tabId = await getTabId('vps-panel');
-    if (tabId) await chrome.tabs.update(tabId, { active: true });
-  }
+  await addLog(`Step 1: Opening VPS panel...`);
+  await reuseOrCreateTab('vps-panel', state.vpsUrl, { inject: ['content/utils.js', 'content/vps-panel.js'] });
 
   await sendToContentScript('vps-panel', {
     type: 'EXECUTE_STEP',
@@ -572,10 +604,9 @@ async function executeStep2(state) {
   if (!state.oauthUrl) {
     throw new Error('No OAuth URL. Complete step 1 first.');
   }
-  await addLog(`Step 2: Opening auth URL in new tab: ${state.oauthUrl.slice(0, 80)}...`);
-  const tab = await chrome.tabs.create({ url: state.oauthUrl, active: true });
-  // signup-page.js will auto-inject via manifest content_scripts
-  // Queue the command — it will flush when script sends READY signal
+  await addLog(`Step 2: Opening auth URL...`);
+  await reuseOrCreateTab('signup-page', state.oauthUrl);
+
   await sendToContentScript('signup-page', {
     type: 'EXECUTE_STEP',
     step: 2,
@@ -625,14 +656,15 @@ function getMailConfig(state) {
 
 async function executeStep4(state) {
   const mail = getMailConfig(state);
+  await addLog(`Step 4: Opening ${mail.label}...`);
 
+  // For mail tabs, only create if not alive — don't navigate (preserves login session)
   const alive = await isTabAlive(mail.source);
-  if (!alive) {
-    await addLog(`Step 4: Opening ${mail.label}...`);
-    await chrome.tabs.create({ url: mail.url, active: true });
-  } else {
+  if (alive) {
     const tabId = await getTabId(mail.source);
-    if (tabId) await chrome.tabs.update(tabId, { active: true });
+    await chrome.tabs.update(tabId, { active: true });
+  } else {
+    await reuseOrCreateTab(mail.source, mail.url);
   }
 
   const result = await sendToContentScript(mail.source, {
@@ -702,15 +734,9 @@ async function executeStep6(state) {
     throw new Error('No email. Complete step 3 first.');
   }
 
-  // Open the OAuth URL again in a new tab to start the login flow
-  // Close the old signup tab first (it's on add-phone page, not needed)
-  const oldSignupTabId = await getTabId('signup-page');
-  if (oldSignupTabId) {
-    try { await chrome.tabs.remove(oldSignupTabId); } catch {}
-  }
-
-  await addLog(`Step 6: Opening OAuth URL for login: ${state.oauthUrl.slice(0, 60)}...`);
-  await chrome.tabs.create({ url: state.oauthUrl, active: true });
+  await addLog(`Step 6: Opening OAuth URL for login...`);
+  // Reuse the signup-page tab — navigate it to the OAuth URL
+  await reuseOrCreateTab('signup-page', state.oauthUrl);
 
   // signup-page.js will inject (same auth.openai.com domain) and handle login
   await sendToContentScript('signup-page', {
@@ -727,14 +753,14 @@ async function executeStep6(state) {
 
 async function executeStep7(state) {
   const mail = getMailConfig(state);
+  await addLog(`Step 7: Opening ${mail.label}...`);
 
   const alive = await isTabAlive(mail.source);
-  if (!alive) {
-    await addLog(`Step 7: Opening ${mail.label}...`);
-    await chrome.tabs.create({ url: mail.url, active: true });
-  } else {
+  if (alive) {
     const tabId = await getTabId(mail.source);
-    if (tabId) await chrome.tabs.update(tabId, { active: true });
+    await chrome.tabs.update(tabId, { active: true });
+  } else {
+    await reuseOrCreateTab(mail.source, mail.url);
   }
 
   const result = await sendToContentScript(mail.source, {
@@ -835,9 +861,8 @@ async function executeStep8(state) {
             payload: {},
           });
         } else {
-          // Auth tab was closed, reopen OAuth URL
-          await chrome.tabs.create({ url: state.oauthUrl, active: true });
-          await addLog('Step 8: Auth tab closed, reopening OAuth URL...');
+          await reuseOrCreateTab('signup-page', state.oauthUrl);
+          await addLog('Step 8: Auth tab reopened...');
           await sendToContentScript('signup-page', {
             type: 'EXECUTE_STEP',
             step: 8,
@@ -865,32 +890,13 @@ async function executeStep9(state) {
   if (!state.localhostUrl) {
     throw new Error('No localhost URL. Complete step 8 first.');
   }
-
-  const vpsUrl = state.vpsUrl || 'http://154.26.182.181:8317/management.html#/oauth';
-
-  // Switch to VPS panel tab
-  const alive = await isTabAlive('vps-panel');
-  if (!alive) {
-    await addLog('Step 9: Opening VPS panel...');
-    const tab = await chrome.tabs.create({ url: vpsUrl, active: true });
-    await new Promise(resolve => {
-      const listener = (tabId, info) => {
-        if (tabId === tab.id && info.status === 'complete') {
-          chrome.tabs.onUpdated.removeListener(listener);
-          resolve();
-        }
-      };
-      chrome.tabs.onUpdated.addListener(listener);
-    });
-    await chrome.scripting.executeScript({
-      target: { tabId: tab.id },
-      files: ['content/utils.js', 'content/vps-panel.js'],
-    });
-  } else {
-    const tabId = await getTabId('vps-panel');
-    if (tabId) await chrome.tabs.update(tabId, { active: true });
+  if (!state.vpsUrl) {
+    throw new Error('VPS URL not set. Please enter VPS URL in the side panel.');
   }
 
+  await addLog('Step 9: Opening VPS panel...');
+  await reuseOrCreateTab('vps-panel', state.vpsUrl, { inject: ['content/utils.js', 'content/vps-panel.js'] });
+
   await sendToContentScript('vps-panel', {
     type: 'EXECUTE_STEP',
     step: 9,

+ 1 - 1
content/chatgpt.js

@@ -39,7 +39,7 @@ async function step6_loginChatGPT() {
   // Get state for email and password
   const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
   const email = state.email;
-  const password = state.password || 'mimashisha0.0';
+  const password = state.password;
 
   if (!email) throw new Error('No email found in state. Complete earlier steps first.');
 

+ 38 - 26
content/mail-163.js

@@ -184,37 +184,49 @@ async function deleteEmail(item, step) {
   try {
     log(`Step ${step}: Deleting email...`);
 
-    // Right-click on the mail item to trigger context menu
-    const rect = item.getBoundingClientRect();
-    item.dispatchEvent(new MouseEvent('contextmenu', {
-      bubbles: true, cancelable: true, button: 2,
-      clientX: rect.left + rect.width / 2,
-      clientY: rect.top + rect.height / 2,
-    }));
-
-    // Wait for context menu to appear
-    let deleteMenuItem = null;
-    for (let i = 0; i < 10; i++) {
+    // Strategy 1: Click the trash icon inside the mail item
+    // Each mail item has: <b class="nui-ico nui-ico-delete" title="删除邮件" sign="trash">
+    // These icons appear on hover, so we trigger mouseover first
+    item.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
+    item.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
+    await sleep(300);
+
+    const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]');
+    if (trashIcon) {
+      trashIcon.click();
+      log(`Step ${step}: Clicked trash icon`, 'ok');
+      await sleep(1500);
+
+      // Check if item disappeared (confirm deletion)
+      const stillExists = document.getElementById(item.id);
+      if (!stillExists || stillExists.style.display === 'none') {
+        log(`Step ${step}: Email deleted successfully`);
+      } else {
+        log(`Step ${step}: Email may not have been deleted, item still visible`, 'warn');
+      }
+      return;
+    }
+
+    // Strategy 2: Select checkbox then click toolbar delete button
+    log(`Step ${step}: Trash icon not found, trying checkbox + toolbar delete...`);
+    const checkbox = item.querySelector('[sign="checkbox"], .nui-chk');
+    if (checkbox) {
+      checkbox.click();
       await sleep(300);
-      const menuItems = document.querySelectorAll('.nui-menu-item .nui-menu-item-text');
-      for (const mi of menuItems) {
-        if (mi.textContent.trim() === '删除邮件') {
-          deleteMenuItem = mi.closest('.nui-menu-item');
-          break;
+
+      // Click toolbar delete button
+      const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
+      for (const btn of toolbarBtns) {
+        if (btn.textContent.replace(/\s/g, '').includes('删除')) {
+          btn.closest('.nui-btn').click();
+          log(`Step ${step}: Clicked toolbar delete`, 'ok');
+          await sleep(1500);
+          return;
         }
       }
-      if (deleteMenuItem) break;
     }
 
-    if (deleteMenuItem) {
-      deleteMenuItem.click();
-      log(`Step ${step}: Clicked "删除邮件"`, 'ok');
-      // Wait for the delete to process and item to disappear
-      await sleep(1000);
-      log(`Step ${step}: Email deleted successfully`);
-    } else {
-      log(`Step ${step}: Context menu "删除邮件" not found`, 'warn');
-    }
+    log(`Step ${step}: Could not delete email (no delete button found)`, 'warn');
   } catch (err) {
     log(`Step ${step}: Failed to delete email: ${err.message}`, 'warn');
   }

+ 16 - 3
content/signup-page.js

@@ -111,7 +111,8 @@ async function step3_fillEmailPassword(payload) {
     }
   }
 
-  fillInput(passwordInput, payload.password || 'mimashisha0.0');
+  if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
+  fillInput(passwordInput, payload.password);
   log('Step 3: Password filled');
 
   // Report complete BEFORE submit, because submit causes page navigation
@@ -265,8 +266,20 @@ async function step8_clickContinue() {
   }
 
   log('Step 8: Found "继续" button, clicking...');
-  simulateClick(continueBtn);
-  log('Step 8: Clicked "继续", redirecting to localhost... (background will capture URL)');
+
+  // Use native .click() — simulateClick (dispatchEvent) may not trigger form submit
+  continueBtn.click();
+  log('Step 8: Clicked via .click()');
+
+  // Also try submitting the form directly as a fallback
+  await sleep(500);
+  const form = continueBtn.closest('form');
+  if (form) {
+    form.requestSubmit(continueBtn);
+    log('Step 8: Also triggered form.requestSubmit()');
+  }
+
+  log('Step 8: Redirecting to localhost... (background will capture URL)');
 
   // Don't reportComplete — background handles it via webNavigation listener
 }

+ 88 - 0
sidepanel/sidepanel.css

@@ -541,6 +541,94 @@ header {
 
 .log-line { animation: fadeIn 120ms ease-out; }
 
+/* ============================================================
+   Toast Notifications
+   ============================================================ */
+
+#toast-container {
+  position: fixed;
+  top: 12px;
+  left: 12px;
+  right: 12px;
+  z-index: 1000;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  pointer-events: none;
+}
+
+.toast {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 10px 12px;
+  border-radius: var(--radius-md);
+  font-size: 12px;
+  font-weight: 500;
+  box-shadow: var(--shadow-md), 0 4px 12px rgba(0,0,0,0.1);
+  pointer-events: auto;
+  animation: toastIn 250ms ease-out;
+  border: 1px solid;
+}
+
+.toast.toast-exit {
+  animation: toastOut 200ms ease-in forwards;
+}
+
+.toast-error {
+  background: var(--red-soft);
+  border-color: var(--red);
+  color: var(--red);
+}
+
+.toast-warn {
+  background: var(--orange-soft);
+  border-color: var(--orange);
+  color: var(--orange);
+}
+
+.toast-success {
+  background: var(--green-soft);
+  border-color: var(--green);
+  color: var(--green);
+}
+
+.toast-info {
+  background: var(--blue-soft);
+  border-color: var(--blue);
+  color: var(--blue);
+}
+
+.toast svg {
+  flex-shrink: 0;
+}
+
+.toast-msg {
+  flex: 1;
+}
+
+.toast-close {
+  background: none;
+  border: none;
+  color: inherit;
+  opacity: 0.6;
+  cursor: pointer;
+  padding: 2px;
+  font-size: 16px;
+  line-height: 1;
+}
+.toast-close:hover { opacity: 1; }
+
+@keyframes toastIn {
+  from { opacity: 0; transform: translateY(-10px) scale(0.96); }
+  to { opacity: 1; transform: translateY(0) scale(1); }
+}
+
+@keyframes toastOut {
+  from { opacity: 1; transform: translateY(0) scale(1); }
+  to { opacity: 0; transform: translateY(-10px) scale(0.96); }
+}
+
 @media (prefers-reduced-motion: reduce) {
   *, *::before, *::after {
     animation-duration: 0.01ms !important;

+ 1 - 0
sidepanel/sidepanel.html

@@ -140,6 +140,7 @@
     <div id="log-area"></div>
   </section>
 
+  <div id="toast-container"></div>
   <script src="sidepanel.js"></script>
 </body>
 </html>

+ 37 - 2
sidepanel/sidepanel.js

@@ -23,6 +23,38 @@ const inputVpsUrl = document.getElementById('input-vps-url');
 const selectMailProvider = document.getElementById('select-mail-provider');
 const inputRunCount = document.getElementById('input-run-count');
 
+// ============================================================
+// Toast Notifications
+// ============================================================
+
+const toastContainer = document.getElementById('toast-container');
+
+const TOAST_ICONS = {
+  error: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
+  warn: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
+  success: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
+  info: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
+};
+
+function showToast(message, type = 'error', duration = 4000) {
+  const toast = document.createElement('div');
+  toast.className = `toast toast-${type}`;
+  toast.innerHTML = `${TOAST_ICONS[type] || ''}<span class="toast-msg">${escapeHtml(message)}</span><button class="toast-close">&times;</button>`;
+
+  toast.querySelector('.toast-close').addEventListener('click', () => dismissToast(toast));
+  toastContainer.appendChild(toast);
+
+  if (duration > 0) {
+    setTimeout(() => dismissToast(toast), duration);
+  }
+}
+
+function dismissToast(toast) {
+  if (!toast.parentNode) return;
+  toast.classList.add('toast-exit');
+  toast.addEventListener('animationend', () => toast.remove());
+}
+
 // ============================================================
 // State Restore on load
 // ============================================================
@@ -193,7 +225,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
     if (step === 3) {
       const email = inputEmail.value.trim();
       if (!email) {
-        appendLog({ message: 'Please paste email address first', level: 'error', timestamp: Date.now() });
+        showToast('Please paste email address first', 'warn');
         return;
       }
       await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
@@ -215,7 +247,7 @@ btnAutoRun.addEventListener('click', async () => {
 btnAutoContinue.addEventListener('click', async () => {
   const email = inputEmail.value.trim();
   if (!email) {
-    appendLog({ message: 'Please paste DuckDuckGo email first!', level: 'error', timestamp: Date.now() });
+    showToast('Please paste DuckDuckGo email first!', 'warn');
     return;
   }
   autoContinueBar.style.display = 'none';
@@ -279,6 +311,9 @@ chrome.runtime.onMessage.addListener((message) => {
   switch (message.type) {
     case 'LOG_ENTRY':
       appendLog(message.payload);
+      if (message.payload.level === 'error') {
+        showToast(message.payload.message, 'error');
+      }
       break;
 
     case 'STEP_STATUS_CHANGED': {