From 79a54c78f077975a897b0e275aadae345f5554de Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sat, 18 Apr 2026 01:23:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=AD=A5=E9=AA=A47?= =?UTF-8?q?=E5=92=8C=E6=AD=A5=E9=AA=A48=E7=9A=84=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E5=9C=A8=E7=99=BB=E5=BD=95=E8=B6=85=E6=97=B6=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E6=97=B6=E4=B8=8D=E5=86=8D=E7=82=B9=E5=87=BB=E9=87=8D=E8=AF=95?= =?UTF-8?q?=EF=BC=8C=E7=9B=B4=E6=8E=A5=E5=9B=9E=E5=88=B0=E6=AD=A5=E9=AA=A4?= =?UTF-8?q?7=E9=87=8D=E8=B7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- background.js | 5 + background/steps/fetch-login-code.js | 17 ++- content/signup-page.js | 19 +-- tests/step6-timeout-recovery.test.js | 4 +- tests/step8-restart-step7-error.test.js | 157 ++++++++++++++++++++++++ 项目完整链路说明.md | 3 +- 7 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 tests/step8-restart-step7-error.test.js diff --git a/README.md b/README.md index 808e467..1ea3427 100644 --- a/README.md +++ b/README.md @@ -534,7 +534,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 - 已刷新到最新 OAuth 链接 - 认证页已经真正进入登录验证码页面 -- 如遇登录超时报错,会先尝试自动点击认证页上的 `重试` 恢复当前页面,再由后台按既有逻辑重跑整个 Step 7 +- 如遇登录超时报错,不会在这一步点击认证页上的 `重试`;而是直接按既有逻辑把当前状态视为可恢复失败,交给后续链路回到 Step 7 重跑 - 如遇登录页长时间停滞,会由后台刷新 OAuth 后重跑整个 Step 7 支持: @@ -552,6 +552,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。 - 打开邮箱并轮询登录验证码 - 填写并提交登录验证码 - 验证码链路失败后按有限次数回退到 Step 7 +- 如果进入登录超时报错/重试页,也会直接报错并回到 Step 7,不会在 Step 8 内部点击 `重试` 与 Step 4 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。 diff --git a/background.js b/background.js index f7bd807..8b82fea 100644 --- a/background.js +++ b/background.js @@ -6430,6 +6430,11 @@ async function ensureStep8VerificationPageReady(options = {}) { return pageState; } + if (pageState.state === 'login_timeout_error_page') { + const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; + throw new Error(`STEP8_RESTART_STEP7::步骤 8:当前认证页进入登录超时报错页,请回到步骤 7 重新开始。${urlPart}`.trim()); + } + const stateLabel = getLoginAuthStateLabel(pageState.state); const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:${stateLabel}.${urlPart}`.trim()); diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index e0dd7ec..b25ab62 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -125,6 +125,11 @@ } } + function isStep8RestartStep7Error(error) { + const message = String(error?.message || error || ''); + return /STEP8_RESTART_STEP7::/i.test(message); + } + async function executeStep8(state) { let currentState = state; let mailPollingAttempt = 1; @@ -135,7 +140,7 @@ await runStep8Attempt(currentState); return; } catch (err) { - if (!isVerificationMailPollingError(err)) { + if (!isVerificationMailPollingError(err) && !isStep8RestartStep7Error(err)) { throw err; } @@ -146,10 +151,16 @@ mailPollingAttempt += 1; await addLog( - `步骤 8:检测到邮箱轮询类失败,准备从步骤 7 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`, + isStep8RestartStep7Error(err) + ? `步骤 8:检测到认证页进入重试/超时报错状态,准备从步骤 7 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...` + : `步骤 8:检测到邮箱轮询类失败,准备从步骤 7 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`, 'warn' ); - await rerunStep7ForStep8Recovery(); + await rerunStep7ForStep8Recovery({ + logMessage: isStep8RestartStep7Error(err) + ? '步骤 8:认证页进入重试/超时报错状态,正在回到步骤 7 重新发起登录流程...' + : '步骤 8:正在回到步骤 7,重新发起登录验证码流程...', + }); currentState = await getState(); } } diff --git a/content/signup-page.js b/content/signup-page.js index a9c7146..3f29e75 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -1235,24 +1235,7 @@ function createStep6RecoverableResult(reason, snapshot, options = {}) { } async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message) { - const resolvedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); - if (resolvedSnapshot?.state === 'login_timeout_error_page') { - try { - const recoveryResult = await recoverCurrentAuthRetryPage({ - flow: 'login', - logLabel: '步骤 7:检测到登录超时报错,正在点击“重试”恢复当前页面', - step: 6, - timeoutMs: 12000, - }); - if (recoveryResult?.recovered) { - log('步骤 7:登录超时报错页已点击“重试”,准备重新执行当前步骤。', 'warn'); - } - } catch (error) { - log(`步骤 7:登录超时报错页自动点击“重试”失败:${error.message}`, 'warn'); - } - } - - return createStep6RecoverableResult(reason, resolvedSnapshot, { + return createStep6RecoverableResult(reason, normalizeStep6Snapshot(snapshot || inspectLoginAuthState()), { message, }); } diff --git a/tests/step6-timeout-recovery.test.js b/tests/step6-timeout-recovery.test.js index 92db5d6..94ba643 100644 --- a/tests/step6-timeout-recovery.test.js +++ b/tests/step6-timeout-recovery.test.js @@ -51,7 +51,7 @@ function extractFunction(name) { return source.slice(start, end); } -test('step 6 timeout recoverable result clicks retry before asking background to rerun', async () => { +test('step 7 timeout recoverable result no longer clicks retry before asking background to rerun', async () => { const api = new Function(` const logs = []; let recoverCalls = 0; @@ -97,7 +97,7 @@ return { const result = await api.run(); const snapshot = api.snapshot(); - assert.equal(snapshot.recoverCalls, 1); + assert.equal(snapshot.recoverCalls, 0); assert.equal(result.step6Outcome, 'recoverable'); assert.equal(result.reason, 'login_timeout_error_page'); assert.equal(result.state, 'login_timeout_error_page'); diff --git a/tests/step8-restart-step7-error.test.js b/tests/step8-restart-step7-error.test.js new file mode 100644 index 0000000..f38beff --- /dev/null +++ b/tests/step8-restart-step7-error.test.js @@ -0,0 +1,157 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const backgroundSource = fs.readFileSync('background.js', 'utf8'); +const step8Source = fs.readFileSync('background/steps/fetch-login-code.js', 'utf8'); +const step8GlobalScope = {}; +const step8Api = new Function('self', `${step8Source}; return self.MultiPageBackgroundStep8;`)(step8GlobalScope); + +function extractFunction(source, name) { + const markers = [`async function ${name}(`, `function ${name}(`]; + const start = markers + .map((marker) => source.indexOf(marker)) + .find((index) => index >= 0); + if (start < 0) { + throw new Error(`missing function ${name}`); + } + + let parenDepth = 0; + let signatureEnded = false; + let braceStart = -1; + for (let i = start; i < source.length; i += 1) { + const ch = source[i]; + if (ch === '(') { + parenDepth += 1; + } else if (ch === ')') { + parenDepth -= 1; + if (parenDepth === 0) { + signatureEnded = true; + } + } else if (ch === '{' && signatureEnded) { + braceStart = i; + break; + } + } + + if (braceStart < 0) { + throw new Error(`missing body for function ${name}`); + } + + let depth = 0; + let end = braceStart; + for (; end < source.length; end += 1) { + const ch = source[end]; + if (ch === '{') depth += 1; + if (ch === '}') { + depth -= 1; + if (depth === 0) { + end += 1; + break; + } + } + } + + return source.slice(start, end); +} + +test('ensureStep8VerificationPageReady throws explicit restart-step7 error on login timeout page', async () => { + const api = new Function(` +function getLoginAuthStateLabel(state) { + return state === 'login_timeout_error_page' ? '登录超时报错页' : '未知页面'; +} + +async function getLoginAuthStateFromContent() { + return { + state: 'login_timeout_error_page', + url: 'https://auth.openai.com/log-in', + }; +} + +${extractFunction(backgroundSource, 'ensureStep8VerificationPageReady')} + +return { + run() { + return ensureStep8VerificationPageReady({}); + }, +}; +`)(); + + await assert.rejects( + () => api.run(), + /STEP8_RESTART_STEP7::步骤 8:当前认证页进入登录超时报错页/ + ); +}); + +test('step 8 reruns step 7 when auth page enters login timeout retry state', async () => { + const calls = { + executeStep7: 0, + ensureReady: 0, + logs: [], + sleeps: [], + resolveCalls: 0, + }; + + const executor = step8Api.createStep8Executor({ + addLog: async (message, level) => { + calls.logs.push({ message, level }); + }, + chrome: { + tabs: { + update: async () => {}, + }, + }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + confirmCustomVerificationStepBypass: async () => {}, + ensureStep8VerificationPageReady: async () => { + calls.ensureReady += 1; + if (calls.ensureReady === 1) { + throw new Error('STEP8_RESTART_STEP7::步骤 8:当前认证页进入登录超时报错页,请回到步骤 7 重新开始。 URL: https://auth.openai.com/log-in'); + } + return { state: 'verification_page' }; + }, + executeStep7: async () => { + calls.executeStep7 += 1; + }, + getOAuthFlowRemainingMs: async () => 8000, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000), + getMailConfig: () => ({ + provider: 'qq', + label: 'QQ 邮箱', + source: 'mail-qq', + url: 'https://mail.qq.com', + navigateOnReuse: false, + }), + getState: async () => ({ email: 'user@example.com', password: 'secret', oauthUrl: 'https://oauth.example/latest' }), + getTabId: async () => 1, + HOTMAIL_PROVIDER: 'hotmail-api', + isTabAlive: async () => true, + isVerificationMailPollingError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + resolveVerificationStep: async () => { + calls.resolveCalls += 1; + }, + reuseOrCreateTab: async () => {}, + setState: async () => {}, + setStepStatus: async () => {}, + shouldUseCustomRegistrationEmail: () => false, + sleepWithStop: async (ms) => { + calls.sleeps.push(ms); + }, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, + STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 3, + throwIfStopped: () => {}, + }); + + await executor.executeStep8({ + email: 'user@example.com', + password: 'secret', + oauthUrl: 'https://oauth.example/latest', + }); + + assert.equal(calls.executeStep7, 1); + assert.equal(calls.ensureReady, 2); + assert.equal(calls.resolveCalls, 1); + assert.equal(calls.logs.some(({ message }) => /准备从步骤 7 重新开始/.test(message)), true); + assert.deepStrictEqual(calls.sleeps, [3000]); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 77dfe46..27e83fe 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -510,5 +510,6 @@ - 新增共享恢复层:`content/auth-page-recovery.js`。 - Step 4 在等待注册验证码页时,如果命中认证页 `Try again / 重试` 页,会先自动点击 `重试` 恢复,再继续回到密码页重提和验证码页确认流程。 -- Step 7 在识别到登录超时报错页时,会先尝试自动点击 `重试` 恢复当前页面,再按原有有限重试逻辑重新执行整步。 +- Step 7 在识别到登录超时报错页时,不会在当前步骤内部点击 `重试`,而是直接返回可恢复失败,交给后续链路回到 Step 7 重跑。 +- Step 8 如果发现认证页已经进入登录超时报错/重试页,会直接报错并回到 Step 7 重新开始,而不是在 Step 8 内部点击 `重试`。 - Step 9 在点击 OAuth 同意页 `继续` 后,会额外检查是否进入认证页重试页;若命中则先自动点击 `重试` 恢复,再重新执行当前轮的 `继续` 点击。