// 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(); // ============================================================ // State Management (chrome.storage.session) // ============================================================ 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: '163', // 'qq' or '163' 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); } } async function setState(updates) { console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200)); await chrome.storage.session.set(updates); } 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', 'inbucketMailbox', ]); await chrome.storage.session.clear(); await chrome.storage.session.set({ ...DEFAULT_STATE, seenCodes: prev.seenCodes || [], seenInbucketMailIds: prev.seenInbucketMailIds || [], accounts: prev.accounts || [], tabRegistry: prev.tabRegistry || {}, vpsUrl: prev.vpsUrl || '', customPassword: prev.customPassword || '', mailProvider: prev.mailProvider || '163', inbucketMailbox: prev.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 }); 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) { 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 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 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); }); 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); return chrome.tabs.sendMessage(entry.tabId, message); } // ============================================================ // 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); } async function clickWithDebugger(tabId, rect) { if (!tabId) { throw new Error('No auth tab found for debugger click.'); } if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) { throw new Error('Step 8 debugger fallback needs a valid button position.'); } const target = { tabId }; try { await chrome.debugger.attach(target, '1.3'); } catch (err) { throw new Error( `Debugger attach failed during step 8 fallback: ${err.message}. ` + 'If DevTools is open on the auth tab, close it and retry.' ); } try { const x = Math.round(rect.centerX); const y = Math.round(rect.centerY); await chrome.debugger.sendCommand(target, 'Page.bringToFront'); await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x, y, button: 'none', buttons: 0, clickCount: 0, }); await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', buttons: 1, clickCount: 1, }); await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', buttons: 0, clickCount: 1, }); } finally { await chrome.debugger.detach(target).catch(() => {}); } } 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.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(); const { generateNew = true } = options; await addLog(`Duck Mail: Opening autofill settings (${generateNew ? 'generate new' : 'reuse current'})...`); await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL); const result = await sendToContentScript('duck-mail', { type: 'FETCH_DUCK_EMAIL', source: 'background', payload: { generateNew }, }); 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}`, 'ok'); return result.email; } // ============================================================ // 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; 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, inbucketMailbox: prevState.inbucketMailbox, 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 } }); try { throwIfStopped(); chrome.runtime.sendMessage(status('running')).catch(() => {}); await executeStepAndWait(1, 2000); await executeStepAndWait(2, 2000); let emailReady = false; try { const duckEmail = await fetchDuckEmail({ generateNew: true }); await addLog(`=== Run ${run}/${totalRuns} — Duck email ready: ${duckEmail} ===`, 'ok'); emailReady = true; } catch (err) { await addLog(`Duck Mail auto-fetch failed: ${err.message}`, 'warn'); } if (!emailReady) { await addLog(`=== Run ${run}/${totalRuns} PAUSED: Fetch Duck email or paste manually, then continue ===`, 'warn'); chrome.runtime.sendMessage(status('waiting_email')).catch(() => {}); // Wait for RESUME_AUTO_RUN — sets a promise that resumeAutoRun resolves await waitForResume(); const resumedState = await getState(); if (!resumedState.email) { await addLog('Cannot resume: no email address.', 'error'); break; } } 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); await executeStepAndWait(7, 2000); await executeStepAndWait(8, 2000); await executeStepAndWait(9, 1000); await addLog(`=== Run ${run}/${totalRuns} COMPLETE! ===`, 'ok'); } catch (err) { if (isStopError(err)) { await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn'); } else { await addLog(`Run ${run}/${totalRuns} failed: ${err.message}`, 'error'); } chrome.runtime.sendMessage(status('stopped')).catch(() => {}); break; // Stop on error } } const completedRuns = autoRunCurrentRun; if (stopRequested) { await addLog(`=== Stopped after ${Math.max(0, completedRuns - 1)}/${autoRunTotalRuns} runs ===`, 'warn'); chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {}); } else if (completedRuns >= autoRunTotalRuns) { await addLog(`=== All ${autoRunTotalRuns} runs completed successfully ===`, 'ok'); chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {}); } else { await addLog(`=== Stopped after ${completedRuns}/${autoRunTotalRuns} runs ===`, 'warn'); chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).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) // ============================================================ function getMailConfig(state) { const provider = state.mailProvider || 'qq'; 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 mailbox = (state.inbucketMailbox || '').trim(); if (!mailbox) { return { error: 'Inbucket mailbox name is empty.' }; } return { source: 'inbucket-mail', url: `https://inbucket.j2to.de/m/${encodeURIComponent(mailbox)}/`, label: `Inbucket Mailbox (${mailbox})`, navigateOnReuse: true, }; } return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' }; } async function executeStep4(state) { const mail = getMailConfig(state); if (mail.error) throw new Error(mail.error); 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); } else { const tabId = await getTabId(mail.source); await chrome.tabs.update(tabId, { active: true }); } } else { await reuseOrCreateTab(mail.source, mail.url); } const result = await sendToContentScript(mail.source, { type: 'POLL_EMAIL', step: 4, source: 'background', payload: { filterAfterTimestamp: state.flowStartTime || 0, senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'], subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'], targetEmail: state.email, maxAttempts: 20, intervalMs: 3000, }, }); if (result && result.error) { throw new Error(result.error); } if (result && result.code) { await setState({ lastEmailTimestamp: result.emailTimestamp }); 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(); 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) { const mail = getMailConfig(state); if (mail.error) throw new Error(mail.error); await addLog(`Step 7: Opening ${mail.label}...`); const alive = await isTabAlive(mail.source); if (alive) { if (mail.navigateOnReuse) { await reuseOrCreateTab(mail.source, mail.url); } else { const tabId = await getTabId(mail.source); await chrome.tabs.update(tabId, { active: true }); } } else { await reuseOrCreateTab(mail.source, mail.url); } const result = await sendToContentScript(mail.source, { type: 'POLL_EMAIL', step: 7, source: 'background', payload: { filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0, senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'], subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'], targetEmail: state.email, maxAttempts: 20, intervalMs: 3000, }, }); if (result && result.error) { throw new Error(result.error); } if (result && result.code) { 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.'); } await addLog('Step 8: Setting up localhost redirect listener...'); // Register webNavigation listener (scoped to this step) return new Promise((resolve, reject) => { let resolved = false; let resolveCaptureWait = null; const captureWait = new Promise((resolveCapture) => { resolveCaptureWait = resolveCapture; }); const cleanupListener = () => { if (webNavListener) { chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener); webNavListener = null; } }; const timeout = setTimeout(() => { cleanupListener(); reject(new Error('Localhost redirect not captured after 120s. Step 8 click may have been blocked.')); }, 120000); webNavListener = (details) => { if (details.url.startsWith('http://localhost')) { console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`); resolved = true; cleanupListener(); clearTimeout(timeout); if (resolveCaptureWait) resolveCaptureWait(details.url); setState({ localhostUrl: details.url }).then(() => { addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok'); setStepStatus(8, 'completed'); notifyStepComplete(8, { localhostUrl: details.url }); broadcastDataUpdate({ localhostUrl: details.url }); resolve(); }); } }; chrome.webNavigation.onBeforeNavigate.addListener(webNavListener); // After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex") // with a "继续" button. We locate the button in-page, then click it through // the debugger Input API directly. (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. Preparing debugger click...'); } else { signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl); await addLog('Step 8: Auth tab reopened. Preparing debugger click...'); } const clickResult = await sendToContentScript('signup-page', { type: 'STEP8_FIND_AND_CLICK', source: 'background', payload: {}, }); if (clickResult?.error) { throw new Error(clickResult.error); } if (!resolved) { await clickWithDebugger(signupTabId, clickResult?.rect); await addLog('Step 8: Debugger click dispatched, waiting for redirect...'); } } catch (err) { clearTimeout(timeout); cleanupListener(); 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 const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true }); 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 });