From 96867783dd7cf839ce3474c8672c4b8437265a32 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Wed, 8 Apr 2026 01:14:22 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BF=BB=E8=AF=91=E7=95=8C=E9=9D=A2=E5=92=8C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E4=BF=A1=E6=81=AF=E4=B8=BA=E4=B8=AD=E6=96=87?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=20OAuth=20=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E6=AD=A5=E9=AA=A4=E6=8F=8F=E8=BF=B0=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AE=A1=E7=90=86=E5=AF=86=E9=92=A5=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=A1=86=E5=92=8C=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=20CSS=20=E6=A0=B7=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86=E9=80=BB=E8=BE=91=E9=94=99?= =?UTF-8?q?=E8=AF=AF=EF=BC=8C=E7=A1=AE=E4=BF=9D=E8=87=AA=E5=8A=A8=E5=8C=96?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E7=9A=84=E7=A8=B3=E5=AE=9A=E6=80=A7=E5=92=8C?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 528 +++++++++++++++++++++++++++------------ content/duck-mail.js | 16 +- content/inbucket-mail.js | 36 +-- content/mail-163.js | 57 +++-- content/qq-mail.js | 31 ++- content/signup-page.js | 480 ++++++++++++++++++++++++++++++----- content/utils.js | 52 ++-- content/vps-panel.js | 240 ++++++++++++++---- manifest.json | 4 +- sidepanel/sidepanel.css | 23 ++ sidepanel/sidepanel.html | 89 ++++--- sidepanel/sidepanel.js | 111 +++++--- 12 files changed, 1228 insertions(+), 439 deletions(-) diff --git a/background.js b/background.js index 437f0fa..a6ec585 100644 --- a/background.js +++ b/background.js @@ -4,7 +4,7 @@ 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 STOP_ERROR_MESSAGE = '流程已被用户停止。'; const HUMAN_STEP_DELAY_MIN = 700; const HUMAN_STEP_DELAY_MAX = 2200; @@ -25,12 +25,16 @@ const DEFAULT_STATE = { password: null, accounts: [], // { email, password, createdAt } lastEmailTimestamp: null, + lastSignupCode: null, + lastLoginCode: null, localhostUrl: null, flowStartTime: null, tabRegistry: {}, logs: [], vpsUrl: '', + vpsPassword: '', customPassword: '', + autoRunSkipFailures: false, mailProvider: '163', // 'qq' or '163' inbucketHost: '', inbucketMailbox: '', @@ -85,7 +89,9 @@ async function resetState() { 'accounts', 'tabRegistry', 'vpsUrl', + 'vpsPassword', 'customPassword', + 'autoRunSkipFailures', 'mailProvider', 'inbucketHost', 'inbucketMailbox', @@ -98,7 +104,9 @@ async function resetState() { accounts: prev.accounts || [], tabRegistry: prev.tabRegistry || {}, vpsUrl: prev.vpsUrl || '', + vpsPassword: prev.vpsPassword || '', customPassword: prev.customPassword || '', + autoRunSkipFailures: Boolean(prev.autoRunSkipFailures), mailProvider: prev.mailProvider || '163', inbucketHost: prev.inbucketHost || '', inbucketMailbox: prev.inbucketMailbox || '', @@ -383,6 +391,19 @@ async function addLog(message, level = 'info') { chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {}); } +function getSourceLabel(source) { + const labels = { + 'sidepanel': '侧边栏', + 'signup-page': '认证页', + 'vps-panel': 'VPS 面板', + 'qq-mail': 'QQ 邮箱', + 'mail-163': '163 邮箱', + 'inbucket-mail': 'Inbucket 邮箱', + 'duck-mail': 'Duck 邮箱', + }; + return labels[source] || source || '未知来源'; +} + // ============================================================ // Step Status Management // ============================================================ @@ -429,10 +450,10 @@ async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY async function clickWithDebugger(tabId, rect) { if (!tabId) { - throw new Error('No auth tab found for debugger click.'); + throw new Error('未找到用于调试点击的认证页面标签页。'); } if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) { - throw new Error('Step 8 debugger fallback needs a valid button position.'); + throw new Error('步骤 8 的调试器兜底点击需要有效的按钮坐标。'); } const target = { tabId }; @@ -440,8 +461,8 @@ async function clickWithDebugger(tabId, rect) { 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.' + `步骤 8 的调试器兜底点击附加失败:${err.message}。` + + '如果认证页标签已打开 DevTools,请先关闭后重试。' ); } @@ -519,14 +540,14 @@ async function handleMessage(message, sender) { if (tabId && message.source) { await registerTab(message.source, tabId); flushCommand(message.source, tabId); - await addLog(`Content script ready: ${message.source} (tab ${tabId})`); + await addLog(`内容脚本已就绪:${getSourceLabel(message.source)}(标签页 ${tabId})`); } return { ok: true }; } case 'LOG': { const { message: msg, level } = message.payload; - await addLog(`[${message.source}] ${msg}`, level); + await addLog(`[${getSourceLabel(message.source)}] ${msg}`, level); return { ok: true }; } @@ -537,7 +558,7 @@ async function handleMessage(message, sender) { return { ok: true }; } await setStepStatus(message.step, 'completed'); - await addLog(`Step ${message.step} completed`, 'ok'); + await addLog(`步骤 ${message.step} 已完成`, 'ok'); await handleStepData(message.step, message.payload); notifyStepComplete(message.step, message.payload); return { ok: true }; @@ -546,11 +567,11 @@ async function handleMessage(message, sender) { case 'STEP_ERROR': { if (isStopError(message.error)) { await setStepStatus(message.step, 'stopped'); - await addLog(`Step ${message.step} stopped by user`, 'warn'); + await addLog(`步骤 ${message.step} 已被用户停止`, 'warn'); notifyStepError(message.step, message.error); } else { await setStepStatus(message.step, 'failed'); - await addLog(`Step ${message.step} failed: ${message.error}`, 'error'); + await addLog(`步骤 ${message.step} 失败:${message.error}`, 'error'); notifyStepError(message.step, message.error); } return { ok: true }; @@ -563,7 +584,7 @@ async function handleMessage(message, sender) { case 'RESET': { clearStopRequest(); await resetState(); - await addLog('Flow reset', 'info'); + await addLog('流程已重置', 'info'); return { ok: true }; } @@ -581,7 +602,9 @@ async function handleMessage(message, sender) { case 'AUTO_RUN': { clearStopRequest(); const totalRuns = message.payload?.totalRuns || 1; - autoRunLoop(totalRuns); // fire-and-forget + const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures); + await setState({ autoRunSkipFailures }); + autoRunLoop(totalRuns, { autoRunSkipFailures }); // fire-and-forget return { ok: true }; } @@ -597,7 +620,9 @@ async function handleMessage(message, sender) { case 'SAVE_SETTING': { const updates = {}; if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl; + if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword; if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword; + if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures); if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider; if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost; if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox; @@ -688,6 +713,19 @@ function notifyStepError(step, error) { if (waiter) waiter.reject(new Error(error)); } +async function completeStepFromBackground(step, payload = {}) { + if (stopRequested) { + await setStepStatus(step, 'stopped'); + notifyStepError(step, STOP_ERROR_MESSAGE); + return; + } + + await setStepStatus(step, 'completed'); + await addLog(`步骤 ${step} 已完成`, 'ok'); + await handleStepData(step, payload); + notifyStepComplete(step, payload); +} + async function markRunningStepsStopped() { const state = await getState(); const runningSteps = Object.entries(state.stepStatuses || {}) @@ -709,7 +747,7 @@ async function requestStop() { webNavListener = null; } - await addLog('Stop requested. Cancelling current operations...', 'warn'); + await addLog('已收到停止请求,正在取消当前操作...', 'warn'); await broadcastStopToContentScripts(); for (const waiter of stepWaiters.values()) { @@ -739,7 +777,7 @@ async function executeStep(step) { console.log(LOG_PREFIX, `Executing step ${step}`); throwIfStopped(); await setStepStatus(step, 'running'); - await addLog(`Step ${step} started`); + await addLog(`步骤 ${step} 开始执行`); await humanStepDelay(); const state = await getState(); @@ -761,16 +799,16 @@ async function executeStep(step) { case 8: await executeStep8(state); break; case 9: await executeStep9(state); break; default: - throw new Error(`Unknown step: ${step}`); + throw new Error(`未知步骤:${step}`); } } catch (err) { if (isStopError(err)) { await setStepStatus(step, 'stopped'); - await addLog(`Step ${step} stopped by user`, 'warn'); + await addLog(`步骤 ${step} 已被用户停止`, 'warn'); throw err; } await setStepStatus(step, 'failed'); - await addLog(`Step ${step} failed: ${err.message}`, 'error'); + await addLog(`步骤 ${step} 失败:${err.message}`, 'error'); throw err; } } @@ -795,7 +833,7 @@ async function fetchDuckEmail(options = {}) { throwIfStopped(); const { generateNew = true } = options; - await addLog(`Duck Mail: Opening autofill settings (${generateNew ? 'generate new' : 'reuse current'})...`); + await addLog(`Duck 邮箱:正在打开自动填充设置(${generateNew ? '生成新地址' : '复用当前地址'})...`); await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL); const result = await sendToContentScript('duck-mail', { @@ -808,11 +846,11 @@ async function fetchDuckEmail(options = {}) { throw new Error(result.error); } if (!result?.email) { - throw new Error('Duck email not returned.'); + throw new Error('未返回 Duck 邮箱地址。'); } await setEmailState(result.email); - await addLog(`Duck Mail: ${result.generated ? 'Generated' : 'Loaded'} ${result.email}`, 'ok'); + await addLog(`Duck 邮箱:${result.generated ? '已生成' : '已读取'} ${result.email}`, 'ok'); return result.email; } @@ -824,38 +862,58 @@ let autoRunActive = false; let autoRunCurrentRun = 0; let autoRunTotalRuns = 1; -// Outer loop: runs the full flow N times -async function autoRunLoop(totalRuns) { +// Outer loop: keep retrying until the target number of successful runs is reached. +async function autoRunLoop(totalRuns, options = {}) { if (autoRunActive) { - await addLog('Auto run already in progress', 'warn'); + await addLog('自动运行已在进行中', 'warn'); return; } clearStopRequest(); autoRunActive = true; autoRunTotalRuns = totalRuns; - await setState({ autoRunning: true }); + const autoRunSkipFailures = Boolean(options.autoRunSkipFailures); + const maxAttempts = autoRunSkipFailures ? Math.max(totalRuns * 10, totalRuns + 20) : totalRuns; + let successfulRuns = 0; + let attemptRuns = 0; + let forceFreshTabsNextRun = false; - for (let run = 1; run <= totalRuns; run++) { - autoRunCurrentRun = run; + await setState({ autoRunning: true, autoRunSkipFailures }); - // Reset everything at the start of each run (keep VPS/mail settings) + while (successfulRuns < totalRuns && attemptRuns < maxAttempts) { + attemptRuns += 1; + const targetRun = successfulRuns + 1; + autoRunCurrentRun = targetRun; + + // Reset everything at the start of each attempt (keep user settings). const prevState = await getState(); const keepSettings = { vpsUrl: prevState.vpsUrl, + vpsPassword: prevState.vpsPassword, + customPassword: prevState.customPassword, + autoRunSkipFailures: prevState.autoRunSkipFailures, mailProvider: prevState.mailProvider, inbucketHost: prevState.inbucketHost, inbucketMailbox: prevState.inbucketMailbox, autoRunning: true, + ...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}), }; 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 } }); + if (forceFreshTabsNextRun) { + await addLog(`兜底模式:上一轮已放弃,当前开始第 ${attemptRuns} 次尝试,将使用新线程继续补足第 ${targetRun}/${totalRuns} 轮。`, 'warn'); + forceFreshTabsNextRun = false; + } + + const status = (phase) => ({ + type: 'AUTO_RUN_STATUS', + payload: { phase, currentRun: targetRun, totalRuns, attemptRun: attemptRuns }, + }); + + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info'); try { throwIfStopped(); @@ -867,27 +925,25 @@ async function autoRunLoop(totalRuns) { let emailReady = false; try { const duckEmail = await fetchDuckEmail({ generateNew: true }); - await addLog(`=== Run ${run}/${totalRuns} — Duck email ready: ${duckEmail} ===`, 'ok'); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:Duck 邮箱已就绪:${duckEmail}(第 ${attemptRuns} 次尝试)===`, 'ok'); emailReady = true; } catch (err) { - await addLog(`Duck Mail auto-fetch failed: ${err.message}`, 'warn'); + await addLog(`Duck 邮箱自动获取失败:${err.message}`, 'warn'); } if (!emailReady) { - await addLog(`=== Run ${run}/${totalRuns} PAUSED: Fetch Duck email or paste manually, then continue ===`, 'warn'); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先获取 Duck 邮箱或手动粘贴邮箱,然后继续 ===`, '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; + throw new Error('无法继续:当前没有邮箱地址。'); } } - await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info'); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,注册、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info'); chrome.runtime.sendMessage(status('running')).catch(() => {}); const signupTabId = await getTabId('signup-page'); @@ -903,29 +959,56 @@ async function autoRunLoop(totalRuns) { await executeStepAndWait(8, 2000); await executeStepAndWait(9, 1000); - await addLog(`=== Run ${run}/${totalRuns} COMPLETE! ===`, 'ok'); - + successfulRuns += 1; + autoRunCurrentRun = successfulRuns; + await addLog(`=== 目标 ${successfulRuns}/${totalRuns} 轮已完成(第 ${attemptRuns} 次尝试成功)===`, 'ok'); + continue; } catch (err) { if (isStopError(err)) { - await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn'); - } else { - await addLog(`Run ${run}/${totalRuns} failed: ${err.message}`, 'error'); + await addLog(`目标 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); + chrome.runtime.sendMessage(status('stopped')).catch(() => {}); + break; } - chrome.runtime.sendMessage(status('stopped')).catch(() => {}); - break; // Stop on error + + if (!autoRunSkipFailures) { + await addLog(`目标 ${targetRun}/${totalRuns} 轮失败:${err.message}`, 'error'); + chrome.runtime.sendMessage(status('stopped')).catch(() => {}); + break; + } + + await addLog(`目标 ${targetRun}/${totalRuns} 轮的第 ${attemptRuns} 次尝试失败:${err.message}`, 'error'); + await addLog('兜底开关已开启:将放弃当前线程,重新开一轮继续补足目标次数。', 'warn'); + cancelPendingCommands('当前尝试已放弃。'); + await broadcastStopToContentScripts(); + chrome.runtime.sendMessage(status('retrying')).catch(() => {}); + forceFreshTabsNextRun = true; } } - 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(() => {}); + if (!stopRequested && autoRunSkipFailures && successfulRuns < totalRuns && attemptRuns >= maxAttempts) { + await addLog(`已达到安全重试上限(${attemptRuns} 次尝试),当前仅完成 ${successfulRuns}/${totalRuns} 轮。`, 'error'); + chrome.runtime.sendMessage({ + type: 'AUTO_RUN_STATUS', + payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns }, + }).catch(() => {}); + } else if (stopRequested) { + await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn'); + chrome.runtime.sendMessage({ + type: 'AUTO_RUN_STATUS', + payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns }, + }).catch(() => {}); + } else if (successfulRuns >= autoRunTotalRuns) { + await addLog(`=== 全部 ${autoRunTotalRuns} 轮均已成功完成,共尝试 ${attemptRuns} 次 ===`, 'ok'); + chrome.runtime.sendMessage({ + type: 'AUTO_RUN_STATUS', + payload: { phase: 'complete', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns }, + }).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(() => {}); + await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn'); + chrome.runtime.sendMessage({ + type: 'AUTO_RUN_STATUS', + payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns }, + }).catch(() => {}); } autoRunActive = false; await setState({ autoRunning: false }); @@ -943,7 +1026,7 @@ 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'); + await addLog('无法继续:当前没有邮箱地址,请先在侧边栏填写邮箱。', 'error'); return; } if (resumeWaiter) { @@ -958,9 +1041,9 @@ async function resumeAutoRun() { async function executeStep1(state) { if (!state.vpsUrl) { - throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.'); + throw new Error('尚未配置 VPS 地址,请先在侧边栏填写。'); } - await addLog(`Step 1: Opening VPS panel...`); + await addLog('步骤 1:正在打开 VPS 面板...'); await reuseOrCreateTab('vps-panel', state.vpsUrl, { inject: ['content/utils.js', 'content/vps-panel.js'], reloadIfSameUrl: true, @@ -970,7 +1053,7 @@ async function executeStep1(state) { type: 'EXECUTE_STEP', step: 1, source: 'background', - payload: {}, + payload: { vpsPassword: state.vpsPassword }, }); } @@ -980,9 +1063,9 @@ async function executeStep1(state) { async function executeStep2(state) { if (!state.oauthUrl) { - throw new Error('No OAuth URL. Complete step 1 first.'); + throw new Error('缺少 OAuth 链接,请先完成步骤 1。'); } - await addLog(`Step 2: Opening auth URL...`); + await addLog('步骤 2:正在打开认证链接...'); await reuseOrCreateTab('signup-page', state.oauthUrl); await sendToContentScript('signup-page', { @@ -999,7 +1082,7 @@ async function executeStep2(state) { async function executeStep3(state) { if (!state.email) { - throw new Error('No email address. Paste email in Side Panel first.'); + throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。'); } const password = state.customPassword || generatePassword(); @@ -1011,7 +1094,7 @@ async function executeStep3(state) { await setState({ accounts }); await addLog( - `Step 3: Filling email ${state.email}, password ${state.customPassword ? 'customized' : 'generated'} (${password.length} chars)` + `步骤 3:正在填写邮箱 ${state.email},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)` ); await sendToContentScript('signup-page', { type: 'EXECUTE_STEP', @@ -1028,27 +1111,27 @@ async function executeStep3(state) { 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' }; + 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 邮箱' }; } if (provider === 'inbucket') { const host = normalizeInbucketOrigin(state.inbucketHost); const mailbox = (state.inbucketMailbox || '').trim(); if (!host) { - return { error: 'Inbucket host is empty or invalid.' }; + return { error: 'Inbucket 主机地址为空或无效。' }; } if (!mailbox) { - return { error: 'Inbucket mailbox name is empty.' }; + return { error: 'Inbucket 邮箱名称为空。' }; } return { source: 'inbucket-mail', url: `${host}/m/${encodeURIComponent(mailbox)}/`, - label: `Inbucket Mailbox (${mailbox})`, + label: `Inbucket 邮箱(${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' }; + return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ 邮箱' }; } function normalizeInbucketOrigin(rawValue) { @@ -1065,10 +1148,189 @@ function normalizeInbucketOrigin(rawValue) { } } +function getVerificationCodeStateKey(step) { + return step === 4 ? 'lastSignupCode' : 'lastLoginCode'; +} + +function getVerificationCodeLabel(step) { + return step === 4 ? '注册' : '登录'; +} + +function getVerificationPollPayload(step, state, overrides = {}) { + if (step === 4) { + return { + filterAfterTimestamp: state.flowStartTime || 0, + senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'], + subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm'], + targetEmail: state.email, + maxAttempts: 5, + intervalMs: 3000, + ...overrides, + }; + } + + return { + filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0, + senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'], + subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm', 'login'], + targetEmail: state.email, + maxAttempts: 5, + intervalMs: 3000, + ...overrides, + }; +} + +async function requestVerificationCodeResend(step) { + const signupTabId = await getTabId('signup-page'); + if (!signupTabId) { + throw new Error('认证页面标签页已关闭,无法重新请求验证码。'); + } + + await chrome.tabs.update(signupTabId, { active: true }); + await addLog(`步骤 ${step}:正在请求新的${getVerificationCodeLabel(step)}验证码...`, 'warn'); + + const result = await sendToContentScript('signup-page', { + type: 'RESEND_VERIFICATION_CODE', + step, + source: 'background', + payload: {}, + }); + + if (result && result.error) { + throw new Error(result.error); + } + + return Date.now(); +} + +async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) { + const stateKey = getVerificationCodeStateKey(step); + const rejectedCodes = new Set(); + if (state[stateKey]) { + rejectedCodes.add(state[stateKey]); + } + for (const code of (pollOverrides.excludeCodes || [])) { + if (code) rejectedCodes.add(code); + } + + let lastError = null; + let filterAfterTimestamp = pollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp; + const maxRounds = pollOverrides.maxRounds || 3; + + for (let round = 1; round <= maxRounds; round++) { + if (round > 1) { + filterAfterTimestamp = await requestVerificationCodeResend(step); + } + + const payload = getVerificationPollPayload(step, state, { + ...pollOverrides, + filterAfterTimestamp, + excludeCodes: [...rejectedCodes], + }); + + try { + const result = await sendToContentScript(mail.source, { + type: 'POLL_EMAIL', + step, + source: 'background', + payload, + }); + + if (result && result.error) { + throw new Error(result.error); + } + + if (!result || !result.code) { + throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`); + } + + if (rejectedCodes.has(result.code)) { + throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`); + } + + return result; + } catch (err) { + lastError = err; + await addLog(`步骤 ${step}:${err.message}`, 'warn'); + if (round < maxRounds) { + await addLog(`步骤 ${step}:将重新发送验证码后重试(${round + 1}/${maxRounds})...`, 'warn'); + } + } + } + + throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`); +} + +async function submitVerificationCode(step, code) { + const signupTabId = await getTabId('signup-page'); + if (!signupTabId) { + throw new Error('认证页面标签页已关闭,无法填写验证码。'); + } + + await chrome.tabs.update(signupTabId, { active: true }); + const result = await sendToContentScript('signup-page', { + type: 'FILL_CODE', + step, + source: 'background', + payload: { code }, + }); + + if (result && result.error) { + throw new Error(result.error); + } + + return result || {}; +} + +async function resolveVerificationStep(step, state, mail) { + const stateKey = getVerificationCodeStateKey(step); + const rejectedCodes = new Set(); + if (state[stateKey]) { + rejectedCodes.add(state[stateKey]); + } + + let nextFilterAfterTimestamp = null; + const maxSubmitAttempts = 3; + + for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) { + const result = await pollFreshVerificationCode(step, state, mail, { + excludeCodes: [...rejectedCodes], + filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined, + }); + + await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`); + const submitResult = await submitVerificationCode(step, result.code); + + if (submitResult.invalidCode) { + rejectedCodes.add(result.code); + await addLog(`步骤 ${step}:验证码被页面拒绝:${submitResult.errorText || result.code}`, 'warn'); + + if (attempt >= maxSubmitAttempts) { + throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`); + } + + nextFilterAfterTimestamp = await requestVerificationCodeResend(step); + await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn'); + continue; + } + + await setState({ + lastEmailTimestamp: result.emailTimestamp, + [stateKey]: result.code, + }); + + await completeStepFromBackground(step, { + emailTimestamp: result.emailTimestamp, + code: result.code, + }); + return; + } +} + async function executeStep4(state) { const mail = getMailConfig(state); if (mail.error) throw new Error(mail.error); - await addLog(`Step 4: Opening ${mail.label}...`); + await addLog(`步骤 4:正在打开${mail.label}...`); // For mail tabs, only create if not alive — don't navigate (preserves login session) const alive = await isTabAlive(mail.source); @@ -1089,42 +1351,8 @@ async function executeStep4(state) { }); } - 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.'); - } - } + await resolveVerificationStep(4, state, mail); + return; } // ============================================================ @@ -1135,7 +1363,7 @@ 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 addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`); await sendToContentScript('signup-page', { type: 'EXECUTE_STEP', @@ -1151,13 +1379,13 @@ async function executeStep5(state) { async function executeStep6(state) { if (!state.oauthUrl) { - throw new Error('No OAuth URL. Complete step 1 first.'); + throw new Error('缺少 OAuth 链接,请先完成步骤 1。'); } if (!state.email) { - throw new Error('No email. Complete step 3 first.'); + throw new Error('缺少邮箱地址,请先完成步骤 3。'); } - await addLog(`Step 6: Opening OAuth URL for login...`); + await addLog('步骤 6:正在打开用于登录的 OAuth 链接...'); // Reuse the signup-page tab — navigate it to the OAuth URL await reuseOrCreateTab('signup-page', state.oauthUrl); @@ -1177,7 +1405,30 @@ async function executeStep6(state) { async function executeStep7(state) { const mail = getMailConfig(state); if (mail.error) throw new Error(mail.error); - await addLog(`Step 7: Opening ${mail.label}...`); + const authTabId = await getTabId('signup-page'); + + if (authTabId) { + await chrome.tabs.update(authTabId, { active: true }); + } else { + if (!state.oauthUrl) { + throw new Error('缺少 OAuth 链接,请先完成步骤 1。'); + } + await reuseOrCreateTab('signup-page', state.oauthUrl); + } + + await addLog('步骤 7:正在准备认证页,必要时切换到一次性验证码登录...'); + const prepareResult = await sendToContentScript('signup-page', { + type: 'PREPARE_LOGIN_CODE', + step: 7, + source: 'background', + payload: {}, + }); + + if (prepareResult && prepareResult.error) { + throw new Error(prepareResult.error); + } + + await addLog(`步骤 7:正在打开${mail.label}...`); const alive = await isTabAlive(mail.source); if (alive) { @@ -1197,41 +1448,8 @@ async function executeStep7(state) { }); } - 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.'); - } - } + await resolveVerificationStep(7, state, mail); + return; } // ============================================================ @@ -1242,10 +1460,10 @@ let webNavListener = null; async function executeStep8(state) { if (!state.oauthUrl) { - throw new Error('No OAuth URL. Complete step 1 first.'); + throw new Error('缺少 OAuth 链接,请先完成步骤 1。'); } - await addLog('Step 8: Setting up localhost redirect listener...'); + await addLog('步骤 8:正在监听 localhost 回调地址...'); // Register webNavigation listener (scoped to this step) return new Promise((resolve, reject) => { @@ -1264,7 +1482,7 @@ async function executeStep8(state) { const timeout = setTimeout(() => { cleanupListener(); - reject(new Error('Localhost redirect not captured after 120s. Step 8 click may have been blocked.')); + reject(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 的点击可能被拦截了。')); }, 120000); webNavListener = (details) => { @@ -1276,7 +1494,7 @@ async function executeStep8(state) { if (resolveCaptureWait) resolveCaptureWait(details.url); setState({ localhostUrl: details.url }).then(() => { - addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok'); + addLog(`步骤 8:已捕获 localhost 地址:${details.url}`, 'ok'); setStepStatus(8, 'completed'); notifyStepComplete(8, { localhostUrl: details.url }); broadcastDataUpdate({ localhostUrl: details.url }); @@ -1295,10 +1513,10 @@ async function executeStep8(state) { 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...'); + await addLog('步骤 8:已切回认证页,正在准备调试器点击...'); } else { signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl); - await addLog('Step 8: Auth tab reopened. Preparing debugger click...'); + await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...'); } const clickResult = await sendToContentScript('signup-page', { @@ -1313,7 +1531,7 @@ async function executeStep8(state) { if (!resolved) { await clickWithDebugger(signupTabId, clickResult?.rect); - await addLog('Step 8: Debugger click dispatched, waiting for redirect...'); + await addLog('步骤 8:已发送调试器点击,正在等待跳转...'); } } catch (err) { clearTimeout(timeout); @@ -1330,13 +1548,13 @@ async function executeStep8(state) { async function executeStep9(state) { if (!state.localhostUrl) { - throw new Error('No localhost URL. Complete step 8 first.'); + throw new Error('缺少 localhost 回调地址,请先完成步骤 8。'); } if (!state.vpsUrl) { - throw new Error('VPS URL not set. Please enter VPS URL in the side panel.'); + throw new Error('尚未填写 VPS 地址,请先在侧边栏输入。'); } - await addLog('Step 9: Opening VPS panel...'); + await addLog('步骤 9:正在打开 VPS 面板...'); let tabId = await getTabId('vps-panel'); const alive = tabId && await isTabAlive('vps-panel'); @@ -1366,12 +1584,12 @@ async function executeStep9(state) { await new Promise(r => setTimeout(r, 1000)); // Send command directly — bypass queue/ready mechanism - await addLog(`Step 9: Filling callback URL...`); + await addLog('步骤 9:正在填写回调地址...'); await chrome.tabs.sendMessage(tabId, { type: 'EXECUTE_STEP', step: 9, source: 'background', - payload: { localhostUrl: state.localhostUrl }, + payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword }, }); } diff --git a/content/duck-mail.js b/content/duck-mail.js index 4acc564..35fd7f8 100644 --- a/content/duck-mail.js +++ b/content/duck-mail.js @@ -10,7 +10,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse(result); }).catch(err => { if (isStopError(err)) { - log('Duck Mail: Stopped by user.', 'warn'); + log('Duck 邮箱:已被用户停止。', 'warn'); sendResponse({ stopped: true, error: err.message }); return; } @@ -23,7 +23,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { async function fetchDuckEmail(payload = {}) { const { generateNew = true } = payload; - log(`Duck Mail: ${generateNew ? 'Generating' : 'Reading'} private address...`); + log(`Duck 邮箱:正在${generateNew ? '生成' : '读取'}私有地址...`); await waitForElement( 'input.AutofillSettingsPanel__PrivateDuckAddressValue, button.AutofillSettingsPanel__GeneratorButton', @@ -46,12 +46,12 @@ async function fetchDuckEmail(payload = {}) { } await sleep(150); } - throw new Error('Timed out waiting for Duck address to appear.'); + throw new Error('等待 Duck 地址出现超时。'); }; const currentEmail = readEmail(); if (currentEmail && !generateNew) { - log(`Duck Mail: Found existing address ${currentEmail}`); + log(`Duck 邮箱:已发现现有地址 ${currentEmail}`); return { email: currentEmail, generated: false }; } @@ -59,16 +59,16 @@ async function fetchDuckEmail(payload = {}) { const generatorButton = getGeneratorButton(); if (!generatorButton) { if (currentEmail) { - log(`Duck Mail: Reusing existing address ${currentEmail}`, 'warn'); + log(`Duck 邮箱:正在复用现有地址 ${currentEmail}`, 'warn'); return { email: currentEmail, generated: false }; } - throw new Error('Could not find "Generate Private Duck Address" button.'); + throw new Error('未找到“生成 Duck 私有地址”按钮。'); } generatorButton.click(); - log('Duck Mail: Clicked "Generate Private Duck Address"'); + log('Duck 邮箱:已点击“生成 Duck 私有地址”按钮'); const nextEmail = await waitForEmailValue(currentEmail); - log(`Duck Mail: Ready address ${nextEmail}`, 'ok'); + log(`Duck 邮箱:地址已就绪 ${nextEmail}`, 'ok'); return { email: nextEmail, generated: true }; } diff --git a/content/inbucket-mail.js b/content/inbucket-mail.js index 02fe7b7..1de5949 100644 --- a/content/inbucket-mail.js +++ b/content/inbucket-mail.js @@ -45,11 +45,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse(result); }).catch(err => { if (isStopError(err)) { - log(`Step ${message.step}: Stopped by user.`, 'warn'); + log(`步骤 ${message.step}:已被用户停止。`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } - reportError(message.step, err.message); + log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn'); sendResponse({ error: err.message }); }); return true; @@ -158,10 +158,10 @@ async function deleteCurrentMailboxMessage(step) { try { const deleteButton = await waitForElement('.button-bar button.danger', 5000); simulateClick(deleteButton); - log(`Step ${step}: Deleted mailbox message`, 'ok'); + log(`步骤 ${step}:已删除邮箱消息`, 'ok'); await sleep(1200); } catch (err) { - log(`Step ${step}: Failed to delete mailbox message: ${err.message}`, 'warn'); + log(`步骤 ${step}:删除邮箱消息失败:${err.message}`, 'warn'); } } @@ -171,24 +171,26 @@ async function handleMailboxPollEmail(step, payload) { subjectFilters = [], maxAttempts = 20, intervalMs = 3000, + excludeCodes = [], } = payload || {}; + const excludedCodeSet = new Set(excludeCodes.filter(Boolean)); - log(`Step ${step}: Starting email poll on Inbucket mailbox page (max ${maxAttempts} attempts)`); + log(`步骤 ${step}:开始轮询 Inbucket 邮箱页面(最多 ${maxAttempts} 次)`); try { await waitForElement('.message-list, .message-list-entry', 15000); - log(`Step ${step}: Mailbox page loaded`); + log(`步骤 ${step}:邮箱页面已加载`); } catch { - throw new Error('Inbucket mailbox page did not load. Make sure /m// is open.'); + throw new Error('Inbucket 邮箱页面未加载完成,请确认已打开 /m// 页面。'); } const existingMailIds = getCurrentMailboxIds(); - log(`Step ${step}: Snapshotted ${existingMailIds.size} existing mailbox messages`); + log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧消息快照`); const FALLBACK_AFTER = 3; for (let attempt = 1; attempt <= maxAttempts; attempt++) { - log(`Polling Inbucket mailbox... attempt ${attempt}/${maxAttempts}`); + log(`步骤 ${step}:正在轮询 Inbucket 邮箱,第 ${attempt}/${maxAttempts} 次`); if (attempt > 1) { await refreshMailbox(); @@ -212,6 +214,10 @@ async function handleMailboxPollEmail(step, payload) { for (const mail of candidates) { const code = mail.code || extractVerificationCode(mail.combinedText); if (!code) continue; + if (excludedCodeSet.has(code)) { + log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); + continue; + } await openMailboxEntry(mail.entry); await deleteCurrentMailboxMessage(step); @@ -219,9 +225,9 @@ async function handleMailboxPollEmail(step, payload) { seenMailIds.add(mail.mailId); await persistSeenMailIds(); - const source = existingMailIds.has(mail.mailId) ? 'fallback' : 'new'; + const source = existingMailIds.has(mail.mailId) ? '回退匹配邮件' : '新邮件'; log( - `Step ${step}: Code found: ${code} (${source}, sender: ${mail.sender || 'unknown'}, subject: ${(mail.subject || '').slice(0, 60)})`, + `步骤 ${step}:已找到验证码:${code}(来源:${source},发件人:${mail.sender || '未知'},主题:${(mail.subject || '').slice(0, 60)})`, 'ok' ); @@ -234,7 +240,7 @@ async function handleMailboxPollEmail(step, payload) { } if (attempt === FALLBACK_AFTER + 1) { - log(`Step ${step}: No new mailbox messages yet, falling back to older matching messages`, 'warn'); + log(`步骤 ${step}:暂未发现新消息,开始回退到较早的匹配邮件`, 'warn'); } if (attempt < maxAttempts) { @@ -243,14 +249,14 @@ async function handleMailboxPollEmail(step, payload) { } throw new Error( - `No matching verification email found in Inbucket mailbox after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` + - 'Check the mailbox page manually.' + `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Inbucket 邮箱中找到匹配的验证码邮件。` + + '请手动检查邮箱页面。' ); } async function handlePollEmail(step, payload) { if (!location.pathname.startsWith('/m/')) { - throw new Error('Inbucket now only supports mailbox pages like /m//.'); + throw new Error('当前 Inbucket 仅支持 /m// 这种邮箱页面。'); } return handleMailboxPollEmail(step, payload); } diff --git a/content/mail-163.js b/content/mail-163.js index 63762b9..2d1f65e 100644 --- a/content/mail-163.js +++ b/content/mail-163.js @@ -54,11 +54,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse(result); }).catch(err => { if (isStopError(err)) { - log(`Step ${message.step}: Stopped by user.`, 'warn'); + log(`步骤 ${message.step}:已被用户停止。`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } - reportError(message.step, err.message); + log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn'); sendResponse({ error: err.message }); }); return true; @@ -87,22 +87,23 @@ function getCurrentMailIds() { // ============================================================ async function handlePollEmail(step, payload) { - const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload; + const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload; + const excludedCodeSet = new Set(excludeCodes.filter(Boolean)); - log(`Step ${step}: Starting email poll on 163 Mail (max ${maxAttempts} attempts)`); + log(`步骤 ${step}:开始轮询 163 邮箱(最多 ${maxAttempts} 次)`); // Click inbox in sidebar to ensure we're in inbox view - log(`Step ${step}: Waiting for sidebar...`); + log(`步骤 ${step}:正在等待侧边栏加载...`); try { const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000); inboxLink.click(); - log(`Step ${step}: Clicked inbox`); + log(`步骤 ${step}:已点击收件箱`); } catch { - log(`Step ${step}: Inbox link not found, proceeding...`, 'warn'); + log(`步骤 ${step}:未找到收件箱入口,继续尝试后续流程...`, 'warn'); } // Wait for mail list to appear - log(`Step ${step}: Waiting for mail list...`); + log(`步骤 ${step}:正在等待邮件列表加载...`); let items = []; for (let i = 0; i < 20; i++) { items = findMailItems(); @@ -117,19 +118,19 @@ async function handlePollEmail(step, payload) { } if (items.length === 0) { - throw new Error('163 Mail list did not load. Make sure inbox is open.'); + throw new Error('163 邮箱列表未加载完成,请确认当前已打开收件箱。'); } - log(`Step ${step}: Mail list loaded, ${items.length} items`); + log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`); // Snapshot existing mail IDs const existingMailIds = getCurrentMailIds(); - log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails`); + log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`); const FALLBACK_AFTER = 3; for (let attempt = 1; attempt <= maxAttempts; attempt++) { - log(`Polling 163 Mail... attempt ${attempt}/${maxAttempts}`); + log(`步骤 ${step}:正在轮询 163 邮箱,第 ${attempt}/${maxAttempts} 次`); if (attempt > 1) { await refreshInbox(); @@ -157,11 +158,13 @@ async function handlePollEmail(step, payload) { if (senderMatch || subjectMatch) { const code = extractVerificationCode(subject + ' ' + ariaLabel); - if (code && !seenCodes.has(code)) { + if (code && excludedCodeSet.has(code)) { + log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); + } else if (code && !seenCodes.has(code)) { seenCodes.add(code); persistSeenCodes(); - const source = useFallback && existingMailIds.has(id) ? 'fallback' : 'new'; - log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok'); + const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件' : '新邮件'; + log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)})`, 'ok'); // Delete this email via right-click menu, WAIT for it to finish before returning await deleteEmail(item, step); @@ -170,13 +173,13 @@ async function handlePollEmail(step, payload) { return { ok: true, code, emailTimestamp: Date.now(), mailId: id }; } else if (code && seenCodes.has(code)) { - log(`Step ${step}: Skipping already-seen code: ${code}`, 'info'); + log(`步骤 ${step}:跳过已处理过的验证码:${code}`, 'info'); } } } if (attempt === FALLBACK_AFTER + 1) { - log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first match`, 'warn'); + log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn'); } if (attempt < maxAttempts) { @@ -185,8 +188,8 @@ async function handlePollEmail(step, payload) { } throw new Error( - `No new matching email found on 163 Mail after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` + - 'Check inbox manually.' + `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 163 邮箱中找到新的匹配邮件。` + + '请手动检查收件箱。' ); } @@ -196,7 +199,7 @@ async function handlePollEmail(step, payload) { async function deleteEmail(item, step) { try { - log(`Step ${step}: Deleting email...`); + log(`步骤 ${step}:正在删除邮件...`); // Strategy 1: Click the trash icon inside the mail item // Each mail item has: @@ -208,21 +211,21 @@ async function deleteEmail(item, step) { const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]'); if (trashIcon) { trashIcon.click(); - log(`Step ${step}: Clicked trash icon`, 'ok'); + log(`步骤 ${step}:已点击删除图标`, '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`); + log(`步骤 ${step}:邮件已成功删除`); } else { - log(`Step ${step}: Email may not have been deleted, item still visible`, 'warn'); + log(`步骤 ${step}:邮件可能尚未删除,列表中仍可见`, 'warn'); } return; } // Strategy 2: Select checkbox then click toolbar delete button - log(`Step ${step}: Trash icon not found, trying checkbox + toolbar delete...`); + log(`步骤 ${step}:未找到删除图标,尝试使用复选框加工具栏删除...`); const checkbox = item.querySelector('[sign="checkbox"], .nui-chk'); if (checkbox) { checkbox.click(); @@ -233,16 +236,16 @@ async function deleteEmail(item, step) { for (const btn of toolbarBtns) { if (btn.textContent.replace(/\s/g, '').includes('删除')) { btn.closest('.nui-btn').click(); - log(`Step ${step}: Clicked toolbar delete`, 'ok'); + log(`步骤 ${step}:已点击工具栏删除`, 'ok'); await sleep(1500); return; } } } - log(`Step ${step}: Could not delete email (no delete button found)`, 'warn'); + log(`步骤 ${step}:无法删除邮件(未找到删除按钮)`, 'warn'); } catch (err) { - log(`Step ${step}: Failed to delete email: ${err.message}`, 'warn'); + log(`步骤 ${step}:删除邮件失败:${err.message}`, 'warn'); } } diff --git a/content/qq-mail.js b/content/qq-mail.js index a8ccfe2..fe21bb5 100644 --- a/content/qq-mail.js +++ b/content/qq-mail.js @@ -27,11 +27,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse(result); }).catch(err => { if (isStopError(err)) { - log(`Step ${message.step}: Stopped by user.`, 'warn'); + log(`步骤 ${message.step}:已被用户停止。`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } - reportError(message.step, err.message); + log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn'); sendResponse({ error: err.message }); }); return true; // async response @@ -55,28 +55,29 @@ function getCurrentMailIds() { // ============================================================ async function handlePollEmail(step, payload) { - const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload; + const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload; + const excludedCodeSet = new Set(excludeCodes.filter(Boolean)); - log(`Step ${step}: Starting email poll (max ${maxAttempts} attempts, every ${intervalMs / 1000}s)`); + log(`步骤 ${step}:开始轮询邮箱(最多 ${maxAttempts} 次,每 ${intervalMs / 1000} 秒一次)`); // Wait for mail list to load try { await waitForElement('.mail-list-page-item', 10000); - log(`Step ${step}: Mail list loaded`); + log(`步骤 ${step}:邮件列表已加载`); } catch { - throw new Error('Mail list did not load. Make sure QQ Mail inbox is open.'); + throw new Error('邮件列表未加载完成,请确认 QQ 邮箱已打开收件箱。'); } // Step 1: Snapshot existing mail IDs BEFORE we start waiting for new email const existingMailIds = getCurrentMailIds(); - log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails as "old"`); + log(`步骤 ${step}:已将当前 ${existingMailIds.size} 封邮件标记为旧邮件快照`); // Fallback after just 3 attempts (~10s). In practice, the email is usually // already in the list but has the same mailid (page was already open). const FALLBACK_AFTER = 3; for (let attempt = 1; attempt <= maxAttempts; attempt++) { - log(`Polling QQ Mail... attempt ${attempt}/${maxAttempts}`); + log(`步骤 ${step}:正在轮询 QQ 邮箱,第 ${attempt}/${maxAttempts} 次`); // Refresh inbox (skip on first attempt, list is fresh) if (attempt > 1) { @@ -104,15 +105,19 @@ async function handlePollEmail(step, payload) { if (senderMatch || subjectMatch) { const code = extractVerificationCode(subject + ' ' + digest); if (code) { - const source = useFallback && existingMailIds.has(mailId) ? 'fallback-first-match' : 'new'; - log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok'); + if (excludedCodeSet.has(code)) { + log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); + continue; + } + const source = useFallback && existingMailIds.has(mailId) ? '回退首封匹配邮件' : '新邮件'; + log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)})`, 'ok'); return { ok: true, code, emailTimestamp: Date.now(), mailId }; } } } if (attempt === FALLBACK_AFTER + 1) { - log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first matching email`, 'warn'); + log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn'); } if (attempt < maxAttempts) { @@ -121,8 +126,8 @@ async function handlePollEmail(step, payload) { } throw new Error( - `No new matching email found after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` + - 'Check QQ Mail manually. Email may be delayed or in spam folder.' + `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未找到新的匹配邮件。` + + '请手动检查 QQ 邮箱,邮件可能延迟到达或进入垃圾箱。' ); } diff --git a/content/signup-page.js b/content/signup-page.js index 12a75aa..dfd9764 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -5,19 +5,25 @@ console.log('[MultiPage:signup-page] Content script loaded on', location.href); // Listen for commands from Background chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE' || message.type === 'STEP8_FIND_AND_CLICK') { + if ( + message.type === 'EXECUTE_STEP' + || message.type === 'FILL_CODE' + || message.type === 'STEP8_FIND_AND_CLICK' + || message.type === 'PREPARE_LOGIN_CODE' + || message.type === 'RESEND_VERIFICATION_CODE' + ) { resetStopState(); handleCommand(message).then((result) => { sendResponse({ ok: true, ...(result || {}) }); }).catch(err => { if (isStopError(err)) { - log(`Step ${message.step || 8}: Stopped by user.`, 'warn'); + log(`步骤 ${message.step || 8}:已被用户停止。`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } if (message.type === 'STEP8_FIND_AND_CLICK') { - log(`Step 8: ${err.message}`, 'error'); + log(`步骤 8:${err.message}`, 'error'); sendResponse({ error: err.message }); return; } @@ -38,22 +44,212 @@ async function handleCommand(message) { case 5: return await step5_fillNameBirthday(message.payload); case 6: return await step6_login(message.payload); case 8: return await step8_findAndClick(); - default: throw new Error(`signup-page.js does not handle step ${message.step}`); + default: throw new Error(`signup-page.js 不处理步骤 ${message.step}`); } case 'FILL_CODE': // Step 4 = signup code, Step 7 = login code (same handler) return await fillVerificationCode(message.step, message.payload); + case 'PREPARE_LOGIN_CODE': + return await prepareLoginCodeFlow(); + case 'RESEND_VERIFICATION_CODE': + return await resendVerificationCode(message.step); case 'STEP8_FIND_AND_CLICK': return await step8_findAndClick(); } } +const VERIFICATION_CODE_INPUT_SELECTOR = [ + 'input[name="code"]', + 'input[name="otp"]', + 'input[autocomplete="one-time-code"]', + 'input[type="text"][maxlength="6"]', + 'input[type="tel"][maxlength="6"]', + 'input[aria-label*="code" i]', + 'input[placeholder*="code" i]', + 'input[inputmode="numeric"]', +].join(', '); + +const ONE_TIME_CODE_LOGIN_PATTERN = /使用一次性验证码登录|改用(?:一次性)?验证码(?:登录)?|使用验证码登录|一次性验证码|验证码登录|one[-\s]*time\s*(?:passcode|password|code)|use\s+(?:a\s+)?one[-\s]*time\s*(?:passcode|password|code)(?:\s+instead)?|use\s+(?:a\s+)?code(?:\s+instead)?|sign\s+in\s+with\s+(?:email|code)|email\s+(?:me\s+)?(?:a\s+)?code/i; + +const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i; + +function isVisibleElement(el) { + if (!el) return false; + const style = window.getComputedStyle(el); + const rect = el.getBoundingClientRect(); + return style.display !== 'none' + && style.visibility !== 'hidden' + && rect.width > 0 + && rect.height > 0; +} + +function getVerificationCodeTarget() { + const codeInput = document.querySelector(VERIFICATION_CODE_INPUT_SELECTOR); + if (codeInput && isVisibleElement(codeInput)) { + return { type: 'single', element: codeInput }; + } + + const singleInputs = Array.from(document.querySelectorAll('input[maxlength="1"]')) + .filter(isVisibleElement); + if (singleInputs.length >= 6) { + return { type: 'split', elements: singleInputs }; + } + + return null; +} + +function getActionText(el) { + return [ + el?.textContent, + el?.value, + el?.getAttribute?.('aria-label'), + el?.getAttribute?.('title'), + ] + .filter(Boolean) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); +} + +function isActionEnabled(el) { + return Boolean(el) + && !el.disabled + && el.getAttribute('aria-disabled') !== 'true'; +} + +function findOneTimeCodeLoginTrigger() { + const candidates = document.querySelectorAll( + 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]' + ); + + for (const el of candidates) { + if (!isVisibleElement(el)) continue; + if (el.disabled || el.getAttribute('aria-disabled') === 'true') continue; + + const text = [ + el.textContent, + el.value, + el.getAttribute('aria-label'), + el.getAttribute('title'), + ] + .filter(Boolean) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); + + if (text && ONE_TIME_CODE_LOGIN_PATTERN.test(text)) { + return el; + } + } + + return null; +} + +function findResendVerificationCodeTrigger({ allowDisabled = false } = {}) { + const candidates = document.querySelectorAll( + 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]' + ); + + for (const el of candidates) { + if (!isVisibleElement(el)) continue; + if (!allowDisabled && !isActionEnabled(el)) continue; + + const text = getActionText(el); + if (text && RESEND_VERIFICATION_CODE_PATTERN.test(text)) { + return el; + } + } + + return null; +} + +async function prepareLoginCodeFlow(timeout = 15000) { + const readyTarget = getVerificationCodeTarget(); + if (readyTarget) { + log('步骤 7:验证码输入框已就绪。'); + return { ready: true, mode: readyTarget.type }; + } + + const start = Date.now(); + let switchClickCount = 0; + let lastSwitchAttemptAt = 0; + let loggedPasswordPage = false; + + while (Date.now() - start < timeout) { + throwIfStopped(); + + const target = getVerificationCodeTarget(); + if (target) { + log('步骤 7:验证码页面已就绪。'); + return { ready: true, mode: target.type }; + } + + const passwordInput = document.querySelector('input[type="password"]'); + const switchTrigger = findOneTimeCodeLoginTrigger(); + + if (switchTrigger && (switchClickCount === 0 || Date.now() - lastSwitchAttemptAt > 1500)) { + switchClickCount += 1; + lastSwitchAttemptAt = Date.now(); + loggedPasswordPage = false; + log('步骤 7:检测到密码页,正在切换到一次性验证码登录...'); + await humanPause(350, 900); + simulateClick(switchTrigger); + await sleep(1200); + continue; + } + + if (passwordInput && !loggedPasswordPage) { + loggedPasswordPage = true; + log('步骤 7:正在等待密码页上的一次性验证码登录入口...'); + } + + await sleep(200); + } + + throw new Error('无法切换到一次性验证码验证页面。URL: ' + location.href); +} + +async function resendVerificationCode(step, timeout = 45000) { + if (step === 7) { + await prepareLoginCodeFlow(); + } + + const start = Date.now(); + let action = null; + let loggedWaiting = false; + + while (Date.now() - start < timeout) { + throwIfStopped(); + action = findResendVerificationCodeTrigger({ allowDisabled: true }); + + if (action && isActionEnabled(action)) { + log(`步骤 ${step}:重新发送验证码按钮已可用。`); + await humanPause(350, 900); + simulateClick(action); + await sleep(1200); + return { + resent: true, + buttonText: getActionText(action), + }; + } + + if (action && !loggedWaiting) { + loggedWaiting = true; + log(`步骤 ${step}:正在等待重新发送验证码按钮变为可点击...`); + } + + await sleep(250); + } + + throw new Error('无法点击重新发送验证码按钮。URL: ' + location.href); +} + // ============================================================ // Step 2: Click Register // ============================================================ async function step2_clickRegister() { - log('Step 2: Looking for Register/Sign up button...'); + log('步骤 2:正在查找注册按钮...'); let registerBtn = null; try { @@ -68,8 +264,8 @@ async function step2_clickRegister() { registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000); } catch { throw new Error( - 'Could not find Register/Sign up button. ' + - 'Check auth page DOM in DevTools. URL: ' + location.href + '未找到注册按钮。' + + '请在 DevTools 中检查认证页面 DOM。URL: ' + location.href ); } } @@ -77,7 +273,7 @@ async function step2_clickRegister() { await humanPause(450, 1200); reportComplete(2); simulateClick(registerBtn); - log('Step 2: Clicked Register button'); + log('步骤 2:已点击注册按钮'); } // ============================================================ @@ -86,9 +282,9 @@ async function step2_clickRegister() { async function step3_fillEmailPassword(payload) { const { email } = payload; - if (!email) throw new Error('No email provided. Paste email in Side Panel first.'); + if (!email) throw new Error('未提供邮箱地址,请先在侧边栏粘贴邮箱。'); - log(`Step 3: Filling email: ${email}`); + log(`步骤 3:正在填写邮箱:${email}`); // Find email input let emailInput = null; @@ -98,40 +294,40 @@ async function step3_fillEmailPassword(payload) { 10000 ); } catch { - throw new Error('Could not find email input field on signup page. URL: ' + location.href); + throw new Error('在注册页未找到邮箱输入框。URL: ' + location.href); } await humanPause(500, 1400); fillInput(emailInput, email); - log('Step 3: Email filled'); + log('步骤 3:邮箱已填写'); // Check if password field is on the same page let passwordInput = document.querySelector('input[type="password"]'); if (!passwordInput) { // Need to submit email first to get to password page - log('Step 3: No password field yet, submitting email first...'); + log('步骤 3:暂未发现密码输入框,先提交邮箱...'); const submitBtn = document.querySelector('button[type="submit"]') || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null); if (submitBtn) { await humanPause(400, 1100); simulateClick(submitBtn); - log('Step 3: Submitted email, waiting for password field...'); + log('步骤 3:邮箱已提交,正在等待密码输入框...'); await sleep(2000); } try { passwordInput = await waitForElement('input[type="password"]', 10000); } catch { - throw new Error('Could not find password input after submitting email. URL: ' + location.href); + throw new Error('提交邮箱后仍未找到密码输入框。URL: ' + location.href); } } - if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.'); + if (!payload.password) throw new Error('未提供密码,步骤 3 需要可用密码。'); await humanPause(600, 1500); fillInput(passwordInput, payload.password); - log('Step 3: Password filled'); + log('步骤 3:密码已填写'); // Report complete BEFORE submit, because submit causes page navigation // which kills the content script connection @@ -145,7 +341,7 @@ async function step3_fillEmailPassword(payload) { if (submitBtn) { await humanPause(500, 1300); simulateClick(submitBtn); - log('Step 3: Form submitted'); + log('步骤 3:表单已提交'); } } @@ -153,40 +349,177 @@ async function step3_fillEmailPassword(payload) { // Fill Verification Code (used by step 4 and step 7) // ============================================================ +const INVALID_VERIFICATION_CODE_PATTERN = /代码不正确|验证码不正确|验证码错误|code\s+(?:is\s+)?incorrect|invalid\s+code|incorrect\s+code|try\s+again/i; +const VERIFICATION_PAGE_PATTERN = /检查您的收件箱|输入我们刚刚向|重新发送电子邮件|重新发送验证码|验证码|代码不正确|email\s+verification/i; +const OAUTH_CONSENT_PAGE_PATTERN = /使用\s*ChatGPT\s*登录到\s*Codex|login\s+to\s+codex|log\s+in\s+to\s+codex|authorize|授权/i; +const ADD_PHONE_PAGE_PATTERN = /add[\s-]*phone|添加手机号|手机号码|手机号|phone\s+number|telephone/i; + +function getVerificationErrorText() { + const messages = []; + const selectors = [ + '.react-aria-FieldError', + '[slot="errorMessage"]', + '[id$="-error"]', + '[data-invalid="true"] + *', + '[aria-invalid="true"] + *', + '[class*="error"]', + ]; + + for (const selector of selectors) { + document.querySelectorAll(selector).forEach((el) => { + const text = (el.textContent || '').replace(/\s+/g, ' ').trim(); + if (text) { + messages.push(text); + } + }); + } + + const invalidInput = document.querySelector(`${VERIFICATION_CODE_INPUT_SELECTOR}[aria-invalid="true"], ${VERIFICATION_CODE_INPUT_SELECTOR}[data-invalid="true"]`); + if (invalidInput) { + const wrapper = invalidInput.closest('form, [data-rac], ._root_18qcl_51, div'); + if (wrapper) { + const text = (wrapper.textContent || '').replace(/\s+/g, ' ').trim(); + if (text) { + messages.push(text); + } + } + } + + return messages.find((text) => INVALID_VERIFICATION_CODE_PATTERN.test(text)) || ''; +} + +function isStep5Ready() { + return Boolean( + document.querySelector('input[name="name"], input[autocomplete="name"], input[name="birthday"], input[name="age"], [role="spinbutton"][data-type="year"]') + ); +} + +function getPageTextSnapshot() { + return (document.body?.innerText || document.body?.textContent || '') + .replace(/\s+/g, ' ') + .trim(); +} + +function getPrimaryContinueButton() { + const continueBtn = document.querySelector( + 'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107' + ); + if (continueBtn && isVisibleElement(continueBtn)) { + return continueBtn; + } + + const buttons = document.querySelectorAll('button, [role="button"]'); + return Array.from(buttons).find((el) => isVisibleElement(el) && /继续|Continue/i.test(el.textContent || '')) || null; +} + +function isVerificationPageStillVisible() { + if (getVerificationCodeTarget()) return true; + if (findResendVerificationCodeTrigger({ allowDisabled: true })) return true; + if (document.querySelector('form[action*="email-verification" i]')) return true; + + return VERIFICATION_PAGE_PATTERN.test(getPageTextSnapshot()); +} + +function isAddPhonePageReady() { + const path = `${location.pathname || ''} ${location.href || ''}`; + if (/\/add-phone(?:[/?#]|$)/i.test(path)) return true; + + const phoneInput = document.querySelector( + 'input[type="tel"]:not([maxlength="6"]), input[name*="phone" i], input[id*="phone" i], input[autocomplete="tel"]' + ); + if (phoneInput && isVisibleElement(phoneInput)) { + return true; + } + + return ADD_PHONE_PAGE_PATTERN.test(getPageTextSnapshot()); +} + +function isStep8Ready() { + const continueBtn = getPrimaryContinueButton(); + if (!continueBtn) return false; + if (isVerificationPageStillVisible()) return false; + if (isAddPhonePageReady()) return false; + + return OAUTH_CONSENT_PAGE_PATTERN.test(getPageTextSnapshot()); +} + +async function waitForVerificationSubmitOutcome(step, timeout) { + const resolvedTimeout = timeout ?? (step === 7 ? 30000 : 12000); + const start = Date.now(); + + while (Date.now() - start < resolvedTimeout) { + throwIfStopped(); + + const errorText = getVerificationErrorText(); + if (errorText) { + return { invalidCode: true, errorText }; + } + + if (step === 4 && isStep5Ready()) { + return { success: true }; + } + + if (step === 7 && isStep8Ready()) { + return { success: true }; + } + + if (step === 7 && isAddPhonePageReady()) { + return { success: true, addPhonePage: true }; + } + + await sleep(150); + } + + if (isVerificationPageStillVisible()) { + return { + invalidCode: true, + errorText: getVerificationErrorText() || '提交后仍停留在验证码页面,准备重新发送验证码。', + }; + } + + return { success: true, assumed: true }; +} + async function fillVerificationCode(step, payload) { const { code } = payload; - if (!code) throw new Error('No verification code provided.'); + if (!code) throw new Error('未提供验证码。'); - log(`Step ${step}: Filling verification code: ${code}`); + log(`步骤 ${step}:正在填写验证码:${code}`); + + if (step === 7) { + await prepareLoginCodeFlow(); + } // Find code input — could be a single input or multiple separate inputs let codeInput = null; try { - codeInput = await waitForElement( - 'input[name="code"], input[name="otp"], input[type="text"][maxlength="6"], input[aria-label*="code"], input[placeholder*="code"], input[placeholder*="Code"], input[inputmode="numeric"]', - 10000 - ); + codeInput = await waitForElement(VERIFICATION_CODE_INPUT_SELECTOR, 10000); } catch { // Check for multiple single-digit inputs (common pattern) const singleInputs = document.querySelectorAll('input[maxlength="1"]'); if (singleInputs.length >= 6) { - log(`Step ${step}: Found single-digit code inputs, filling individually...`); + log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`); for (let i = 0; i < 6 && i < singleInputs.length; i++) { fillInput(singleInputs[i], code[i]); await sleep(100); } - await sleep(1000); - reportComplete(step); - return; + const outcome = await waitForVerificationSubmitOutcome(step); + if (outcome.invalidCode) { + log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn'); + } else if (outcome.addPhonePage) { + log(`步骤 ${step}:验证码已通过,并已跳转到手机号页面。`, 'ok'); + } else { + log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok'); + } + return outcome; } - throw new Error('Could not find verification code input. URL: ' + location.href); + throw new Error('未找到验证码输入框。URL: ' + location.href); } fillInput(codeInput, code); - log(`Step ${step}: Code filled`); + log(`步骤 ${step}:验证码已填写`); // Report complete BEFORE submit (page may navigate away) - reportComplete(step); // Submit await sleep(500); @@ -196,8 +529,19 @@ async function fillVerificationCode(step, payload) { if (submitBtn) { await humanPause(450, 1200); simulateClick(submitBtn); - log(`Step ${step}: Verification submitted`); + log(`步骤 ${step}:验证码已提交`); } + + const outcome = await waitForVerificationSubmitOutcome(step); + if (outcome.invalidCode) { + log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn'); + } else if (outcome.addPhonePage) { + log(`步骤 ${step}:验证码已通过,并已跳转到手机号页面。`, 'ok'); + } else { + log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok'); + } + + return outcome; } // ============================================================ @@ -206,9 +550,9 @@ async function fillVerificationCode(step, payload) { async function step6_login(payload) { const { email, password } = payload; - if (!email) throw new Error('No email provided for login.'); + if (!email) throw new Error('登录时缺少邮箱地址。'); - log(`Step 6: Logging in with ${email}...`); + log(`步骤 6:正在使用 ${email} 登录...`); // Wait for email input on the auth page let emailInput = null; @@ -218,12 +562,12 @@ async function step6_login(payload) { 15000 ); } catch { - throw new Error('Could not find email input on login page. URL: ' + location.href); + throw new Error('在登录页未找到邮箱输入框。URL: ' + location.href); } await humanPause(500, 1400); fillInput(emailInput, email); - log('Step 6: Email filled'); + log('步骤 6:邮箱已填写'); // Submit email await sleep(500); @@ -232,7 +576,7 @@ async function step6_login(payload) { if (submitBtn1) { await humanPause(400, 1100); simulateClick(submitBtn1); - log('Step 6: Submitted email'); + log('步骤 6:邮箱已提交'); } await sleep(2000); @@ -240,7 +584,7 @@ async function step6_login(payload) { // Check for password field const passwordInput = document.querySelector('input[type="password"]'); if (passwordInput) { - log('Step 6: Password field found, filling password...'); + log('步骤 6:已找到密码输入框,正在填写密码...'); await humanPause(550, 1450); fillInput(passwordInput, password); @@ -253,13 +597,13 @@ async function step6_login(payload) { if (submitBtn2) { await humanPause(450, 1200); simulateClick(submitBtn2); - log('Step 6: Submitted password, may need verification code (step 7)'); + log('步骤 6:密码已提交,可能还需要验证码(步骤 7)'); } return; } // No password field — OTP flow - log('Step 6: No password field. OTP flow or auto-redirect.'); + log('步骤 6:未发现密码输入框,可能进入验证码流程或自动跳转。'); reportComplete(6, { needsOTP: true }); } @@ -271,7 +615,7 @@ async function step6_login(payload) { // Background performs the actual click through the debugger Input API. async function step8_findAndClick() { - log('Step 8: Looking for OAuth consent "继续" button...'); + log('步骤 8:正在查找 OAuth 同意页的“继续”按钮...'); const continueBtn = await findContinueButton(); await waitForButtonEnabled(continueBtn); @@ -282,7 +626,7 @@ async function step8_findAndClick() { await sleep(250); const rect = getSerializableRect(continueBtn); - log('Step 8: Found "继续" button and prepared debugger click coordinates.'); + log('步骤 8:已找到“继续”按钮并准备好调试器点击坐标。'); return { rect, buttonText: (continueBtn.textContent || '').trim(), @@ -291,18 +635,20 @@ async function step8_findAndClick() { } async function findContinueButton() { - try { - return await waitForElement( - 'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107', - 10000 - ); - } catch { - try { - return await waitForElementByText('button', /继续|Continue/, 5000); - } catch { - throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href); + const start = Date.now(); + while (Date.now() - start < 10000) { + throwIfStopped(); + if (isAddPhonePageReady()) { + throw new Error('当前页面已进入手机号页面,不是 OAuth 授权同意页。URL: ' + location.href); } + const button = getPrimaryContinueButton(); + if (button && isStep8Ready()) { + return button; + } + await sleep(150); } + + throw new Error('在 OAuth 同意页未找到“继续”按钮,或页面尚未进入授权同意状态。URL: ' + location.href); } async function waitForButtonEnabled(button, timeout = 8000) { @@ -312,7 +658,7 @@ async function waitForButtonEnabled(button, timeout = 8000) { if (isButtonEnabled(button)) return; await sleep(150); } - throw new Error('"继续" button stayed disabled for too long. URL: ' + location.href); + throw new Error('“继续”按钮长时间不可点击。URL: ' + location.href); } function isButtonEnabled(button) { @@ -324,7 +670,7 @@ function isButtonEnabled(button) { function getSerializableRect(el) { const rect = el.getBoundingClientRect(); if (!rect.width || !rect.height) { - throw new Error('"继续" button has no clickable size after scrolling. URL: ' + location.href); + throw new Error('滚动后“继续”按钮没有可点击尺寸。URL: ' + location.href); } return { @@ -343,16 +689,16 @@ function getSerializableRect(el) { async function step5_fillNameBirthday(payload) { const { firstName, lastName, age, year, month, day } = payload; - if (!firstName || !lastName) throw new Error('No name data provided.'); + if (!firstName || !lastName) throw new Error('未提供姓名数据。'); const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null); const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value))); if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) { - throw new Error('No birthday or age data provided.'); + throw new Error('未提供生日或年龄数据。'); } const fullName = `${firstName} ${lastName}`; - log(`Step 5: Filling name: ${fullName}`); + log(`步骤 5:正在填写姓名:${fullName}`); // Actual DOM structure: // - Full name: @@ -367,11 +713,11 @@ async function step5_fillNameBirthday(payload) { 10000 ); } catch { - throw new Error('Could not find name input. URL: ' + location.href); + throw new Error('未找到姓名输入框。URL: ' + location.href); } await humanPause(500, 1300); fillInput(nameInput, fullName); - log(`Step 5: Name filled: ${fullName}`); + log(`步骤 5:姓名已填写:${fullName}`); let birthdayMode = false; let ageInput = null; @@ -393,7 +739,7 @@ async function step5_fillNameBirthday(payload) { if (birthdayMode) { if (!hasBirthdayData) { - throw new Error('Birthday field detected, but no birthday data provided.'); + throw new Error('检测到生日字段,但未提供生日数据。'); } const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]'); @@ -401,7 +747,7 @@ async function step5_fillNameBirthday(payload) { const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]'); if (yearSpinner && monthSpinner && daySpinner) { - log('Step 5: Birthday fields detected, filling birthday...'); + log('步骤 5:检测到生日字段,正在填写生日...'); async function setSpinButton(el, value) { el.focus(); @@ -429,7 +775,7 @@ async function step5_fillNameBirthday(payload) { await setSpinButton(monthSpinner, String(month).padStart(2, '0')); await humanPause(250, 650); await setSpinButton(daySpinner, String(day).padStart(2, '0')); - log(`Step 5: Birthday filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`); + log(`步骤 5:生日已填写:${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`); } const hiddenBirthday = document.querySelector('input[name="birthday"]'); @@ -437,17 +783,17 @@ async function step5_fillNameBirthday(payload) { const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; hiddenBirthday.value = dateStr; hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true })); - log(`Step 5: Hidden birthday input set: ${dateStr}`); + log(`步骤 5:已设置隐藏生日输入框:${dateStr}`); } } else if (ageInput) { if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) { - throw new Error('Age field detected, but no age data provided.'); + throw new Error('检测到年龄字段,但未提供年龄数据。'); } await humanPause(500, 1300); fillInput(ageInput, String(resolvedAge)); - log(`Step 5: Age filled: ${resolvedAge}`); + log(`步骤 5:年龄已填写:${resolvedAge}`); } else { - throw new Error('Could not find birthday or age input. URL: ' + location.href); + throw new Error('未找到生日或年龄输入项。URL: ' + location.href); } // Click "完成帐户创建" button @@ -461,6 +807,6 @@ async function step5_fillNameBirthday(payload) { if (completeBtn) { await humanPause(500, 1300); simulateClick(completeBtn); - log('Step 5: Clicked "完成帐户创建"'); + log('步骤 5:已点击“完成帐户创建”'); } } diff --git a/content/utils.js b/content/utils.js index ff17740..a80ffca 100644 --- a/content/utils.js +++ b/content/utils.js @@ -13,7 +13,7 @@ const SCRIPT_SOURCE = (() => { })(); const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`; -const STOP_ERROR_MESSAGE = 'Flow stopped by user.'; +const STOP_ERROR_MESSAGE = '流程已被用户停止。'; let flowStopped = false; chrome.runtime.onMessage.addListener((message) => { @@ -50,14 +50,14 @@ function waitForElement(selector, timeout = 10000) { const existing = document.querySelector(selector); if (existing) { - console.log(LOG_PREFIX, `Found immediately: ${selector}`); - log(`Found element: ${selector}`); + console.log(LOG_PREFIX, `立即找到元素: ${selector}`); + log(`已找到元素:${selector}`); resolve(existing); return; } - console.log(LOG_PREFIX, `Waiting for: ${selector} (timeout: ${timeout}ms)`); - log(`Waiting for selector: ${selector}...`); + console.log(LOG_PREFIX, `等待元素: ${selector}(超时 ${timeout}ms)`); + log(`正在等待选择器:${selector}...`); let settled = false; let stopTimer = null; @@ -78,8 +78,8 @@ function waitForElement(selector, timeout = 10000) { const el = document.querySelector(selector); if (el) { cleanup(); - console.log(LOG_PREFIX, `Found after wait: ${selector}`); - log(`Found element: ${selector}`); + console.log(LOG_PREFIX, `等待后找到元素: ${selector}`); + log(`已找到元素:${selector}`); resolve(el); } }); @@ -91,7 +91,7 @@ function waitForElement(selector, timeout = 10000) { const timer = setTimeout(() => { cleanup(); - const msg = `Timeout waiting for ${selector} after ${timeout}ms on ${location.href}`; + const msg = `在 ${location.href} 等待 ${selector} 超时,已超过 ${timeout}ms`; console.error(LOG_PREFIX, msg); reject(new Error(msg)); }, timeout); @@ -132,14 +132,14 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) { const existing = search(); if (existing) { - console.log(LOG_PREFIX, `Found by text immediately: ${containerSelector} matching ${textPattern}`); - log(`Found element by text: ${textPattern}`); + console.log(LOG_PREFIX, `立即按文本找到元素: ${containerSelector} 匹配 ${textPattern}`); + log(`已按文本找到元素:${textPattern}`); resolve(existing); return; } - console.log(LOG_PREFIX, `Waiting for text match: ${containerSelector} / ${textPattern}`); - log(`Waiting for element with text: ${textPattern}...`); + console.log(LOG_PREFIX, `等待文本匹配: ${containerSelector} / ${textPattern}`); + log(`正在等待包含文本的元素:${textPattern}...`); let settled = false; let stopTimer = null; @@ -160,8 +160,8 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) { const el = search(); if (el) { cleanup(); - console.log(LOG_PREFIX, `Found by text after wait: ${textPattern}`); - log(`Found element by text: ${textPattern}`); + console.log(LOG_PREFIX, `等待后按文本找到元素: ${textPattern}`); + log(`已按文本找到元素:${textPattern}`); resolve(el); } }); @@ -173,7 +173,7 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) { const timer = setTimeout(() => { cleanup(); - const msg = `Timeout waiting for text "${textPattern}" in "${containerSelector}" after ${timeout}ms on ${location.href}`; + const msg = `在 ${location.href} 的 ${containerSelector} 中等待文本 "${textPattern}" 超时,已超过 ${timeout}ms`; console.error(LOG_PREFIX, msg); reject(new Error(msg)); }, timeout); @@ -206,8 +206,8 @@ function fillInput(el, value) { nativeInputValueSetter.call(el, value); el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); - console.log(LOG_PREFIX, `Filled input ${el.name || el.id || el.type} with: ${value}`); - log(`Filled input [${el.name || el.id || el.type || 'unknown'}]`); + console.log(LOG_PREFIX, `已填写输入框 ${el.name || el.id || el.type}: ${value}`); + log(`已填写输入框 [${el.name || el.id || el.type || '未知'}]`); } /** @@ -219,8 +219,8 @@ function fillSelect(el, value) { throwIfStopped(); el.value = value; el.dispatchEvent(new Event('change', { bubbles: true })); - console.log(LOG_PREFIX, `Selected value ${value} in ${el.name || el.id}`); - log(`Selected [${el.name || el.id || 'unknown'}] = ${value}`); + console.log(LOG_PREFIX, `已在 ${el.name || el.id} 中选择值: ${value}`); + log(`已选择 [${el.name || el.id || '未知'}] = ${value}`); } /** @@ -242,7 +242,7 @@ function log(message, level = 'info') { * Report that this content script is loaded and ready. */ function reportReady() { - console.log(LOG_PREFIX, 'Content script ready'); + console.log(LOG_PREFIX, '内容脚本已就绪'); chrome.runtime.sendMessage({ type: 'CONTENT_SCRIPT_READY', source: SCRIPT_SOURCE, @@ -258,8 +258,8 @@ function reportReady() { * @param {Object} data - Step output data */ function reportComplete(step, data = {}) { - console.log(LOG_PREFIX, `Step ${step} completed`, data); - log(`Step ${step} completed successfully`, 'ok'); + console.log(LOG_PREFIX, `步骤 ${step} 已完成`, data); + log(`步骤 ${step} 已成功完成`, 'ok'); chrome.runtime.sendMessage({ type: 'STEP_COMPLETE', source: SCRIPT_SOURCE, @@ -275,8 +275,8 @@ function reportComplete(step, data = {}) { * @param {string} errorMessage */ function reportError(step, errorMessage) { - console.error(LOG_PREFIX, `Step ${step} failed: ${errorMessage}`); - log(`Step ${step} failed: ${errorMessage}`, 'error'); + console.error(LOG_PREFIX, `步骤 ${step} 失败: ${errorMessage}`); + log(`步骤 ${step} 失败:${errorMessage}`, 'error'); chrome.runtime.sendMessage({ type: 'STEP_ERROR', source: SCRIPT_SOURCE, @@ -293,8 +293,8 @@ function reportError(step, errorMessage) { function simulateClick(el) { throwIfStopped(); el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); - console.log(LOG_PREFIX, `Clicked: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`); - log(`Clicked [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`); + console.log(LOG_PREFIX, `已点击: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`); + log(`已点击 [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`); } /** diff --git a/content/vps-panel.js b/content/vps-panel.js index a5f34a9..ac51839 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -33,7 +33,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse({ ok: true }); }).catch(err => { if (isStopError(err)) { - log(`Step ${message.step}: Stopped by user.`, 'warn'); + log(`步骤 ${message.step}:已被用户停止。`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } @@ -46,65 +46,199 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { async function handleStep(step, payload) { switch (step) { - case 1: return await step1_getOAuthLink(); + case 1: return await step1_getOAuthLink(payload); case 9: return await step9_vpsVerify(payload); default: - throw new Error(`vps-panel.js does not handle step ${step}`); + throw new Error(`vps-panel.js 不处理步骤 ${step}`); } } +function isVisibleElement(el) { + if (!el) return false; + const style = window.getComputedStyle(el); + const rect = el.getBoundingClientRect(); + return style.display !== 'none' + && style.visibility !== 'hidden' + && rect.width > 0 + && rect.height > 0; +} + +function getActionText(el) { + return [ + el?.textContent, + el?.value, + el?.getAttribute?.('aria-label'), + el?.getAttribute?.('title'), + ] + .filter(Boolean) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); +} + +function findManagementKeyInput() { + const candidates = document.querySelectorAll( + '.LoginPage-module__loginCard___OgP-R input[type="password"], input[placeholder*="管理密钥"], input[aria-label*="管理密钥"]' + ); + return Array.from(candidates).find(isVisibleElement) || null; +} + +function findManagementLoginButton() { + const candidates = document.querySelectorAll('.LoginPage-module__loginCard___OgP-R button, .LoginPage-module__loginCard___OgP-R .btn'); + return Array.from(candidates).find((el) => { + if (!isVisibleElement(el)) return false; + return /登录|login/i.test(getActionText(el)); + }) || null; +} + +function findRememberPasswordCheckbox() { + const candidates = document.querySelectorAll('.LoginPage-module__loginCard___OgP-R input[type="checkbox"]'); + return Array.from(candidates).find((el) => { + const label = el.closest('label'); + const text = getActionText(label || el); + return /记住密码|remember/i.test(text); + }) || null; +} + +function findOAuthNavLink() { + const candidates = document.querySelectorAll('a[href*="#/oauth"], a.nav-item, button, [role="link"], [role="button"]'); + return Array.from(candidates).find((el) => { + if (!isVisibleElement(el)) return false; + const text = getActionText(el); + const href = el.getAttribute('href') || ''; + return href.includes('#/oauth') || /oauth/i.test(text); + }) || null; +} + +function findCodexOAuthHeader() { + const candidates = document.querySelectorAll('.card-header, [class*="cardHeader"], .card, [class*="card"]'); + return Array.from(candidates).find((el) => { + if (!isVisibleElement(el)) return false; + const text = (el.textContent || '').toLowerCase(); + return text.includes('codex') && text.includes('oauth'); + }) || null; +} + +function findOAuthCardLoginButton(header) { + const card = header?.closest('.card, [class*="card"]') || header?.parentElement || document; + const candidates = card.querySelectorAll('button.btn.btn-primary, button.btn-primary, button.btn'); + return Array.from(candidates).find((el) => isVisibleElement(el) && /登录|login/i.test(getActionText(el))) || null; +} + +function findAuthUrlElement() { + const candidates = document.querySelectorAll('[class*="authUrlValue"], .OAuthPage-module__authUrlValue___axvUJ'); + return Array.from(candidates).find((el) => isVisibleElement(el) && /^https?:\/\//i.test((el.textContent || '').trim())) || null; +} + +async function ensureOAuthManagementPage(vpsPassword, step = 1, timeout = 45000) { + const start = Date.now(); + let lastLoginAttemptAt = 0; + let lastOauthNavAttemptAt = 0; + + while (Date.now() - start < timeout) { + throwIfStopped(); + + const authUrlEl = findAuthUrlElement(); + if (authUrlEl) { + return { header: findCodexOAuthHeader(), authUrlEl }; + } + + const oauthHeader = findCodexOAuthHeader(); + if (oauthHeader) { + return { header: oauthHeader, authUrlEl: null }; + } + + const managementKeyInput = findManagementKeyInput(); + const managementLoginButton = findManagementLoginButton(); + if (managementKeyInput && managementLoginButton) { + if (!vpsPassword) { + throw new Error('VPS 面板需要管理密钥,请先在侧边栏填写 VPS Key(管理密钥)。'); + } + + if ((managementKeyInput.value || '') !== vpsPassword) { + await humanPause(350, 900); + fillInput(managementKeyInput, vpsPassword); + log(`步骤 ${step}:已填写 VPS 管理密钥。`); + } + + const rememberCheckbox = findRememberPasswordCheckbox(); + if (rememberCheckbox && !rememberCheckbox.checked) { + simulateClick(rememberCheckbox); + log(`步骤 ${step}:已勾选 VPS 面板“记住密码”。`); + await sleep(300); + } + + if (Date.now() - lastLoginAttemptAt > 3000) { + lastLoginAttemptAt = Date.now(); + await humanPause(350, 900); + simulateClick(managementLoginButton); + log(`步骤 ${step}:已提交 VPS 管理登录。`); + } + + await sleep(1500); + continue; + } + + const oauthNavLink = findOAuthNavLink(); + if (oauthNavLink && Date.now() - lastOauthNavAttemptAt > 2000) { + lastOauthNavAttemptAt = Date.now(); + await humanPause(300, 800); + simulateClick(oauthNavLink); + log(`步骤 ${step}:已打开“OAuth 登录”导航。`); + await sleep(1200); + continue; + } + + await sleep(250); + } + + throw new Error('无法进入 VPS 的 OAuth 管理页面,请检查面板是否正常加载。URL: ' + location.href); +} + // ============================================================ // Step 1: Get OAuth Link // ============================================================ -async function step1_getOAuthLink() { - log('Step 1: Waiting for VPS panel to load (auto-login may take a moment)...'); +async function step1_getOAuthLink(payload) { + const { vpsPassword } = payload || {}; - // The page may start at #/login and auto-redirect to #/oauth. - // Wait for the Codex OAuth card to appear (up to 30s for auto-login + redirect). - let loginBtn = null; - try { - // Wait for any card-header containing "Codex" to appear - const header = await waitForElementByText('.card-header', /codex/i, 30000); - loginBtn = header.querySelector('button.btn.btn-primary, button.btn'); - log('Step 1: Found Codex OAuth card'); - } catch { - throw new Error( - 'Codex OAuth card did not appear after 30s. Page may still be loading or not logged in. ' + - 'Current URL: ' + location.href - ); - } + log('步骤 1:正在等待 VPS 面板加载并进入 OAuth 页面...'); - if (!loginBtn) { - throw new Error('Found Codex OAuth card but no login button inside it. URL: ' + location.href); - } + const { header, authUrlEl: existingAuthUrlEl } = await ensureOAuthManagementPage(vpsPassword, 1); + let authUrlEl = existingAuthUrlEl; - // Check if button is disabled (already clicked / loading) - if (loginBtn.disabled) { - log('Step 1: Login button is disabled (already loading), waiting for auth URL...'); + if (!authUrlEl) { + const loginBtn = findOAuthCardLoginButton(header); + if (!loginBtn) { + throw new Error('已找到 Codex OAuth 卡片,但卡片内没有登录按钮。URL: ' + location.href); + } + + if (loginBtn.disabled) { + log('步骤 1:OAuth 登录按钮当前不可用,正在等待授权链接出现...'); + } else { + await humanPause(500, 1400); + simulateClick(loginBtn); + log('步骤 1:已点击 OAuth 登录按钮,正在等待授权链接...'); + } + + try { + authUrlEl = await waitForElement('[class*="authUrlValue"]', 15000); + } catch { + throw new Error( + '点击 OAuth 登录按钮后未出现授权链接。' + + '请检查 VPS 面板服务是否正在运行。URL: ' + location.href + ); + } } else { - await humanPause(500, 1400); - simulateClick(loginBtn); - log('Step 1: Clicked login button, waiting for auth URL...'); - } - - // Wait for the auth URL to appear in the specific div - let authUrlEl = null; - try { - authUrlEl = await waitForElement('[class*="authUrlValue"]', 15000); - } catch { - throw new Error( - 'Auth URL did not appear after clicking login. ' + - 'Check if VPS panel is logged in and Codex service is running. URL: ' + location.href - ); + log('步骤 1:VPS 面板上已显示授权链接。'); } const oauthUrl = (authUrlEl.textContent || '').trim(); if (!oauthUrl || !oauthUrl.startsWith('http')) { - throw new Error(`Invalid OAuth URL found: "${oauthUrl.slice(0, 50)}". Expected URL starting with http.`); + throw new Error(`拿到的 OAuth 链接无效:\"${oauthUrl.slice(0, 50)}\"。应为 http 开头的 URL。`); } - log(`Step 1: OAuth URL obtained: ${oauthUrl.slice(0, 80)}...`, 'ok'); + log(`步骤 1:已获取 OAuth 链接:${oauthUrl.slice(0, 80)}...`, 'ok'); reportComplete(1, { oauthUrl }); } @@ -113,19 +247,21 @@ async function step1_getOAuthLink() { // ============================================================ async function step9_vpsVerify(payload) { + await ensureOAuthManagementPage(payload?.vpsPassword, 9); + // Get localhostUrl from payload (passed directly by background) or fallback to state let localhostUrl = payload?.localhostUrl; if (!localhostUrl) { - log('Step 9: localhostUrl not in payload, fetching from state...'); + log('步骤 9:payload 中没有 localhostUrl,正在从状态中读取...'); const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); localhostUrl = state.localhostUrl; } if (!localhostUrl) { - throw new Error('No localhost URL found. Complete step 8 first.'); + throw new Error('未找到 localhost 回调地址,请先完成步骤 8。'); } - log(`Step 9: Got localhostUrl: ${localhostUrl.slice(0, 60)}...`); + log(`步骤 9:已获取 localhostUrl:${localhostUrl.slice(0, 60)}...`); - log('Step 9: Looking for callback URL input...'); + log('步骤 9:正在查找回调地址输入框...'); // Find the callback URL input // Actual DOM: @@ -136,13 +272,13 @@ async function step9_vpsVerify(payload) { try { urlInput = await waitForElement('input[placeholder*="localhost"]', 5000); } catch { - throw new Error('Could not find callback URL input on VPS panel. URL: ' + location.href); + throw new Error('在 VPS 面板中未找到回调地址输入框。URL: ' + location.href); } } await humanPause(600, 1500); fillInput(urlInput, localhostUrl); - log(`Step 9: Filled callback URL: ${localhostUrl.slice(0, 80)}...`); + log(`步骤 9:已填写回调地址:${localhostUrl.slice(0, 80)}...`); // Find and click "提交回调 URL" button let submitBtn = null; @@ -156,26 +292,26 @@ async function step9_vpsVerify(payload) { try { submitBtn = await waitForElementByText('button.btn', /提交回调/, 5000); } catch { - throw new Error('Could not find "提交回调 URL" button. URL: ' + location.href); + throw new Error('未找到“提交回调 URL”按钮。URL: ' + location.href); } } await humanPause(450, 1200); simulateClick(submitBtn); - log('Step 9: Clicked "提交回调 URL", waiting for authentication result...'); + log('步骤 9:已点击“提交回调 URL”,正在等待认证结果...'); // Wait for "认证成功!" status badge to appear try { await waitForElementByText('.status-badge, [class*="status"]', /认证成功/, 30000); - log('Step 9: Authentication successful!', 'ok'); + log('步骤 9:认证成功!', 'ok'); } catch { // Check if there's an error message instead const statusEl = document.querySelector('.status-badge, [class*="status"]'); const statusText = statusEl ? statusEl.textContent : 'unknown'; if (/成功|success/i.test(statusText)) { - log('Step 9: Authentication successful!', 'ok'); + log('步骤 9:认证成功!', 'ok'); } else { - log(`Step 9: Status after submit: "${statusText}". May still be processing.`, 'warn'); + log(`步骤 9:提交后的状态为“${statusText}”,可能仍在处理中。`, 'warn'); } } diff --git a/manifest.json b/manifest.json index 0a18df6..7d486ff 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, - "name": "Multi-Page Automation", + "name": "多页面自动化", "version": "1.1.0", - "description": "Automates multi-step OAuth registration workflow", + "description": "用于自动执行多步骤 OAuth 注册流程", "permissions": [ "sidePanel", "tabs", diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 5c16ca1..da6895a 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -244,6 +244,10 @@ header { gap: 8px; } +.data-check-row { + align-items: flex-start; +} + .data-inline { display: flex; align-items: center; @@ -323,6 +327,25 @@ header { .data-select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); } [data-theme="dark"] .data-select { color-scheme: dark; } +.data-check { + display: flex; + align-items: flex-start; + gap: 8px; + flex: 1; + min-width: 0; + font-size: 12px; + line-height: 1.5; + color: var(--text-secondary); + cursor: pointer; +} + +.data-check input[type="checkbox"] { + margin-top: 2px; + accent-color: var(--blue); + cursor: pointer; + flex-shrink: 0; +} + /* Status Bar */ .status-bar { display: flex; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index b2167b0..d0c19db 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -3,7 +3,7 @@ - Multi-Page Automation + 多页面自动化面板 @@ -15,23 +15,23 @@ -

MultiPage

+

多页面

- - - +
- -
- Mail + 管理密钥 + +
+
+ 邮箱服务
- Email + 邮箱
- - + +
- Password + 密码
- - + +
+
+ 兜底 + +
OAuth - Waiting... + 等待中...
- Callback - Waiting... + 回调 + 等待中...
- Ready + 就绪
- + 0 / 9
1
- +
2
- +
3
- +
4
- +
5
- +
6
- +
7
- +
8
- +
9
- +
@@ -154,8 +165,8 @@
- - + +
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index f859e54..5915d0e 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -25,12 +25,14 @@ const btnAutoContinue = document.getElementById('btn-auto-continue'); const autoContinueBar = document.getElementById('auto-continue-bar'); const btnClearLog = document.getElementById('btn-clear-log'); const inputVpsUrl = document.getElementById('input-vps-url'); +const inputVpsPassword = document.getElementById('input-vps-password'); const selectMailProvider = document.getElementById('select-mail-provider'); const rowInbucketHost = document.getElementById('row-inbucket-host'); const inputInbucketHost = document.getElementById('input-inbucket-host'); const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox'); const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox'); const inputRunCount = document.getElementById('input-run-count'); +const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures'); // ============================================================ // Toast Notifications @@ -45,6 +47,13 @@ const TOAST_ICONS = { info: '', }; +const LOG_LEVEL_LABELS = { + info: '信息', + ok: '成功', + warn: '警告', + error: '错误', +}; + function showToast(message, type = 'error', duration = 4000) { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; @@ -87,6 +96,9 @@ async function restoreState() { if (state.vpsUrl) { inputVpsUrl.value = state.vpsUrl; } + if (state.vpsPassword) { + inputVpsPassword.value = state.vpsPassword; + } if (state.mailProvider) { selectMailProvider.value = state.mailProvider; } @@ -96,6 +108,7 @@ async function restoreState() { if (state.inbucketMailbox) { inputInbucketMailbox.value = state.inbucketMailbox; } + inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures); if (state.stepStatuses) { for (const [step, status] of Object.entries(state.stepStatuses)) { @@ -194,21 +207,21 @@ function updateStatusDisplay(state) { const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running'); if (running) { - displayStatus.textContent = `Step ${running[0]} running...`; + displayStatus.textContent = `步骤 ${running[0]} 运行中...`; statusBar.classList.add('running'); return; } const failed = Object.entries(state.stepStatuses).find(([, s]) => s === 'failed'); if (failed) { - displayStatus.textContent = `Step ${failed[0]} failed`; + displayStatus.textContent = `步骤 ${failed[0]} 失败`; statusBar.classList.add('failed'); return; } const stopped = Object.entries(state.stepStatuses).find(([, s]) => s === 'stopped'); if (stopped) { - displayStatus.textContent = `Step ${stopped[0]} stopped`; + displayStatus.textContent = `步骤 ${stopped[0]} 已停止`; statusBar.classList.add('stopped'); return; } @@ -219,28 +232,28 @@ function updateStatusDisplay(state) { .sort((a, b) => b - a)[0]; if (lastCompleted === 9) { - displayStatus.textContent = 'All steps completed!'; + displayStatus.textContent = '全部步骤已完成'; statusBar.classList.add('completed'); } else if (lastCompleted) { - displayStatus.textContent = `Step ${lastCompleted} done`; + displayStatus.textContent = `步骤 ${lastCompleted} 已完成`; } else { - displayStatus.textContent = 'Ready'; + displayStatus.textContent = '就绪'; } } function appendLog(entry) { - const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false }); - const levelLabel = entry.level.toUpperCase(); + const time = new Date(entry.timestamp).toLocaleTimeString('zh-CN', { hour12: false }); + const levelLabel = LOG_LEVEL_LABELS[entry.level] || entry.level; const line = document.createElement('div'); line.className = `log-line log-${entry.level}`; - const stepMatch = entry.message.match(/Step (\d)/); - const stepNum = stepMatch ? stepMatch[1] : null; + const stepMatch = entry.message.match(/(?:Step\s+(\d+)|步骤\s*(\d+))/); + const stepNum = stepMatch ? (stepMatch[1] || stepMatch[2]) : null; let html = `${time} `; html += `${levelLabel} `; if (stepNum) { - html += `S${stepNum}`; + html += `步${stepNum}`; } html += `${escapeHtml(entry.message)}`; @@ -256,7 +269,7 @@ function escapeHtml(text) { } async function fetchDuckEmail() { - const defaultLabel = 'Auto'; + const defaultLabel = '获取'; btnFetchEmail.disabled = true; btnFetchEmail.textContent = '...'; @@ -271,14 +284,14 @@ async function fetchDuckEmail() { throw new Error(response.error); } if (!response?.email) { - throw new Error('Duck email was not returned.'); + throw new Error('未返回 Duck 邮箱。'); } inputEmail.value = response.email; - showToast(`Fetched ${response.email}`, 'success', 2500); + showToast(`已获取 ${response.email}`, 'success', 2500); return response.email; } catch (err) { - showToast(`Auto fetch failed: ${err.message}`, 'error'); + showToast(`自动获取失败:${err.message}`, 'error'); throw err; } finally { btnFetchEmail.disabled = false; @@ -287,7 +300,7 @@ async function fetchDuckEmail() { } function syncPasswordToggleLabel() { - btnTogglePassword.textContent = inputPassword.type === 'password' ? 'Show' : 'Hide'; + btnTogglePassword.textContent = inputPassword.type === 'password' ? '显示' : '隐藏'; } // ============================================================ @@ -300,7 +313,7 @@ document.querySelectorAll('.step-btn').forEach(btn => { if (step === 3) { const email = inputEmail.value.trim(); if (!email) { - showToast('Please paste email address or use Auto first', 'warn'); + showToast('请先粘贴邮箱,或先点击获取。', 'warn'); return; } await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } }); @@ -322,7 +335,7 @@ btnTogglePassword.addEventListener('click', () => { btnStop.addEventListener('click', async () => { btnStop.disabled = true; await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} }); - showToast('Stopping current flow...', 'warn', 2000); + showToast('正在停止当前流程...', 'warn', 2000); }); // Auto Run @@ -330,14 +343,21 @@ btnAutoRun.addEventListener('click', async () => { const totalRuns = parseInt(inputRunCount.value) || 1; btnAutoRun.disabled = true; inputRunCount.disabled = true; - btnAutoRun.innerHTML = ' Running...'; - await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel', payload: { totalRuns } }); + btnAutoRun.innerHTML = ' 运行中...'; + await chrome.runtime.sendMessage({ + type: 'AUTO_RUN', + source: 'sidepanel', + payload: { + totalRuns, + autoRunSkipFailures: inputAutoSkipFailures.checked, + }, + }); }); btnAutoContinue.addEventListener('click', async () => { const email = inputEmail.value.trim(); if (!email) { - showToast('Please fetch or paste DuckDuckGo email first!', 'warn'); + showToast('请先获取或粘贴 DuckDuckGo 邮箱。', 'warn'); return; } autoContinueBar.style.display = 'none'; @@ -346,21 +366,21 @@ btnAutoContinue.addEventListener('click', async () => { // Reset btnReset.addEventListener('click', async () => { - if (confirm('Reset all steps and data?')) { + if (confirm('确认重置全部步骤和数据吗?')) { await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' }); - displayOauthUrl.textContent = 'Waiting...'; + displayOauthUrl.textContent = '等待中...'; displayOauthUrl.classList.remove('has-value'); - displayLocalhostUrl.textContent = 'Waiting...'; + displayLocalhostUrl.textContent = '等待中...'; displayLocalhostUrl.classList.remove('has-value'); inputEmail.value = ''; - displayStatus.textContent = 'Ready'; + displayStatus.textContent = '就绪'; statusBar.className = 'status-bar'; logArea.innerHTML = ''; document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row'); document.querySelectorAll('.step-status').forEach(el => el.textContent = ''); btnAutoRun.disabled = false; inputRunCount.disabled = false; - btnAutoRun.innerHTML = ' Auto'; + btnAutoRun.innerHTML = ' 自动'; autoContinueBar.style.display = 'none'; updateStopButtonState(false); updateButtonStates(); @@ -388,6 +408,14 @@ inputVpsUrl.addEventListener('change', async () => { } }); +inputVpsPassword.addEventListener('change', async () => { + await chrome.runtime.sendMessage({ + type: 'SAVE_SETTING', + source: 'sidepanel', + payload: { vpsPassword: inputVpsPassword.value }, + }); +}); + inputPassword.addEventListener('change', async () => { await chrome.runtime.sendMessage({ type: 'SAVE_SETTING', @@ -420,6 +448,14 @@ inputInbucketHost.addEventListener('change', async () => { }); }); +inputAutoSkipFailures.addEventListener('change', async () => { + await chrome.runtime.sendMessage({ + type: 'SAVE_SETTING', + source: 'sidepanel', + payload: { autoRunSkipFailures: inputAutoSkipFailures.checked }, + }); +}); + // ============================================================ // Listen for Background broadcasts // ============================================================ @@ -455,12 +491,12 @@ chrome.runtime.onMessage.addListener((message) => { case 'AUTO_RUN_RESET': { // Full UI reset for next run - displayOauthUrl.textContent = 'Waiting...'; + displayOauthUrl.textContent = '等待中...'; displayOauthUrl.classList.remove('has-value'); - displayLocalhostUrl.textContent = 'Waiting...'; + displayLocalhostUrl.textContent = '等待中...'; displayLocalhostUrl.classList.remove('has-value'); inputEmail.value = ''; - displayStatus.textContent = 'Ready'; + displayStatus.textContent = '就绪'; statusBar.className = 'status-bar'; logArea.innerHTML = ''; document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row'); @@ -489,29 +525,34 @@ chrome.runtime.onMessage.addListener((message) => { } case 'AUTO_RUN_STATUS': { - const { phase, currentRun, totalRuns } = message.payload; - const runLabel = totalRuns > 1 ? ` (${currentRun}/${totalRuns})` : ''; + const { phase, currentRun, totalRuns, attemptRun } = message.payload; + const attemptLabel = attemptRun ? ` · 尝试${attemptRun}` : ''; + const runLabel = totalRuns > 1 ? ` (${currentRun}/${totalRuns}${attemptLabel})` : (attemptLabel ? ` (${attemptLabel.slice(3)})` : ''); switch (phase) { case 'waiting_email': autoContinueBar.style.display = 'flex'; - btnAutoRun.innerHTML = `Paused${runLabel}`; + btnAutoRun.innerHTML = `已暂停${runLabel}`; updateStopButtonState(true); break; case 'running': - btnAutoRun.innerHTML = `Running${runLabel}`; + btnAutoRun.innerHTML = `运行中${runLabel}`; + updateStopButtonState(true); + break; + case 'retrying': + btnAutoRun.innerHTML = `重试中${runLabel}`; updateStopButtonState(true); break; case 'complete': btnAutoRun.disabled = false; inputRunCount.disabled = false; - btnAutoRun.innerHTML = ' Auto'; + btnAutoRun.innerHTML = ' 自动'; autoContinueBar.style.display = 'none'; updateStopButtonState(false); break; case 'stopped': btnAutoRun.disabled = false; inputRunCount.disabled = false; - btnAutoRun.innerHTML = ' Auto'; + btnAutoRun.innerHTML = ' 自动'; autoContinueBar.style.display = 'none'; updateStopButtonState(false); break;