diff --git a/background.js b/background.js index 87868d1..0cc354d 100644 --- a/background.js +++ b/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, diff --git a/content/chatgpt.js b/content/chatgpt.js index 75af27e..584a446 100644 --- a/content/chatgpt.js +++ b/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.'); diff --git a/content/mail-163.js b/content/mail-163.js index cd9a803..b54f92b 100644 --- a/content/mail-163.js +++ b/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, - })); + // Strategy 1: Click the trash icon inside the mail item + // Each mail item has: + // 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); - // Wait for context menu to appear - let deleteMenuItem = null; - for (let i = 0; i < 10; i++) { + 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'); } diff --git a/content/signup-page.js b/content/signup-page.js index 17a81f2..c7db04f 100644 --- a/content/signup-page.js +++ b/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 } diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index fd30ea0..f4c6da0 100644 --- a/sidepanel/sidepanel.css +++ b/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; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index bb5ecc7..d72afe9 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -140,6 +140,7 @@
+
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index b9b9860..7578886 100644 --- a/sidepanel/sidepanel.js +++ b/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: '', + warn: '', + success: '', + info: '', +}; + +function showToast(message, type = 'error', duration = 4000) { + const toast = document.createElement('div'); + toast.className = `toast toast-${type}`; + toast.innerHTML = `${TOAST_ICONS[type] || ''}${escapeHtml(message)}`; + + 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': {