From 7cb1bda6729f5b36225a935459085d4ac6a0935d Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 19 Apr 2026 18:59:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20OAuth=20=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=EF=BC=8C=E8=B0=83=E6=95=B4=E6=AD=A5=E9=AA=A4=206=20?= =?UTF-8?q?=E5=92=8C=E6=AD=A5=E9=AA=A4=207=20=E7=9A=84=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E7=9B=B8=E5=85=B3=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 157 ++++++++++++++-- background/panel-bridge.js | 4 +- background/steps/fetch-login-code.js | 27 +-- background/steps/oauth-login.js | 1 + content/signup-page.js | 2 +- content/vps-panel.js | 2 +- tests/background-auth-chain-guard.test.js | 186 +++++++++++++++++++ tests/background-panel-bridge-module.test.js | 6 + tests/background-step6-retry-limit.test.js | 1 + tests/background-step7-recovery.test.js | 28 +-- tests/step8-restart-step7-error.test.js | 20 +- 项目文件结构说明.md | 6 +- 12 files changed, 372 insertions(+), 68 deletions(-) create mode 100644 tests/background-auth-chain-guard.test.js diff --git a/background.js b/background.js index b614309..b5958ac 100644 --- a/background.js +++ b/background.js @@ -321,6 +321,7 @@ const DEFAULT_STATE = { signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, + oauthFlowDeadlineSourceUrl: null, currentHotmailAccountId: null, preferredIcloudHost: '', }; @@ -3975,6 +3976,7 @@ function getDownstreamStateResets(step) { signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, + oauthFlowDeadlineSourceUrl: null, lastSignupCode: null, lastLoginCode: null, localhostUrl: null, @@ -3987,6 +3989,7 @@ function getDownstreamStateResets(step) { signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, + oauthFlowDeadlineSourceUrl: null, lastSignupCode: null, lastLoginCode: null, localhostUrl: null, @@ -3998,6 +4001,7 @@ function getDownstreamStateResets(step) { signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, + oauthFlowDeadlineSourceUrl: null, lastSignupCode: null, lastLoginCode: null, localhostUrl: null, @@ -4008,6 +4012,7 @@ function getDownstreamStateResets(step) { lastLoginCode: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, + oauthFlowDeadlineSourceUrl: null, localhostUrl: null, }; } @@ -4612,7 +4617,11 @@ async function skipStep(step) { return { ok: true, step, status: 'skipped' }; } -function throwIfStopped() { +function throwIfStopped(error = null) { + const errorMessage = typeof error === 'string' ? error : error?.message; + if (errorMessage === STOP_ERROR_MESSAGE) { + throw error instanceof Error ? error : new Error(STOP_ERROR_MESSAGE); + } if (stopRequested) { throw new Error(STOP_ERROR_MESSAGE); } @@ -4796,6 +4805,7 @@ async function handleStepData(step, payload) { await setState({ localhostUrl: payload.localhostUrl, oauthFlowDeadlineAt: null, + oauthFlowDeadlineSourceUrl: null, }); broadcastDataUpdate({ localhostUrl: payload.localhostUrl }); } @@ -4993,6 +5003,59 @@ async function waitForRunningStepsToFinish(payload = {}) { return currentState; } +const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]); +let activeTopLevelAuthChainExecution = null; + +function isAuthChainStep(step) { + return AUTH_CHAIN_STEP_IDS.has(Number(step)); +} + +async function acquireTopLevelAuthChainExecution(step) { + const normalizedStep = Number(step); + if (!isAuthChainStep(normalizedStep)) { + return { + joined: false, + release() {}, + }; + } + + if (activeTopLevelAuthChainExecution) { + const activeExecution = activeTopLevelAuthChainExecution; + await addLog( + `步骤 ${normalizedStep}:检测到步骤 ${activeExecution.step} 正在运行,本次请求将复用当前授权链,不再重复启动。`, + 'warn' + ); + const result = await activeExecution.promise; + if (result?.error) { + throw result.error; + } + return { + joined: true, + release() {}, + }; + } + + let settleExecution = () => {}; + const promise = new Promise((resolve) => { + settleExecution = (error = null) => resolve({ error }); + }); + const execution = { + step: normalizedStep, + promise, + }; + activeTopLevelAuthChainExecution = execution; + + return { + joined: false, + release(error = null) { + if (activeTopLevelAuthChainExecution === execution) { + activeTopLevelAuthChainExecution = null; + } + settleExecution(error); + }, + }; +} + async function markRunningStepsStopped() { const state = await getState(); const runningSteps = getRunningSteps(state.stepStatuses); @@ -5089,21 +5152,29 @@ async function requestStop(options = {}) { async function executeStep(step, options = {}) { const { deferRetryableTransportError = false } = options; console.log(LOG_PREFIX, `Executing step ${step}`); - throwIfStopped(); - await setStepStatus(step, 'running'); - await addLog(`步骤 ${step} 开始执行`); - await humanStepDelay(); - - const state = await getState(); - - // Set flow start time on first step - if (step === 1 && !state.flowStartTime) { - await setState({ flowStartTime: Date.now() }); + const authChainClaim = await acquireTopLevelAuthChainExecution(step); + if (authChainClaim.joined) { + return; } + let executionError = null; + throwIfStopped(); try { + await setStepStatus(step, 'running'); + await addLog(`步骤 ${step} 开始执行`); + await humanStepDelay(); + + const state = await getState(); + + // Set flow start time on first step + if (step === 1 && !state.flowStartTime) { + await setState({ flowStartTime: Date.now() }); + } + await stepRegistry.executeStep(step, state); } catch (err) { + executionError = err; + const state = await getState(); if (isStopError(err)) { await setStepStatus(step, 'stopped'); await addLog(`步骤 ${step} 已被用户停止`, 'warn'); @@ -5125,6 +5196,8 @@ async function executeStep(step, options = {}) { ); } throw err; + } finally { + authChainClaim.release(executionError); } } @@ -5963,7 +6036,6 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ CLOUDFLARE_TEMP_EMAIL_PROVIDER, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, ensureStep8VerificationPageReady, - executeStep7: (...args) => executeStep7(...args), getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, getPanelMode, @@ -5975,9 +6047,9 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ isVerificationMailPollingError, LUCKMAIL_PROVIDER, resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, + rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args), reuseOrCreateTab, setState, - setStepStatus, shouldUseCustomRegistrationEmail, sleepWithStop, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, @@ -6455,10 +6527,18 @@ function normalizeOAuthFlowDeadlineAt(value) { return Math.floor(numeric); } +function normalizeOAuthFlowSourceUrl(value) { + const normalized = String(value || '').trim(); + return normalized || null; +} + async function startOAuthFlowTimeoutWindow(options = {}) { const step = Number(options.step) || 7; const deadlineAt = Date.now() + OAUTH_FLOW_TIMEOUT_MS; - await setState({ oauthFlowDeadlineAt: deadlineAt }); + await setState({ + oauthFlowDeadlineAt: deadlineAt, + oauthFlowDeadlineSourceUrl: normalizeOAuthFlowSourceUrl(options.oauthUrl), + }); await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 6 分钟倒计时。`, 'info'); return deadlineAt; } @@ -6468,10 +6548,22 @@ async function getOAuthFlowRemainingMs(options = {}) { const actionLabel = String(options.actionLabel || '后续授权流程').trim() || '后续授权流程'; const state = options.state || await getState(); const deadlineAt = normalizeOAuthFlowDeadlineAt(state?.oauthFlowDeadlineAt); + const deadlineSourceUrl = normalizeOAuthFlowSourceUrl(state?.oauthFlowDeadlineSourceUrl); + const currentOauthUrl = normalizeOAuthFlowSourceUrl(options.oauthUrl !== undefined ? options.oauthUrl : state?.oauthUrl); if (!deadlineAt) { return null; } + if (deadlineSourceUrl && currentOauthUrl && deadlineSourceUrl !== currentOauthUrl) { + console.warn(LOG_PREFIX, '[oauth-flow] ignoring stale deadline due to oauth url mismatch', { + step, + actionLabel, + deadlineSourceUrl, + currentOauthUrl, + }); + return null; + } + const remainingMs = deadlineAt - Date.now(); if (remainingMs <= 0) { throw buildOAuthFlowTimeoutError(step, actionLabel); @@ -6617,6 +6709,43 @@ async function ensureStep8VerificationPageReady(options = {}) { throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:${stateLabel}.${urlPart}`.trim()); } +async function rerunStep7ForStep8Recovery(options = {}) { + const { + logMessage = '步骤 8:正在回到步骤 7,重新发起登录验证码流程...', + postStepDelayMs = 3000, + } = options; + + throwIfStopped(); + const initialState = await getState(); + await addLog(logMessage, 'warn'); + await setStepStatus(7, 'running'); + await addLog('步骤 7 开始执行'); + + try { + await step7Executor.executeStep7(initialState); + } catch (err) { + const latestState = await getState(); + if (isStopError(err)) { + await setStepStatus(7, 'stopped'); + await addLog('步骤 7 已被用户停止', 'warn'); + await appendManualAccountRunRecordIfNeeded('step7_stopped', latestState, getErrorMessage(err)); + throw err; + } + if (isTerminalSecurityBlockedError(err)) { + await handleCloudflareSecurityBlocked(err); + throw new Error(STOP_ERROR_MESSAGE); + } + await setStepStatus(7, 'failed'); + await addLog(`步骤 7 失败:${getErrorMessage(err)}`, 'error'); + await appendManualAccountRunRecordIfNeeded('step7_failed', latestState, getErrorMessage(err)); + throw err; + } + + if (postStepDelayMs > 0) { + await sleepWithStop(postStepDelayMs); + } +} + async function executeStep6() { return step6Executor.executeStep6(); } diff --git a/background/panel-bridge.js b/background/panel-bridge.js index b25af20..ad97e6c 100644 --- a/background/panel-bridge.js +++ b/background/panel-bridge.js @@ -60,7 +60,7 @@ source: 'background', payload: { vpsPassword: state.vpsPassword, - logStep: 6, + logStep: 7, }, }, { timeoutMs: 30000, @@ -121,7 +121,7 @@ sub2apiPassword: state.sub2apiPassword, sub2apiGroupName: groupName, sub2apiDefaultProxyName: state.sub2apiDefaultProxyName, - logStep: 6, + logStep: 7, }, }, { responseTimeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS, diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 0046dd7..d147bb3 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -8,7 +8,6 @@ CLOUDFLARE_TEMP_EMAIL_PROVIDER, confirmCustomVerificationStepBypass, ensureStep8VerificationPageReady, - executeStep7, getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, getMailConfig, @@ -19,17 +18,16 @@ isVerificationMailPollingError, LUCKMAIL_PROVIDER, resolveVerificationStep, + rerunStep7ForStep8Recovery, reuseOrCreateTab, setState, - setStepStatus, shouldUseCustomRegistrationEmail, - sleepWithStop, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS, throwIfStopped, } = deps; - async function getStep8ReadyTimeoutMs(actionLabel) { + async function getStep8ReadyTimeoutMs(actionLabel, expectedOauthUrl = '') { if (typeof getOAuthFlowStepTimeoutMs !== 'function') { return 15000; } @@ -37,10 +35,11 @@ return getOAuthFlowStepTimeoutMs(15000, { step: 8, actionLabel, + oauthUrl: expectedOauthUrl, }); } - function getStep8RemainingTimeResolver() { + function getStep8RemainingTimeResolver(expectedOauthUrl = '') { if (typeof getOAuthFlowRemainingMs !== 'function') { return undefined; } @@ -48,6 +47,7 @@ return async (details = {}) => getOAuthFlowRemainingMs({ step: 8, actionLabel: details.actionLabel || '登录验证码流程', + oauthUrl: expectedOauthUrl, }); } @@ -73,7 +73,7 @@ throwIfStopped(); const pageState = await ensureStep8VerificationPageReady({ - timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪'), + timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || ''), }); const shouldCompareVerificationEmail = mail.provider !== '2925'; const displayedVerificationEmail = shouldCompareVerificationEmail @@ -131,7 +131,7 @@ step8VerificationTargetEmail: displayedVerificationEmail || '', }, mail, { filterAfterTimestamp: stepStartedAt, - getRemainingTimeMs: getStep8RemainingTimeResolver(), + getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || ''), requestFreshCodeFirst: false, targetEmail: fixedTargetEmail, resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') @@ -140,19 +140,6 @@ }); } - async function rerunStep7ForStep8Recovery(options = {}) { - const { - logMessage = '步骤 8:正在回到步骤 7,重新发起登录验证码流程...', - postStepDelayMs = 3000, - } = options; - const currentState = await getState(); - await addLog(logMessage, 'warn'); - await executeStep7(currentState); - if (postStepDelayMs > 0) { - await sleepWithStop(postStepDelayMs); - } - } - function isStep8RestartStep7Error(error) { const message = String(error?.message || error || ''); return /STEP8_RESTART_STEP7::/i.test(message); diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index facf21e..596ac9f 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -45,6 +45,7 @@ ? await getOAuthFlowStepTimeoutMs(180000, { step: 7, actionLabel: 'OAuth 登录并进入验证码页', + oauthUrl, }) : 180000; diff --git a/content/signup-page.js b/content/signup-page.js index 4ef7f78..de736d4 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -1671,7 +1671,7 @@ async function fillVerificationCode(step, payload) { } // ============================================================ -// Step 6: Login with registered account (on OAuth auth page) +// Step 7: Login with registered account (on OAuth auth page) // ============================================================ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000) { diff --git a/content/vps-panel.js b/content/vps-panel.js index 28b3c4f..4e84d49 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -1,4 +1,4 @@ -// content/vps-panel.js — Content script for CPA panel (steps 1, 9) +// content/vps-panel.js — Content script for CPA panel (steps 7, 10 / OAuth URL request) // Injected on: CPA panel (user-configured URL) // // Actual DOM structure (after login click): diff --git a/tests/background-auth-chain-guard.test.js b/tests/background-auth-chain-guard.test.js new file mode 100644 index 0000000..3fec903 --- /dev/null +++ b/tests/background-auth-chain-guard.test.js @@ -0,0 +1,186 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('background.js', 'utf8'); + +function extractFunction(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; + } + } + + 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('throwIfStopped rethrows an explicit stop error even when stopRequested has been cleared', () => { + const api = new Function(` +const STOP_ERROR_MESSAGE = '流程已被用户停止。'; +let stopRequested = false; +${extractFunction('isStopError')} +${extractFunction('throwIfStopped')} +return { + run(error) { + throwIfStopped(error); + }, +}; +`)(); + + assert.throws( + () => api.run(new Error('流程已被用户停止。')), + /流程已被用户停止。/ + ); +}); + +test('executeStep reuses the active top-level auth chain instead of starting a duplicate step', async () => { + const api = new Function(` +const LOG_PREFIX = '[test]'; +const STOP_ERROR_MESSAGE = '流程已被用户停止。'; +const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]); +let activeTopLevelAuthChainExecution = null; +let stopRequested = false; +let releaseStep8 = null; +const events = { + logs: [], + statusCalls: [], + registryCalls: [], +}; +const state = { + stepStatuses: {}, +}; + +async function addLog(message, level = 'info') { + events.logs.push({ message, level }); +} +async function setStepStatus(step, status) { + state.stepStatuses[step] = status; + events.statusCalls.push({ step, status }); +} +async function humanStepDelay() {} +async function getState() { + return { + flowStartTime: null, + stepStatuses: { ...state.stepStatuses }, + }; +} +function getErrorMessage(error) { + return error?.message || String(error || ''); +} +async function appendManualAccountRunRecordIfNeeded() {} +function isTerminalSecurityBlockedError() { + return false; +} +async function handleCloudflareSecurityBlocked() {} +function doesStepUseCompletionSignal() { + return false; +} +function isRetryableContentScriptTransportError() { + return false; +} +const stepRegistry = { + async executeStep(step) { + events.registryCalls.push(step); + if (step === 8) { + await new Promise((resolve) => { + releaseStep8 = resolve; + }); + } + }, +}; + +${extractFunction('isStopError')} +${extractFunction('throwIfStopped')} +${extractFunction('isAuthChainStep')} +${extractFunction('acquireTopLevelAuthChainExecution')} +${extractFunction('executeStep')} + +return { + executeStep, + releaseStep8() { + if (releaseStep8) { + releaseStep8(); + } + }, + snapshot() { + return events; + }, +}; +`)(); + + const firstRun = api.executeStep(8); + await new Promise((resolve) => setImmediate(resolve)); + const duplicateRun = api.executeStep(7); + await new Promise((resolve) => setImmediate(resolve)); + api.releaseStep8(); + + await firstRun; + await duplicateRun; + + const events = api.snapshot(); + assert.deepStrictEqual(events.registryCalls, [8]); + assert.deepStrictEqual(events.statusCalls, [ + { step: 8, status: 'running' }, + ]); + assert.ok(events.logs.some(({ message }) => /复用当前授权链/.test(message))); +}); + +test('oauth timeout budget ignores stale deadlines from an old oauth url', async () => { + const api = new Function(` +const LOG_PREFIX = '[test]'; +const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000; +${extractFunction('normalizeOAuthFlowDeadlineAt')} +${extractFunction('normalizeOAuthFlowSourceUrl')} +${extractFunction('getOAuthFlowRemainingMs')} +${extractFunction('getOAuthFlowStepTimeoutMs')} +return { + getOAuthFlowStepTimeoutMs, +}; +`)(); + + const timeoutMs = await api.getOAuthFlowStepTimeoutMs(15000, { + step: 8, + actionLabel: '登录验证码流程', + state: { + oauthUrl: 'https://oauth.example/current', + oauthFlowDeadlineAt: Date.now() + 1200, + oauthFlowDeadlineSourceUrl: 'https://oauth.example/old', + }, + }); + + assert.equal(timeoutMs, 15000); +}); diff --git a/tests/background-panel-bridge-module.test.js b/tests/background-panel-bridge-module.test.js index 1faf2d5..8473571 100644 --- a/tests/background-panel-bridge-module.test.js +++ b/tests/background-panel-bridge-module.test.js @@ -15,3 +15,9 @@ test('panel bridge module exposes a factory', () => { assert.equal(typeof api?.createPanelBridge, 'function'); }); + +test('panel bridge requests oauth url with step 7 log label payload', () => { + const source = fs.readFileSync('background/panel-bridge.js', 'utf8'); + assert.match(source, /logStep:\s*7/); + assert.doesNotMatch(source, /logStep:\s*6/); +}); diff --git a/tests/background-step6-retry-limit.test.js b/tests/background-step6-retry-limit.test.js index 865b9c5..af1783c 100644 --- a/tests/background-step6-retry-limit.test.js +++ b/tests/background-step6-retry-limit.test.js @@ -172,6 +172,7 @@ test('step 7 starts a new oauth timeout window for each refreshed oauth url', as options: { step: 7, actionLabel: 'OAuth 登录并进入验证码页', + oauthUrl: 'https://oauth.example/latest', }, }, ]); diff --git a/tests/background-step7-recovery.test.js b/tests/background-step7-recovery.test.js index e906140..c117f69 100644 --- a/tests/background-step7-recovery.test.js +++ b/tests/background-step7-recovery.test.js @@ -10,8 +10,7 @@ test('step 8 submits login verification directly without replaying step 7', asyn const calls = { ensureReady: 0, ensureReadyOptions: [], - executeStep7: 0, - sleep: [], + rerunStep7: 0, resolveOptions: null, setStates: [], }; @@ -32,8 +31,8 @@ test('step 8 submits login verification directly without replaying step 7', asyn calls.ensureReadyOptions.push(options || null); return { state: 'verification_page', displayedEmail: 'display.user@example.com' }; }, - executeStep7: async () => { - calls.executeStep7 += 1; + rerunStep7ForStep8Recovery: async () => { + calls.rerunStep7 += 1; }, getOAuthFlowRemainingMs: async () => 5000, getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 5000), @@ -59,9 +58,6 @@ test('step 8 submits login verification directly without replaying step 7', asyn }, setStepStatus: async () => {}, shouldUseCustomRegistrationEmail: () => false, - sleepWithStop: async (ms) => { - calls.sleep.push(ms); - }, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8, throwIfStopped: () => {}, @@ -79,8 +75,7 @@ test('step 8 submits login verification directly without replaying step 7', asyn assert.equal(calls.resolveOptions.beforeSubmit, undefined); assert.equal(calls.ensureReady, 1); - assert.equal(calls.executeStep7, 0); - assert.deepStrictEqual(calls.sleep, []); + assert.equal(calls.rerunStep7, 0); assert.equal(calls.resolveOptions.filterAfterTimestamp, 123456); assert.equal(typeof calls.resolveOptions.getRemainingTimeMs, 'function'); assert.equal(await calls.resolveOptions.getRemainingTimeMs({ actionLabel: '登录验证码流程' }), 5000); @@ -107,7 +102,7 @@ test('step 8 disables resend interval for 2925 mailbox polling', async () => { CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', confirmCustomVerificationStepBypass: async () => {}, ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }), - executeStep7: async () => {}, + rerunStep7ForStep8Recovery: async () => {}, getOAuthFlowRemainingMs: async () => 8000, getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000), getMailConfig: () => ({ @@ -130,7 +125,6 @@ test('step 8 disables resend interval for 2925 mailbox polling', async () => { setState: async () => {}, setStepStatus: async () => {}, shouldUseCustomRegistrationEmail: () => false, - sleepWithStop: async () => {}, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8, throwIfStopped: () => {}, @@ -161,7 +155,7 @@ test('step 8 falls back to the run email when the verification page does not exp CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', confirmCustomVerificationStepBypass: async () => {}, ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: '' }), - executeStep7: async () => {}, + rerunStep7ForStep8Recovery: async () => {}, getOAuthFlowRemainingMs: async () => 8000, getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000), getMailConfig: () => ({ @@ -184,7 +178,6 @@ test('step 8 falls back to the run email when the verification page does not exp setState: async () => {}, setStepStatus: async () => {}, shouldUseCustomRegistrationEmail: () => false, - sleepWithStop: async () => {}, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8, throwIfStopped: () => {}, @@ -201,7 +194,7 @@ test('step 8 falls back to the run email when the verification page does not exp test('step 8 does not rerun step 7 when verification submit lands on add-phone', async () => { const calls = { - executeStep7: 0, + rerunStep7: 0, logs: [], }; @@ -217,8 +210,8 @@ test('step 8 does not rerun step 7 when verification submit lands on add-phone', CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', confirmCustomVerificationStepBypass: async () => {}, ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }), - executeStep7: async () => { - calls.executeStep7 += 1; + rerunStep7ForStep8Recovery: async () => { + calls.rerunStep7 += 1; }, getOAuthFlowRemainingMs: async () => 8000, getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000), @@ -242,7 +235,6 @@ test('step 8 does not rerun step 7 when verification submit lands on add-phone', setState: async () => {}, setStepStatus: async () => {}, shouldUseCustomRegistrationEmail: () => false, - sleepWithStop: async () => {}, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8, throwIfStopped: () => {}, @@ -257,6 +249,6 @@ test('step 8 does not rerun step 7 when verification submit lands on add-phone', /add-phone/ ); - assert.equal(calls.executeStep7, 0); + assert.equal(calls.rerunStep7, 0); assert.ok(!calls.logs.some(({ message }) => /准备从步骤 7 重新开始/.test(message))); }); diff --git a/tests/step8-restart-step7-error.test.js b/tests/step8-restart-step7-error.test.js index 2ad8622..621be5e 100644 --- a/tests/step8-restart-step7-error.test.js +++ b/tests/step8-restart-step7-error.test.js @@ -117,10 +117,10 @@ return { test('step 8 reruns step 7 when auth page enters login timeout retry state', async () => { const calls = { - executeStep7: 0, + rerunStep7: 0, ensureReady: 0, logs: [], - sleeps: [], + rerunOptions: [], resolveCalls: 0, }; @@ -142,8 +142,9 @@ test('step 8 reruns step 7 when auth page enters login timeout retry state', asy } return { state: 'verification_page' }; }, - executeStep7: async () => { - calls.executeStep7 += 1; + rerunStep7ForStep8Recovery: async (options) => { + calls.rerunStep7 += 1; + calls.rerunOptions.push(options || null); }, getOAuthFlowRemainingMs: async () => 8000, getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000), @@ -167,9 +168,6 @@ test('step 8 reruns step 7 when auth page enters login timeout retry state', asy 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: () => {}, @@ -181,9 +179,13 @@ test('step 8 reruns step 7 when auth page enters login timeout retry state', asy oauthUrl: 'https://oauth.example/latest', }); - assert.equal(calls.executeStep7, 1); + assert.equal(calls.rerunStep7, 1); assert.equal(calls.ensureReady, 2); assert.equal(calls.resolveCalls, 1); assert.equal(calls.logs.some(({ message }) => /重新开始|重新发起/.test(message)), true); - assert.deepStrictEqual(calls.sleeps, [3000]); + assert.deepStrictEqual(calls.rerunOptions, [ + { + logMessage: '步骤 8:认证页进入重试/超时报错状态,正在回到步骤 7 重新发起登录流程...', + }, + ]); }); diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 9aab010..15ddeb6 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -46,7 +46,7 @@ - `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。 - `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息。 - `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API 地址、步骤跳转相关判断。 -- `background/panel-bridge.js`:CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。 +- `background/panel-bridge.js`:CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信;当前 `REQUEST_OAUTH_URL` 链路的面板日志会统一按步骤 7 记录,避免继续误标为旧的步骤 6。 - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail / 2925 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成。 - `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。 - `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 7 次,步骤 8 提交登录验证码后若页面进入 `add-phone / 手机号页`,会直接抛出 fatal 错误而不是把当前步骤视为成功,并在 2925 提交成功后异步触发“删除全部邮件”的清理消息。 @@ -55,11 +55,11 @@ - `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。 - `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成。 -- `background/steps/fetch-login-code.js`:步骤 8 实现,负责登录验证码阶段的邮箱轮询、验证码回填与回退控制;验证码获取后直接提交,不再在提交前回放步骤 7 刷新 OAuth;对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱。 +- `background/steps/fetch-login-code.js`:步骤 8 实现,负责登录验证码阶段的邮箱轮询、验证码回填与回退控制;验证码获取后直接提交,不再在提交前回放步骤 7 刷新 OAuth;对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱;当步骤 8 需要回退到步骤 7 重开授权链时,会走后台统一恢复入口,避免直接并发拉起新的 Step 7。 - `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口。 - `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。 - `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。 -- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。 +- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试;当前每次拿到新的 OAuth 链接都会刷新对应的 6 分钟授权预算,并把预算与具体 OAuth URL 绑定,避免旧链路 deadline 污染新链路。 - `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。 - `background/steps/platform-verify.js`:步骤 10 实现,负责 CPA / SUB2API 回调验证。 - `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。