// background.js — Service Worker: orchestration, state, tab management, message routing importScripts('data/names.js'); const LOG_PREFIX = '[MultiPage:bg]'; const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill'; const STOP_ERROR_MESSAGE = 'Flow stopped by user.'; const HUMAN_STEP_DELAY_MIN = 700; const HUMAN_STEP_DELAY_MAX = 2200; initializeSessionStorageAccess(); let automationWindowId = null; async function ensureAutomationWindowId() { if (automationWindowId != null) { try { await chrome.windows.get(automationWindowId); return automationWindowId; } catch { automationWindowId = null; } } const registry = await getTabRegistry(); for (const entry of Object.values(registry)) { if (entry && entry.tabId) { try { const tab = await chrome.tabs.get(entry.tabId); automationWindowId = tab.windowId; return automationWindowId; } catch {} } } const win = await chrome.windows.getLastFocused(); automationWindowId = win.id; return automationWindowId; } // ============================================================ // State Management (chrome.storage.session + local for persistence) // ============================================================ // Keys that should survive extension reload (saved to chrome.storage.local) const PERSISTENT_KEYS = [ 'vpsUrl', 'customPassword', 'mailProvider', 'tempApiUrl', 'inbucketHost', 'inbucketMailbox', 'accounts', 'seenCodes', 'seenInbucketMailIds', 'duckToken', ]; const DEFAULT_STATE = { currentStep: 0, stepStatuses: { 1: 'pending', 2: 'pending', 3: 'pending', 4: 'pending', 5: 'pending', 6: 'pending', 7: 'pending', 8: 'pending', 9: 'pending', }, oauthUrl: null, email: null, password: null, accounts: [], // { email, password, createdAt } lastEmailTimestamp: null, localhostUrl: null, flowStartTime: null, tabRegistry: {}, logs: [], vpsUrl: '', customPassword: '', mailProvider: 'temp-api', // 'temp-api', 'qq', '163', or 'inbucket' tempApiUrl: '', inbucketHost: '', inbucketMailbox: '', }; async function getState() { const state = await chrome.storage.session.get(null); return { ...DEFAULT_STATE, ...state }; } async function initializeSessionStorageAccess() { try { if (chrome.storage?.session?.setAccessLevel) { await chrome.storage.session.setAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS', }); console.log(LOG_PREFIX, 'Enabled storage.session for content scripts'); } } catch (err) { console.warn(LOG_PREFIX, 'Failed to enable storage.session for content scripts:', err?.message || err); } // Restore persistent settings from chrome.storage.local into session try { const saved = await chrome.storage.local.get(PERSISTENT_KEYS); const toRestore = {}; for (const key of PERSISTENT_KEYS) { if (saved[key] !== undefined) toRestore[key] = saved[key]; } if (Object.keys(toRestore).length > 0) { await chrome.storage.session.set(toRestore); console.log(LOG_PREFIX, 'Restored persistent settings from local storage:', Object.keys(toRestore).join(', ')); } } catch (err) { console.warn(LOG_PREFIX, 'Failed to restore persistent settings:', err?.message || err); } } async function setState(updates) { console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200)); await chrome.storage.session.set(updates); // Persist relevant keys to chrome.storage.local so they survive reload const toPersist = {}; for (const key of PERSISTENT_KEYS) { if (key in updates) toPersist[key] = updates[key]; } if (Object.keys(toPersist).length > 0) { await chrome.storage.local.set(toPersist); } } function broadcastDataUpdate(payload) { chrome.runtime.sendMessage({ type: 'DATA_UPDATED', payload, }).catch(() => {}); } async function setEmailState(email) { await setState({ email }); broadcastDataUpdate({ email }); } async function setPasswordState(password) { await setState({ password }); broadcastDataUpdate({ password }); } async function resetState() { console.log(LOG_PREFIX, 'Resetting all state'); // Preserve settings and persistent data across resets const prev = await chrome.storage.session.get([ 'seenCodes', 'seenInbucketMailIds', 'accounts', 'tabRegistry', 'vpsUrl', 'customPassword', 'mailProvider', 'inbucketHost', 'inbucketMailbox', ]); // Also check chrome.storage.local in case session was empty (after reload) const local = await chrome.storage.local.get(PERSISTENT_KEYS); await chrome.storage.session.clear(); await chrome.storage.session.set({ ...DEFAULT_STATE, seenCodes: prev.seenCodes || local.seenCodes || [], seenInbucketMailIds: prev.seenInbucketMailIds || local.seenInbucketMailIds || [], accounts: prev.accounts || local.accounts || [], tabRegistry: prev.tabRegistry || {}, vpsUrl: prev.vpsUrl || local.vpsUrl || '', customPassword: prev.customPassword || local.customPassword || '', mailProvider: prev.mailProvider || local.mailProvider || '163', inbucketHost: prev.inbucketHost || local.inbucketHost || '', inbucketMailbox: prev.inbucketMailbox || local.inbucketMailbox || '', }); } /** * Generate a random password: 14 chars, mix of uppercase, lowercase, digits, symbols. */ function generatePassword() { const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; const lower = 'abcdefghjkmnpqrstuvwxyz'; const digits = '23456789'; const symbols = '!@#$%&*?'; const all = upper + lower + digits + symbols; // Ensure at least one of each type let pw = ''; pw += upper[Math.floor(Math.random() * upper.length)]; pw += lower[Math.floor(Math.random() * lower.length)]; pw += digits[Math.floor(Math.random() * digits.length)]; pw += symbols[Math.floor(Math.random() * symbols.length)]; // Fill remaining 10 chars for (let i = 0; i < 10; i++) { pw += all[Math.floor(Math.random() * all.length)]; } // Shuffle return pw.split('').sort(() => Math.random() - 0.5).join(''); } // ============================================================ // Tab Registry // ============================================================ async function getTabRegistry() { const state = await getState(); return state.tabRegistry || {}; } async function registerTab(source, tabId) { const registry = await getTabRegistry(); registry[source] = { tabId, ready: true }; await setState({ tabRegistry: registry }); console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`); } async function isTabAlive(source) { const registry = await getTabRegistry(); const entry = registry[source]; if (!entry) return false; try { await chrome.tabs.get(entry.tabId); return true; } catch { // Tab no longer exists — clean up registry registry[source] = null; await setState({ tabRegistry: registry }); return false; } } async function getTabId(source) { const registry = await getTabRegistry(); return registry[source]?.tabId || null; } // ============================================================ // Command Queue (for content scripts not yet ready) // ============================================================ const pendingCommands = new Map(); // source -> { message, resolve, reject, timer } function queueCommand(source, message, timeout = 15000) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { pendingCommands.delete(source); const err = `Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`; console.error(LOG_PREFIX, err); reject(new Error(err)); }, timeout); pendingCommands.set(source, { message, resolve, reject, timer }); console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`); }); } function flushCommand(source, tabId) { const pending = pendingCommands.get(source); if (pending) { clearTimeout(pending.timer); pendingCommands.delete(source); chrome.tabs.sendMessage(tabId, pending.message).then(pending.resolve).catch(pending.reject); console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`); } } function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) { for (const [source, pending] of pendingCommands.entries()) { clearTimeout(pending.timer); pending.reject(new Error(reason)); pendingCommands.delete(source); console.log(LOG_PREFIX, `Cancelled queued command for ${source}`); } } // ============================================================ // Reuse or create tab // ============================================================ async function reuseOrCreateTab(source, url, options = {}) { const alive = await isTabAlive(source); if (alive) { const tabId = await getTabId(source); const currentTab = await chrome.tabs.get(tabId); const sameUrl = currentTab.url === url; const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl; const registry = await getTabRegistry(); if (sameUrl) { await chrome.tabs.update(tabId, { active: true }); console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}) on same URL`); if (shouldReloadOnReuse) { if (registry[source]) registry[source].ready = false; await setState({ tabRegistry: registry }); await chrome.tabs.reload(tabId); await new Promise((resolve) => { const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000); const listener = (tid, info) => { if (tid === tabId && info.status === 'complete') { chrome.tabs.onUpdated.removeListener(listener); clearTimeout(timer); resolve(); } }; chrome.tabs.onUpdated.addListener(listener); }); } // For dynamically injected pages like the VPS panel, re-inject immediately. if (options.inject) { if (registry[source]) registry[source].ready = false; await setState({ tabRegistry: registry }); if (options.injectSource) { await chrome.scripting.executeScript({ target: { tabId }, func: (injectedSource) => { window.__MULTIPAGE_SOURCE = injectedSource; }, args: [options.injectSource], }); } await chrome.scripting.executeScript({ target: { tabId }, files: options.inject, }); await new Promise(r => setTimeout(r, 500)); } return tabId; } // Mark as not ready BEFORE navigating — so READY signal from new page is captured correctly if (registry[source]) registry[source].ready = false; await setState({ tabRegistry: registry }); // Navigate existing tab to new 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 complete (with 30s timeout) await new Promise((resolve) => { const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000); const listener = (tid, info) => { if (tid === tabId && info.status === 'complete') { chrome.tabs.onUpdated.removeListener(listener); clearTimeout(timer); resolve(); } }; chrome.tabs.onUpdated.addListener(listener); }); // If dynamic injection needed (VPS panel), re-inject after navigation if (options.inject) { if (options.injectSource) { await chrome.scripting.executeScript({ target: { tabId }, func: (injectedSource) => { window.__MULTIPAGE_SOURCE = injectedSource; }, args: [options.injectSource], }); } await chrome.scripting.executeScript({ target: { tabId }, files: options.inject, }); } // Wait a bit for content script to inject and send READY await new Promise(r => setTimeout(r, 500)); return tabId; } // Create new tab in the automation window const wid = await ensureAutomationWindowId(); const tab = await chrome.tabs.create({ url, active: true, windowId: wid }); 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 timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000); const listener = (tabId, info) => { if (tabId === tab.id && info.status === 'complete') { chrome.tabs.onUpdated.removeListener(listener); clearTimeout(timer); resolve(); } }; chrome.tabs.onUpdated.addListener(listener); }); if (options.injectSource) { await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (injectedSource) => { window.__MULTIPAGE_SOURCE = injectedSource; }, args: [options.injectSource], }); } await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: options.inject, }); } return tab.id; } // ============================================================ // Send command to content script (with readiness check) // ============================================================ async function sendToContentScript(source, message) { const registry = await getTabRegistry(); const entry = registry[source]; if (!entry || !entry.ready) { console.log(LOG_PREFIX, `${source} not ready, queuing command`); return queueCommand(source, message); } // Verify tab is still alive const alive = await isTabAlive(source); if (!alive) { // Tab was closed — queue the command, it will be sent when tab is reopened console.log(LOG_PREFIX, `${source} tab was closed, queuing command`); return queueCommand(source, message); } console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type); try { return await chrome.tabs.sendMessage(entry.tabId, message); } catch (err) { // Handle bfcache / port closed errors by reloading the tab and retrying if (err?.message?.includes('back/forward cache') || err?.message?.includes('message channel is closed')) { console.log(LOG_PREFIX, `${source} hit bfcache error, reloading tab and retrying...`); try { await chrome.tabs.reload(entry.tabId); // Wait for page to reload and content script to re-inject await new Promise((resolve) => { const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 15000); const listener = (tid, info) => { if (tid === entry.tabId && info.status === 'complete') { chrome.tabs.onUpdated.removeListener(listener); clearTimeout(timer); resolve(); } }; chrome.tabs.onUpdated.addListener(listener); }); // Mark as not ready and wait for READY signal const reg = await getTabRegistry(); if (reg[source]) reg[source].ready = false; await setState({ tabRegistry: reg }); // Queue the command — it'll be sent once the content script signals READY return queueCommand(source, message); } catch (retryErr) { throw new Error(`${source} bfcache recovery failed: ${retryErr.message}`); } } throw err; } } // ============================================================ // Logging // ============================================================ async function addLog(message, level = 'info') { const state = await getState(); const logs = state.logs || []; const entry = { message, level, timestamp: Date.now() }; logs.push(entry); // Keep last 500 logs if (logs.length > 500) logs.splice(0, logs.length - 500); await setState({ logs }); // Broadcast to side panel chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {}); } // ============================================================ // Step Status Management // ============================================================ async function setStepStatus(step, status) { const state = await getState(); const statuses = { ...state.stepStatuses }; statuses[step] = status; await setState({ stepStatuses: statuses, currentStep: step }); // Broadcast to side panel chrome.runtime.sendMessage({ type: 'STEP_STATUS_CHANGED', payload: { step, status }, }).catch(() => {}); } function isStopError(error) { const message = typeof error === 'string' ? error : error?.message; return message === STOP_ERROR_MESSAGE; } function clearStopRequest() { stopRequested = false; } function throwIfStopped() { if (stopRequested) { throw new Error(STOP_ERROR_MESSAGE); } } async function sleepWithStop(ms) { const start = Date.now(); while (Date.now() - start < ms) { throwIfStopped(); await new Promise(r => setTimeout(r, Math.min(100, ms - (Date.now() - start)))); } } async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY_MAX) { const duration = Math.floor(Math.random() * (max - min + 1)) + min; await sleepWithStop(duration); } // clickWithDebugger removed — Step 8 now uses content script simulateClick directly async function broadcastStopToContentScripts() { const registry = await getTabRegistry(); for (const entry of Object.values(registry)) { if (!entry?.tabId) continue; try { await chrome.tabs.sendMessage(entry.tabId, { type: 'STOP_FLOW', source: 'background', payload: {}, }); } catch {} } } let stopRequested = false; // ============================================================ // Message Handler (central router) // ============================================================ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message); handleMessage(message, sender).then(response => { sendResponse(response); }).catch(err => { console.error(LOG_PREFIX, 'Handler error:', err); sendResponse({ error: err.message }); }); return true; // async response }); async function handleMessage(message, sender) { switch (message.type) { case 'CONTENT_SCRIPT_READY': { const tabId = sender.tab?.id; if (tabId && message.source) { await registerTab(message.source, tabId); flushCommand(message.source, tabId); await addLog(`Content script ready: ${message.source} (tab ${tabId})`); } return { ok: true }; } case 'LOG': { const { message: msg, level } = message.payload; await addLog(`[${message.source}] ${msg}`, level); return { ok: true }; } case 'STEP_COMPLETE': { if (stopRequested) { await setStepStatus(message.step, 'stopped'); notifyStepError(message.step, STOP_ERROR_MESSAGE); return { ok: true }; } await setStepStatus(message.step, 'completed'); await addLog(`Step ${message.step} completed`, 'ok'); await handleStepData(message.step, message.payload); notifyStepComplete(message.step, message.payload); return { ok: true }; } case 'STEP_ERROR': { if (isStopError(message.error)) { await setStepStatus(message.step, 'stopped'); await addLog(`Step ${message.step} stopped by user`, 'warn'); notifyStepError(message.step, message.error); } else { await setStepStatus(message.step, 'failed'); await addLog(`Step ${message.step} failed: ${message.error}`, 'error'); notifyStepError(message.step, message.error); } return { ok: true }; } case 'GET_STATE': { return await getState(); } case 'RESET': { clearStopRequest(); await resetState(); await addLog('Flow reset', 'info'); return { ok: true }; } case 'EXECUTE_STEP': { clearStopRequest(); const step = message.payload.step; // Save email if provided (from side panel step 3) if (message.payload.email) { await setEmailState(message.payload.email); } await executeStep(step); return { ok: true }; } case 'AUTO_RUN': { clearStopRequest(); const totalRuns = message.payload?.totalRuns || 1; autoRunLoop(totalRuns); // fire-and-forget return { ok: true }; } case 'RESUME_AUTO_RUN': { clearStopRequest(); if (message.payload.email) { await setEmailState(message.payload.email); } resumeAutoRun(); // fire-and-forget return { ok: true }; } case 'SAVE_SETTING': { const updates = {}; if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl; if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword; if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider; if (message.payload.tempApiUrl !== undefined) updates.tempApiUrl = message.payload.tempApiUrl; if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost; if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox; await setState(updates); return { ok: true }; } // Side panel data updates case 'SAVE_EMAIL': { await setEmailState(message.payload.email); return { ok: true, email: message.payload.email }; } case 'FETCH_DUCK_EMAIL': { clearStopRequest(); const email = await fetchDuckEmail(message.payload || {}); return { ok: true, email }; } case 'STOP_FLOW': { await requestStop(); return { ok: true }; } default: console.warn(LOG_PREFIX, `Unknown message type: ${message.type}`); return { error: `Unknown message type: ${message.type}` }; } } // ============================================================ // Step Data Handlers // ============================================================ async function handleStepData(step, payload) { switch (step) { case 1: if (payload.oauthUrl) { await setState({ oauthUrl: payload.oauthUrl }); broadcastDataUpdate({ oauthUrl: payload.oauthUrl }); } break; case 3: if (payload.email) await setEmailState(payload.email); break; case 4: if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp }); break; case 8: if (payload.localhostUrl) { await setState({ localhostUrl: payload.localhostUrl }); broadcastDataUpdate({ localhostUrl: payload.localhostUrl }); } break; } } // ============================================================ // Step Completion Waiting // ============================================================ // Map of step -> { resolve, reject } for waiting on step completion const stepWaiters = new Map(); let resumeWaiter = null; function waitForStepComplete(step, timeoutMs = 120000) { return new Promise((resolve, reject) => { throwIfStopped(); const timer = setTimeout(() => { stepWaiters.delete(step); reject(new Error(`Step ${step} timed out after ${timeoutMs / 1000}s`)); }, timeoutMs); stepWaiters.set(step, { resolve: (data) => { clearTimeout(timer); stepWaiters.delete(step); resolve(data); }, reject: (err) => { clearTimeout(timer); stepWaiters.delete(step); reject(err); }, }); }); } function notifyStepComplete(step, payload) { const waiter = stepWaiters.get(step); if (waiter) waiter.resolve(payload); } function notifyStepError(step, error) { const waiter = stepWaiters.get(step); if (waiter) waiter.reject(new Error(error)); } async function markRunningStepsStopped() { const state = await getState(); const runningSteps = Object.entries(state.stepStatuses || {}) .filter(([, status]) => status === 'running') .map(([step]) => Number(step)); for (const step of runningSteps) { await setStepStatus(step, 'stopped'); } } async function requestStop() { if (stopRequested) return; stopRequested = true; cancelPendingCommands(); if (webNavListener) { chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener); webNavListener = null; } await addLog('Stop requested. Cancelling current operations...', 'warn'); await broadcastStopToContentScripts(); for (const waiter of stepWaiters.values()) { waiter.reject(new Error(STOP_ERROR_MESSAGE)); } stepWaiters.clear(); if (resumeWaiter) { resumeWaiter.reject(new Error(STOP_ERROR_MESSAGE)); resumeWaiter = null; } await markRunningStepsStopped(); autoRunActive = false; await setState({ autoRunning: false }); chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: autoRunCurrentRun, totalRuns: autoRunTotalRuns }, }).catch(() => {}); } // ============================================================ // Step Execution // ============================================================ async function executeStep(step) { console.log(LOG_PREFIX, `Executing step ${step}`); throwIfStopped(); await setStepStatus(step, 'running'); await addLog(`Step ${step} started`); await humanStepDelay(); const state = await getState(); // Set flow start time on first step if (step === 1 && !state.flowStartTime) { await setState({ flowStartTime: Date.now() }); } try { switch (step) { case 1: await executeStep1(state); break; case 2: await executeStep2(state); break; case 3: await executeStep3(state); break; case 4: await executeStep4(state); break; case 5: await executeStep5(state); break; case 6: await executeStep6(state); break; case 7: await executeStep7(state); break; case 8: await executeStep8(state); break; case 9: await executeStep9(state); break; default: throw new Error(`Unknown step: ${step}`); } } catch (err) { if (isStopError(err)) { await setStepStatus(step, 'stopped'); await addLog(`Step ${step} stopped by user`, 'warn'); throw err; } await setStepStatus(step, 'failed'); await addLog(`Step ${step} failed: ${err.message}`, 'error'); throw err; } } /** * Execute a step and wait for it to complete before returning. * @param {number} step * @param {number} delayAfter - ms to wait after completion (for page transitions) */ async function executeStepAndWait(step, delayAfter = 2000) { throwIfStopped(); const promise = waitForStepComplete(step, 120000); await executeStep(step); await promise; // Extra delay for page transitions / DOM updates if (delayAfter > 0) { await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200)); } } async function fetchDuckEmail(options = {}) { throwIfStopped(); // Try API-based generation first (no tab needed) const state = await getState(); let token = state.duckToken; if (token) { try { const email = await generateDuckEmailViaApi(token); await setEmailState(email); await addLog(`Duck Mail: Generated ${email} (via API, no tab)`, 'ok'); return email; } catch (err) { await addLog(`Duck Mail: API generation failed: ${err.message}, falling back to page...`, 'warn'); // Token may be expired, clear it so we re-extract token = null; await setState({ duckToken: null }); } } // Fallback: open page, extract token for future use, then generate await addLog('Duck Mail: Opening page to extract token...'); await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL); // Try to extract and save the access token try { const tokenResult = await sendToContentScript('duck-mail', { type: 'EXTRACT_DUCK_TOKEN', source: 'background', payload: {}, }); if (tokenResult?.token) { token = tokenResult.token; await setState({ duckToken: token }); await addLog(`Duck Mail: Token saved for ${tokenResult.username}, future runs won't need page`, 'ok'); // Now use API to generate const email = await generateDuckEmailViaApi(token); await setEmailState(email); await addLog(`Duck Mail: Generated ${email} (via API)`, 'ok'); return email; } } catch (err) { await addLog(`Duck Mail: Token extraction failed: ${err.message}, using page method`, 'warn'); } // Final fallback: use the old page-based method const result = await sendToContentScript('duck-mail', { type: 'FETCH_DUCK_EMAIL', source: 'background', payload: { generateNew: true }, }); if (result?.error) throw new Error(result.error); if (!result?.email) throw new Error('Duck email not returned.'); await setEmailState(result.email); await addLog(`Duck Mail: ${result.generated ? 'Generated' : 'Loaded'} ${result.email} (via page)`, 'ok'); return result.email; } async function generateDuckEmailViaApi(token) { const resp = await fetch('https://quack.duckduckgo.com/api/email/addresses', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (!resp.ok) { throw new Error(`DuckDuckGo API returned ${resp.status}`); } const data = await resp.json(); // Response: { address: "random-words" } const address = data?.address; if (!address) throw new Error('No address in API response'); return `${address}@duck.com`; } // ============================================================ // Auto Run Flow // ============================================================ let autoRunActive = false; let autoRunCurrentRun = 0; let autoRunTotalRuns = 1; // Outer loop: runs the full flow N times async function autoRunLoop(totalRuns) { if (autoRunActive) { await addLog('Auto run already in progress', 'warn'); return; } clearStopRequest(); autoRunActive = true; autoRunTotalRuns = totalRuns; let successCount = 0; let failCount = 0; await setState({ autoRunning: true }); for (let run = 1; run <= totalRuns; run++) { autoRunCurrentRun = run; // Reset everything at the start of each run (keep VPS/mail settings) const prevState = await getState(); const keepSettings = { vpsUrl: prevState.vpsUrl, mailProvider: prevState.mailProvider, tempApiUrl: prevState.tempApiUrl, inbucketHost: prevState.inbucketHost, inbucketMailbox: prevState.inbucketMailbox, duckToken: prevState.duckToken, autoRunning: true, }; await resetState(); await setState(keepSettings); // Tell side panel to reset all UI chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {}); await sleepWithStop(500); await addLog(`=== Auto Run ${run}/${totalRuns} — Phase 1: Get OAuth link & open signup ===`, 'info'); const status = (phase) => ({ type: 'AUTO_RUN_STATUS', payload: { phase, currentRun: run, totalRuns, successCount } }); try { throwIfStopped(); chrome.runtime.sendMessage(status('running')).catch(() => {}); await executeStepAndWait(1, 2000); await executeStepAndWait(2, 2000); const duckEmail = await fetchDuckEmail({ generateNew: true }); await addLog(`=== Run ${run}/${totalRuns} — Duck email ready: ${duckEmail} ===`, 'ok'); await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info'); chrome.runtime.sendMessage(status('running')).catch(() => {}); const signupTabId = await getTabId('signup-page'); if (signupTabId) { await chrome.tabs.update(signupTabId, { active: true }); } await executeStepAndWait(3, 3000); await executeStepAndWait(4, 2000); await executeStepAndWait(5, 3000); await executeStepAndWait(6, 3000); // After login, check if page landed on add-phone again (not normal after step 5) const phoneCheckTabId = await getTabId('signup-page'); if (phoneCheckTabId) { try { const phoneTab = await chrome.tabs.get(phoneCheckTabId); if (phoneTab.url && (phoneTab.url.includes('add-phone') || phoneTab.url.includes('/phone'))) { throw new Error('PHONE_VERIFY_REQUIRED'); } } catch (e) { if (e.message === 'PHONE_VERIFY_REQUIRED') throw e; } } await executeStepAndWait(7, 2000); await executeStepAndWait(8, 2000); await executeStepAndWait(9, 1000); successCount++; await addLog(`=== Run ${run}/${totalRuns} COMPLETE! (${successCount} succeeded, ${failCount} failed) ===`, 'ok'); } catch (err) { if (isStopError(err)) { await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn'); chrome.runtime.sendMessage(status('stopped')).catch(() => {}); break; // Only stop on explicit user stop } else { failCount++; if (err.message === 'PHONE_VERIFY_REQUIRED') { await addLog(`Run ${run}/${totalRuns} failed: Phone verification required — sleeping 30s before next run (${successCount} succeeded, ${failCount} failed)`, 'error'); await sleepWithStop(30000); } else { await addLog(`Run ${run}/${totalRuns} failed: ${err.message} (${successCount} succeeded, ${failCount} failed)`, 'error'); } await addLog(`Skipping to next run...`, 'warn'); continue; } } } const attempted = autoRunCurrentRun; if (stopRequested) { await addLog(`=== Stopped after ${Math.max(0, attempted - 1)}/${autoRunTotalRuns} runs — ${successCount} succeeded, ${failCount} failed ===`, 'warn'); chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: attempted, totalRuns: autoRunTotalRuns, successCount, failCount } }).catch(() => {}); } else { await addLog(`=== All ${autoRunTotalRuns} runs finished — ${successCount} succeeded, ${failCount} failed ===`, successCount > 0 ? 'ok' : 'warn'); chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: attempted, totalRuns: autoRunTotalRuns, successCount, failCount } }).catch(() => {}); } autoRunActive = false; await setState({ autoRunning: false }); clearStopRequest(); } function waitForResume() { return new Promise((resolve, reject) => { throwIfStopped(); resumeWaiter = { resolve, reject }; }); } async function resumeAutoRun() { throwIfStopped(); const state = await getState(); if (!state.email) { await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error'); return; } if (resumeWaiter) { resumeWaiter.resolve(); resumeWaiter = null; } } // ============================================================ // Step 1: Get OAuth Link (via vps-panel.js) // ============================================================ async function executeStep1(state) { if (!state.vpsUrl) { throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.'); } await addLog(`Step 1: Opening VPS panel...`); await reuseOrCreateTab('vps-panel', state.vpsUrl, { inject: ['content/utils.js', 'content/vps-panel.js'], reloadIfSameUrl: true, }); await sendToContentScript('vps-panel', { type: 'EXECUTE_STEP', step: 1, source: 'background', payload: {}, }); } // ============================================================ // Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register) // ============================================================ async function executeStep2(state) { if (!state.oauthUrl) { throw new Error('No OAuth URL. Complete step 1 first.'); } await addLog(`Step 2: Opening auth URL...`); await reuseOrCreateTab('signup-page', state.oauthUrl); await sendToContentScript('signup-page', { type: 'EXECUTE_STEP', step: 2, source: 'background', payload: {}, }); } // ============================================================ // Step 3: Fill Email & Password (via signup-page.js) // ============================================================ async function executeStep3(state) { if (!state.email) { throw new Error('No email address. Paste email in Side Panel first.'); } const password = state.customPassword || generatePassword(); await setPasswordState(password); // Save account record const accounts = state.accounts || []; accounts.push({ email: state.email, password, createdAt: new Date().toISOString() }); await setState({ accounts }); await addLog( `Step 3: Filling email ${state.email}, password ${state.customPassword ? 'customized' : 'generated'} (${password.length} chars)` ); await sendToContentScript('signup-page', { type: 'EXECUTE_STEP', step: 3, source: 'background', payload: { email: state.email, password }, }); } // ============================================================ // Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js) // ============================================================ async function getMailConfig(state) { const provider = state.mailProvider || 'temp-api'; if (provider === 'temp-api') { let rawUrl = (state.tempApiUrl || '').trim(); // Fallback: read directly from chrome.storage.local in case session is stale if (!rawUrl) { const local = await chrome.storage.local.get('tempApiUrl'); rawUrl = (local.tempApiUrl || '').trim(); } if (!rawUrl) { return { error: 'Temp Email API URL is empty. Please paste the full URL with JWT in Side Panel.' }; } // Extract base origin and JWT from the URL (e.g. https://host/?jwt=xxx) let apiOrigin, jwt; try { const parsed = new URL(rawUrl); apiOrigin = parsed.origin; jwt = parsed.searchParams.get('jwt'); } catch { return { error: 'Temp Email API URL is invalid.' }; } if (!jwt) { return { error: 'Temp Email API URL has no jwt parameter.' }; } return { source: 'temp-api', apiOrigin, jwt, label: 'Temp Email API', useFetch: true, // signal that this provider uses fetch, not a tab }; } if (provider === '163') { return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 Mail' }; } if (provider === 'inbucket') { const host = normalizeInbucketOrigin(state.inbucketHost); const mailbox = (state.inbucketMailbox || '').trim(); if (!host) { return { error: 'Inbucket host is empty or invalid.' }; } if (!mailbox) { return { error: 'Inbucket mailbox name is empty.' }; } return { source: 'inbucket-mail', url: `${host}/m/${encodeURIComponent(mailbox)}/`, label: `Inbucket Mailbox (${mailbox})`, navigateOnReuse: true, inject: ['content/utils.js', 'content/inbucket-mail.js'], injectSource: 'inbucket-mail', }; } return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' }; } function normalizeInbucketOrigin(rawValue) { const value = (rawValue || '').trim(); if (!value) return ''; const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`; try { const parsed = new URL(candidate); return parsed.origin; } catch { return ''; } } // ============================================================ // Temp Email API: fetch-based polling (no tab needed) // ============================================================ function extractVerificationCodeFromText(text) { // Pattern 1: Chinese format "代码为 370794" or "验证码...370794" const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/); if (matchCn) return matchCn[1]; // Pattern 2: English format "code is 370794" or "code: 370794" const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i); if (matchEn) return matchEn[1] || matchEn[2]; // Pattern 3: standalone 6-digit number const match6 = text.match(/\b(\d{6})\b/); if (match6) return match6[1]; return null; } function decodeMimeWord(encoded) { // Decode =?charset?encoding?text?= (RFC 2047) return encoded.replace(/=\?([^?]+)\?(B|Q)\?([^?]*)\?=/gi, (_, charset, enc, text) => { if (enc.toUpperCase() === 'B') { // Base64 try { return atob(text); } catch { return text; } } // Quoted-Printable return text.replace(/_/g, ' ').replace(/=([0-9A-Fa-f]{2})/g, (__, hex) => String.fromCharCode(parseInt(hex, 16)) ); }); } function decodeUtf8Bytes(str) { // If the decoded string contains raw UTF-8 bytes, decode them try { return decodeURIComponent(escape(str)); } catch { return str; } } function extractSubjectFromRaw(raw) { // Extract Subject header (may span multiple lines with folding) const match = raw.match(/^Subject:\s*([\s\S]*?)(?=\r?\n[^\s]|\r?\n\r?\n)/mi); if (!match) return ''; // Unfold continuation lines and decode MIME words const unfolded = match[1].replace(/\r?\n\s+/g, ' ').trim(); return decodeUtf8Bytes(decodeMimeWord(unfolded)); } function extractFromHeader(raw) { const match = raw.match(/^From:\s*([\s\S]*?)(?=\r?\n[^\s]|\r?\n\r?\n)/mi); if (!match) return ''; return match[1].replace(/\r?\n\s+/g, ' ').trim().toLowerCase(); } function extractPlainTextFromRaw(raw) { // Try to extract the text/plain part from a multipart email const plainMatch = raw.match(/Content-Type:\s*text\/plain[\s\S]*?\r?\n\r?\n([\s\S]*?)(?:\r?\n--|\r?\n$)/i); if (plainMatch) { let text = plainMatch[1]; // Decode quoted-printable text = text.replace(/=\r?\n/g, ''); text = text.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))); try { return decodeURIComponent(escape(text)); } catch { return text; } } return raw; } async function pollTempEmailApi(mail, step, payload) { const { senderFilters, subjectFilters, maxAttempts, intervalMs, usedMailIds, usedCodes } = payload; const usedIdSet = new Set((usedMailIds || []).map(String)); const usedCodeSet = new Set(usedCodes || []); const headers = { 'Authorization': `Bearer ${mail.jwt}` }; for (let attempt = 1; attempt <= maxAttempts; attempt++) { throwIfStopped(); await addLog(`Step ${step}: Polling Temp Email API... attempt ${attempt}/${maxAttempts}`); try { const resp = await fetch(`${mail.apiOrigin}/api/mails?limit=10&offset=0`, { headers }); if (!resp.ok) throw new Error(`API returned ${resp.status}`); const data = await resp.json(); const mails = data.results || []; for (const m of mails) { if (usedIdSet.has(String(m.id))) continue; const raw = m.raw || ''; const source = (m.source || '').toLowerCase(); const fromHeader = extractFromHeader(raw); const subject = extractSubjectFromRaw(raw).toLowerCase(); const plainText = extractPlainTextFromRaw(raw); // Match against both envelope sender and From header const senderMatch = senderFilters.some(f => { const fl = f.toLowerCase(); return source.includes(fl) || fromHeader.includes(fl); }); const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase())); if (senderMatch || subjectMatch) { const code = extractVerificationCodeFromText(subject + ' ' + plainText); if (code) { if (usedCodeSet.has(code)) { console.log(LOG_PREFIX, `Step ${step}: Skipping already-used code ${code} (id: ${m.id})`); continue; } await addLog(`Step ${step}: Code found from API: ${code} (subject: ${subject.slice(0, 40)})`, 'ok'); return { ok: true, code, emailTimestamp: Date.now(), mailId: String(m.id) }; } } } } catch (err) { await addLog(`Step ${step}: API fetch error: ${err.message}`, 'warn'); } if (attempt < maxAttempts) { await sleepWithStop(intervalMs); } } throw new Error( `No matching email found via Temp Email API after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` + 'Email may be delayed.' ); } async function clickResendOnSignupPage(step) { const signupTabId = await getTabId('signup-page'); if (!signupTabId) return; await chrome.tabs.update(signupTabId, { active: true }); await sleepWithStop(500); try { await sendToContentScript('signup-page', { type: 'CLICK_RESEND_EMAIL', step, source: 'background', }); } catch (err) { await addLog(`Step ${step}: Resend click skipped: ${err.message}`, 'warn'); } } async function executeStep4(state) { // Click "重新发送电子邮件" on the signup page before polling await clickResendOnSignupPage(4); // Re-read state to ensure tempApiUrl is fresh (may have been saved after initial getState) const freshState = await getState(); const mail = await getMailConfig(freshState); if (mail.error) throw new Error(mail.error); const pollPayload = { filterAfterTimestamp: state.flowStartTime || 0, senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'], subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'], targetEmail: state.email, maxAttempts: 20, intervalMs: 3000, usedMailIds: state.usedMailIds || [], usedCodes: state.usedCodes || [], }; let result; if (mail.useFetch) { // Temp Email API: fetch directly, no tab needed await addLog(`Step 4: Polling ${mail.label}...`); result = await pollTempEmailApi(mail, 4, pollPayload); } else { 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) { if (mail.navigateOnReuse) { await reuseOrCreateTab(mail.source, mail.url, { inject: mail.inject, injectSource: mail.injectSource, }); } else { const tabId = await getTabId(mail.source); await chrome.tabs.update(tabId, { active: true }); } } else { await reuseOrCreateTab(mail.source, mail.url, { inject: mail.inject, injectSource: mail.injectSource, }); } result = await sendToContentScript(mail.source, { type: 'POLL_EMAIL', step: 4, source: 'background', payload: pollPayload, }); } if (result && result.error) { throw new Error(result.error); } if (result && result.code) { // Track used mail IDs and codes to avoid reusing stale ones const usedMailIds = [...(state.usedMailIds || [])]; const usedCodes = [...(state.usedCodes || [])]; if (result.mailId) usedMailIds.push(result.mailId); usedCodes.push(result.code); await setState({ lastEmailTimestamp: result.emailTimestamp, usedMailIds, usedCodes }); await addLog(`Step 4: Got verification code: ${result.code}`); // Switch to signup tab and fill code const signupTabId = await getTabId('signup-page'); if (signupTabId) { await chrome.tabs.update(signupTabId, { active: true }); await sendToContentScript('signup-page', { type: 'FILL_CODE', step: 4, source: 'background', payload: { code: result.code }, }); } else { throw new Error('Signup page tab was closed. Cannot fill verification code.'); } } } // ============================================================ // Step 5: Fill Name & Birthday (via signup-page.js) // ============================================================ async function executeStep5(state) { const { firstName, lastName } = generateRandomName(); const { year, month, day } = generateRandomBirthday(); // Save birthday data to state for potential reuse on about-you page await setState({ birthYear: year, birthMonth: month, birthDay: day, fullName: `${firstName} ${lastName}` }); await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`); await sendToContentScript('signup-page', { type: 'EXECUTE_STEP', step: 5, source: 'background', payload: { firstName, lastName, year, month, day }, }); } // ============================================================ // Step 6: Login ChatGPT (Background opens tab, chatgpt.js handles login) // ============================================================ async function executeStep6(state) { if (!state.oauthUrl) { throw new Error('No OAuth URL. Complete step 1 first.'); } if (!state.email) { throw new Error('No email. Complete step 3 first.'); } 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', { type: 'EXECUTE_STEP', step: 6, source: 'background', payload: { email: state.email, password: state.password }, }); } // ============================================================ // Step 7: Get Login Verification Code (qq-mail.js polls, then fills in chatgpt.js) // ============================================================ async function executeStep7(state) { // Check if the page is on /about-you (birthday not filled during signup) // and handle it before proceeding with verification code polling try { const aboutYouResult = await sendToContentScript('signup-page', { type: 'HANDLE_ABOUT_YOU', step: 7, source: 'background', payload: { year: state.birthYear, month: state.birthMonth, day: state.birthDay, fullName: state.fullName, }, }); if (aboutYouResult && aboutYouResult.handled) { await addLog('Step 7: Handled about-you page, waiting for next page...', 'info'); await sleep(3000); } } catch (e) { // Not on about-you page, continue normally } // Click "重新发送电子邮件" on the auth page before polling await clickResendOnSignupPage(7); const mail = await getMailConfig(state); if (mail.error) throw new Error(mail.error); // Re-read state to get latest usedMailIds/usedCodes const freshState = await getState(); const pollPayload = { filterAfterTimestamp: freshState.lastEmailTimestamp || freshState.flowStartTime || 0, senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'], subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'], targetEmail: freshState.email, maxAttempts: 20, intervalMs: 3000, usedMailIds: freshState.usedMailIds || [], usedCodes: freshState.usedCodes || [], }; let result; if (mail.useFetch) { await addLog(`Step 7: Polling ${mail.label}...`); result = await pollTempEmailApi(mail, 7, pollPayload); } else { await addLog(`Step 7: Opening ${mail.label}...`); const alive = await isTabAlive(mail.source); if (alive) { if (mail.navigateOnReuse) { await reuseOrCreateTab(mail.source, mail.url, { inject: mail.inject, injectSource: mail.injectSource, }); } else { const tabId = await getTabId(mail.source); await chrome.tabs.update(tabId, { active: true }); } } else { await reuseOrCreateTab(mail.source, mail.url, { inject: mail.inject, injectSource: mail.injectSource, }); } result = await sendToContentScript(mail.source, { type: 'POLL_EMAIL', step: 7, source: 'background', payload: pollPayload, }); } if (result && result.error) { throw new Error(result.error); } if (result && result.code) { // Track used mail IDs and codes const usedMailIds = [...(freshState.usedMailIds || [])]; const usedCodes = [...(freshState.usedCodes || [])]; if (result.mailId) usedMailIds.push(result.mailId); usedCodes.push(result.code); await setState({ usedMailIds, usedCodes }); await addLog(`Step 7: Got login verification code: ${result.code}`); // Switch to signup/auth tab and fill code const signupTabId = await getTabId('signup-page'); if (signupTabId) { await chrome.tabs.update(signupTabId, { active: true }); await sendToContentScript('signup-page', { type: 'FILL_CODE', step: 7, source: 'background', payload: { code: result.code }, }); } else { throw new Error('Auth page tab was closed. Cannot fill verification code.'); } } } // ============================================================ // Step 8: Complete OAuth (auto click + localhost listener) // ============================================================ let webNavListener = null; async function executeStep8(state) { if (!state.oauthUrl) { throw new Error('No OAuth URL. Complete step 1 first.'); } // Check if the signup tab already redirected to localhost before listener setup const signupTabIdEarly = await getTabId('signup-page'); if (signupTabIdEarly) { try { const tab = await chrome.tabs.get(signupTabIdEarly); if (tab.url && (tab.url.startsWith('http://localhost') || tab.url.startsWith('http://127.0.0.1'))) { await addLog(`Step 8: Localhost redirect already captured: ${tab.url}`, 'ok'); await setState({ localhostUrl: tab.url }); broadcastDataUpdate({ localhostUrl: tab.url }); return; } } catch {} } await addLog('Step 8: Setting up localhost redirect listener...'); // Register webNavigation listener (scoped to this step) return new Promise((resolve, reject) => { let resolved = false; const isLocalhostUrl = (url) => url && (url.startsWith('http://localhost') || url.startsWith('http://127.0.0.1')); const cleanupListeners = () => { if (webNavListener) { chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener); chrome.webNavigation.onCommitted.removeListener(webNavListener); chrome.webNavigation.onErrorOccurred.removeListener(webNavListener); webNavListener = null; } }; const captureLocalhostUrl = (url) => { if (resolved) return; resolved = true; cleanupListeners(); clearTimeout(timeout); setState({ localhostUrl: url }).then(() => { addLog(`Step 8: Captured localhost URL: ${url}`, 'ok'); setStepStatus(8, 'completed'); notifyStepComplete(8, { localhostUrl: url }); broadcastDataUpdate({ localhostUrl: url }); resolve(); }); }; const timeout = setTimeout(() => { cleanupListeners(); reject(new Error('Localhost redirect not captured after 120s. Step 8 click may have been blocked.')); }, 120000); webNavListener = (details) => { if (details.frameId === 0 && isLocalhostUrl(details.url)) { console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`); captureLocalhostUrl(details.url); } }; chrome.webNavigation.onBeforeNavigate.addListener(webNavListener); chrome.webNavigation.onCommitted.addListener(webNavListener); chrome.webNavigation.onErrorOccurred.addListener(webNavListener); // After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex") // with a "继续" button. Content script clicks it directly (no debugger needed). (async () => { try { let signupTabId = await getTabId('signup-page'); if (signupTabId) { await chrome.tabs.update(signupTabId, { active: true }); await addLog('Step 8: Switched to auth page...'); } else { signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl); await addLog('Step 8: Auth tab reopened...'); } const clickResult = await sendToContentScript('signup-page', { type: 'STEP8_FIND_AND_CLICK', source: 'background', payload: {}, }); if (clickResult?.error) { throw new Error(clickResult.error); } await addLog('Step 8: "继续" button clicked, waiting for redirect...'); // Poll tab URL in case webNavigation listeners missed the redirect for (let i = 0; i < 30 && !resolved; i++) { await new Promise(r => setTimeout(r, 1000)); try { const tab = await chrome.tabs.get(signupTabId); if (isLocalhostUrl(tab.url)) { captureLocalhostUrl(tab.url); break; } } catch { break; } } } catch (err) { clearTimeout(timeout); cleanupListeners(); reject(err); } })(); }); } // ============================================================ // Step 9: VPS Verify (via vps-panel.js) // ============================================================ async function executeStep9(state) { if (!state.localhostUrl) { throw new Error('No localhost URL. Complete step 8 first.'); } 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...'); let tabId = await getTabId('vps-panel'); const alive = tabId && await isTabAlive('vps-panel'); if (!alive) { // Create new tab in the automation window const wid = await ensureAutomationWindowId(); const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true, windowId: wid }); tabId = tab.id; 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); }); } else { await chrome.tabs.update(tabId, { active: true }); } // Inject scripts directly and wait for them to be ready await chrome.scripting.executeScript({ target: { tabId }, files: ['content/utils.js', 'content/vps-panel.js'], }); await new Promise(r => setTimeout(r, 1000)); // Send command directly — bypass queue/ready mechanism await addLog(`Step 9: Filling callback URL...`); await chrome.tabs.sendMessage(tabId, { type: 'EXECUTE_STEP', step: 9, source: 'background', payload: { localhostUrl: state.localhostUrl }, }); } // ============================================================ // Open Side Panel on extension icon click // ============================================================ chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });