From 88ceb79af69f44622502cf74f0c07df1d89d87a8 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 26 Apr 2026 05:22:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=99=BB=E9=99=86=E5=8F=8A?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E9=80=BB=E8=BE=91=EF=BC=8C=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=B8=80=E4=B8=AA=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 160 ++++-- background/logging-status.js | 1 - background/message-router.js | 41 +- background/steps/confirm-oauth.js | 6 +- background/steps/fetch-login-code.js | 63 ++- background/steps/oauth-login.js | 13 +- background/steps/platform-verify.js | 18 +- background/verification-flow.js | 26 +- content/signup-page.js | 478 ++++++++---------- content/vps-panel.js | 2 +- data/step-definitions.js | 5 +- sidepanel/sidepanel.js | 5 +- .../background-logging-status-module.test.js | 1 + ...ckground-message-router-step2-skip.test.js | 45 ++ tests/background-step6-retry-limit.test.js | 49 ++ tests/background-step7-recovery.test.js | 72 ++- tests/sidepanel-custom-email-pool.test.js | 40 ++ tests/step-definitions-module.test.js | 7 +- tests/step6-login-state.test.js | 9 +- tests/step6-oauth-consent-skip.test.js | 134 +++++ tests/verification-flow-polling.test.js | 63 +++ 21 files changed, 868 insertions(+), 370 deletions(-) create mode 100644 tests/step6-oauth-consent-skip.test.js diff --git a/background.js b/background.js index 3ec9e4a..ca6482b 100644 --- a/background.js +++ b/background.js @@ -4420,7 +4420,6 @@ function getLoginAuthStateLabel(state) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.getLoginAuthStateLabel) { return loggingStatus.getLoginAuthStateLabel(state); } - state = state === 'oauth_consent_page' ? 'unknown' : state; switch (state) { case 'verification_page': return '登录验证码页'; case 'password_page': return '密码页'; @@ -4487,7 +4486,8 @@ function hasSavedProgress(statuses = {}, stateOverride = null) { return activeStepIds.some((step) => (merged[step] || 'pending') !== 'pending'); } -function getDownstreamStateResets(step) { +function getDownstreamStateResets(step, state = {}) { + const stepKey = getStepExecutionKeyForState(step, state); const plusRuntimeResets = { plusCheckoutTabId: null, plusCheckoutUrl: null, @@ -4574,6 +4574,20 @@ function getDownstreamStateResets(step) { localhostUrl: null, }; } + if (stepKey === 'oauth-login' || stepKey === 'fetch-login-code') { + return { + lastLoginCode: null, + loginVerificationRequestedAt: null, + oauthFlowDeadlineAt: null, + oauthFlowDeadlineSourceUrl: null, + localhostUrl: null, + }; + } + if (stepKey === 'confirm-oauth') { + return { + localhostUrl: null, + }; + } return {}; } @@ -4609,7 +4623,7 @@ async function invalidateDownstreamAfterStepRestart(step, options = {}) { await addLog(`${logLabel},已重置后续步骤状态:${changedSteps.join(', ')}`, 'warn'); } - const resets = getDownstreamStateResets(step); + const resets = getDownstreamStateResets(step, state); if (Object.keys(resets).length) { await setState(resets); broadcastDataUpdate(resets); @@ -5436,6 +5450,24 @@ let resumeWaiter = null; const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000; const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8, 9]); const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 10, 12]); +const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([ + 'open-chatgpt', + 'submit-signup-email', + 'fetch-signup-code', + 'clear-login-cookies', + 'plus-checkout-create', + 'plus-checkout-billing', + 'paypal-approve', + 'plus-checkout-return', + 'oauth-login', + 'fetch-login-code', + 'confirm-oauth', +]); +const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([ + 'fill-password', + 'fill-profile', + 'platform-verify', +]); function waitForStepComplete(step, timeoutMs = 120000) { return new Promise((resolve, reject) => { @@ -5457,7 +5489,26 @@ function waitForStepComplete(step, timeoutMs = 120000) { }); } -function doesStepUseCompletionSignal(step) { +function getStepExecutionKeyForState(step, state = {}) { + if (typeof getStepDefinitionForState !== 'function') { + return ''; + } + return String(getStepDefinitionForState(step, state)?.key || '').trim(); +} + +function doesStepUseBackgroundCompletion(step, state = {}) { + const stepKey = getStepExecutionKeyForState(step, state); + if (stepKey) { + return AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS.has(stepKey); + } + return AUTO_RUN_BACKGROUND_COMPLETED_STEPS.has(step); +} + +function doesStepUseCompletionSignal(step, state = {}) { + const stepKey = getStepExecutionKeyForState(step, state); + if (stepKey) { + return STEP_COMPLETION_SIGNAL_STEP_KEYS.has(stepKey); + } return STEP_COMPLETION_SIGNAL_STEPS.has(step); } @@ -5586,16 +5637,28 @@ async function waitForRunningStepsToFinish(payload = {}) { return currentState; } -const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10, 11, 12]); +const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10, 11, 12, 13]); +const AUTH_CHAIN_STEP_KEYS = new Set([ + 'oauth-login', + 'fetch-login-code', + 'confirm-oauth', + 'platform-verify', +]); let activeTopLevelAuthChainExecution = null; -function isAuthChainStep(step) { +function isAuthChainStep(step, state = {}) { + const stepKey = typeof getStepDefinitionForState === 'function' + ? String(getStepDefinitionForState(step, state)?.key || '').trim() + : ''; + if (stepKey && typeof AUTH_CHAIN_STEP_KEYS !== 'undefined') { + return AUTH_CHAIN_STEP_KEYS.has(stepKey); + } return AUTH_CHAIN_STEP_IDS.has(Number(step)); } -async function acquireTopLevelAuthChainExecution(step) { +async function acquireTopLevelAuthChainExecution(step, state = {}) { const normalizedStep = Number(step); - if (!isAuthChainStep(normalizedStep)) { + if (!isAuthChainStep(normalizedStep, state)) { return { joined: false, release() {}, @@ -5735,7 +5798,8 @@ async function requestStop(options = {}) { async function executeStep(step, options = {}) { const { deferRetryableTransportError = false } = options; console.log(LOG_PREFIX, `Executing step ${step}`); - const authChainClaim = await acquireTopLevelAuthChainExecution(step); + let state = await getState(); + const authChainClaim = await acquireTopLevelAuthChainExecution(step, state); if (authChainClaim.joined) { return; } @@ -5747,7 +5811,7 @@ async function executeStep(step, options = {}) { await addLog(`步骤 ${step} 开始执行`); await humanStepDelay(); - const state = await getState(); + state = await getState(); // Set flow start time on first step if (step === 1 && !state.flowStartTime) { @@ -5765,11 +5829,11 @@ async function executeStep(step, options = {}) { }); } catch (err) { executionError = err; - const state = await getState(); + const errorState = await getState(); if (isStopError(err)) { await setStepStatus(step, 'stopped'); await addLog(`步骤 ${step} 已被用户停止`, 'warn'); - await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, state, getErrorMessage(err)); + await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, errorState, getErrorMessage(err)); throw err; } if (isTerminalSecurityBlockedError(err)) { @@ -5780,10 +5844,10 @@ async function executeStep(step, options = {}) { await handleBrowserSwitchRequired(err); throw new Error(STOP_ERROR_MESSAGE); } - if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step) && isRetryableContentScriptTransportError(err))) { + if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step, errorState) && isRetryableContentScriptTransportError(err))) { await setStepStatus(step, 'failed'); await addLog(`步骤 ${step} 失败:${err.message}`, 'error'); - await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, state, getErrorMessage(err)); + await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, errorState, getErrorMessage(err)); } else { console.warn( LOG_PREFIX, @@ -5813,12 +5877,13 @@ async function executeStepAndWait(step, delayAfter = 2000) { await sleepWithStop(delaySeconds * 1000); } - if (AUTO_RUN_BACKGROUND_COMPLETED_STEPS.has(step)) { + const executionState = await getState(); + if (doesStepUseBackgroundCompletion(step, executionState)) { await addLog(`自动运行:步骤 ${step} 由后台流程负责收尾,执行函数返回后将直接进入下一步。`, 'info'); await executeStep(step); const latestState = await getState(); await addLog(`自动运行:步骤 ${step} 已执行返回,当前状态为 ${latestState.stepStatuses?.[step] || 'pending'},准备继续后续步骤。`, 'info'); - } else if (doesStepUseCompletionSignal(step)) { + } else if (doesStepUseCompletionSignal(step, executionState)) { await addLog(`自动运行:步骤 ${step} 已发起,正在等待完成信号(超时 ${AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS / 1000} 秒)。`, 'info'); await executeStepViaCompletionSignal(step, AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS); await addLog(`自动运行:步骤 ${step} 已收到完成信号,准备继续后续步骤。`, 'info'); @@ -6581,7 +6646,10 @@ async function runAutoSequenceFromStep(startStep, context = {}) { if (restartDecision.blockedByAddPhone) { const addPhoneUrl = restartDecision.authState?.url || 'https://auth.openai.com/add-phone'; - await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 7 重开。`, 'warn'); + const authChainStartStep = typeof getAuthChainStartStepId === 'function' + ? getAuthChainStartStepId(await getState()) + : FINAL_OAUTH_CHAIN_START_STEP; + await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 ${authChainStartStep} 重开。`, 'warn'); } throw err; } @@ -7440,12 +7508,13 @@ async function runPreStep6CookieCleanup() { // Step 7: Login and ensure the auth page reaches the login verification page // ============================================================ -async function refreshOAuthUrlBeforeStep6(state) { +async function refreshOAuthUrlBeforeStep6(state, options = {}) { + const visibleStep = Number(options.visibleStep) || Number(state?.visibleStep) || 7; if (state?.contributionModeExpected && !state?.contributionMode) { - throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。'); + throw new Error(`步骤 ${visibleStep}:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。`); } if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) { - await addLog('步骤 7:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...', 'info'); + await addLog(`步骤 ${visibleStep}:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...`, 'info'); const contributionState = await contributionOAuthManager.startContributionFlow({ nickname: state.contributionNickname || '', openAuthTab: false, @@ -7458,9 +7527,9 @@ async function refreshOAuthUrlBeforeStep6(state) { await handleStepData(1, { oauthUrl }); return oauthUrl; } - await addLog(`步骤 7:contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info'); + await addLog(`步骤 ${visibleStep}:contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info'); console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel'); - const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 7' }); + const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: `步骤 ${visibleStep}` }); await handleStepData(1, refreshResult); if (!refreshResult?.oauthUrl) { @@ -7470,9 +7539,12 @@ async function refreshOAuthUrlBeforeStep6(state) { return refreshResult.oauthUrl; } -function buildOAuthFlowTimeoutError(step, actionLabel = '后续授权流程') { +function buildOAuthFlowTimeoutError(step, actionLabel = '后续授权流程', state = {}) { + const restartStep = typeof getAuthChainStartStepId === 'function' + ? getAuthChainStartStepId(state) + : FINAL_OAUTH_CHAIN_START_STEP; return new Error( - `步骤 ${step}:从拿到 OAuth 登录地址开始,${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟内未完成${actionLabel},结束当前链路,准备从步骤 7 重新开始。` + `步骤 ${step}:从拿到 OAuth 登录地址开始,${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟内未完成${actionLabel},结束当前链路,准备从步骤 ${restartStep} 重新开始。` ); } @@ -7523,7 +7595,7 @@ async function getOAuthFlowRemainingMs(options = {}) { const remainingMs = deadlineAt - Date.now(); if (remainingMs <= 0) { - throw buildOAuthFlowTimeoutError(step, actionLabel); + throw buildOAuthFlowTimeoutError(step, actionLabel, state); } return remainingMs; @@ -7539,9 +7611,11 @@ async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) { const budgetMs = remainingMs - reserveMs; if (budgetMs <= 0) { + const stateForError = options.state || await getState(); throw buildOAuthFlowTimeoutError( Number(options.step) || 7, - String(options.actionLabel || '后续授权流程').trim() || '后续授权流程' + String(options.actionLabel || '后续授权流程').trim() || '后续授权流程', + stateForError ); } @@ -7615,7 +7689,7 @@ async function getPostStep6AutoRestartDecision(step, error) { let authState = null; try { authState = await getLoginAuthStateFromContent({ - logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 7 重开...`, + logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 ${authChainStartStep} 重开...`, }); } catch (inspectError) { console.warn(LOG_PREFIX, '[AutoRun] failed to inspect login auth state after post-step6 error', { @@ -7671,6 +7745,8 @@ async function getLoginAuthStateFromContent(options = {}) { } async function ensureStep8VerificationPageReady(options = {}) { + const visibleStep = Number(options.visibleStep) || 8; + const authLoginStep = Number(options.authLoginStep) || (visibleStep >= 11 ? 10 : 7); const pageState = await getLoginAuthStateFromContent(options); if (pageState.state === 'verification_page') { return pageState; @@ -7682,17 +7758,17 @@ async function ensureStep8VerificationPageReady(options = {}) { if (pageState.state === 'login_timeout_error_page') { const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; - throw new Error(`STEP8_RESTART_STEP7::步骤 8:当前认证页进入登录超时报错页,请回到步骤 7 重新开始。${urlPart}`.trim()); + throw new Error(`STEP8_RESTART_STEP7::步骤 ${visibleStep}:当前认证页进入登录超时报错页,请回到步骤 ${authLoginStep} 重新开始。${urlPart}`.trim()); } if (pageState.state === 'add_phone_page') { const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; - throw new Error(`步骤 8:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim()); + throw new Error(`步骤 ${visibleStep}:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim()); } const stateLabel = getLoginAuthStateLabel(pageState.state); const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; - throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:${stateLabel}.${urlPart}`.trim()); + throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 ${authLoginStep}。当前状态:${stateLabel}.${urlPart}`.trim()); } async function rerunStep7ForStep8Recovery(options = {}) { @@ -7703,27 +7779,33 @@ async function rerunStep7ForStep8Recovery(options = {}) { throwIfStopped(); const initialState = await getState(); + const authLoginStep = typeof getAuthChainStartStepId === 'function' + ? getAuthChainStartStepId(initialState) + : FINAL_OAUTH_CHAIN_START_STEP; await addLog(logMessage, 'warn'); - await setStepStatus(7, 'running'); - await addLog('步骤 7 开始执行'); + await setStepStatus(authLoginStep, 'running'); + await addLog(`步骤 ${authLoginStep} 开始执行`); try { - await step7Executor.executeStep7(initialState); + await step7Executor.executeStep7({ + ...initialState, + visibleStep: authLoginStep, + }); } catch (err) { const latestState = await getState(); if (isStopError(err)) { - await setStepStatus(7, 'stopped'); - await addLog('步骤 7 已被用户停止', 'warn'); - await appendManualAccountRunRecordIfNeeded('step7_stopped', latestState, getErrorMessage(err)); + await setStepStatus(authLoginStep, 'stopped'); + await addLog(`步骤 ${authLoginStep} 已被用户停止`, 'warn'); + await appendManualAccountRunRecordIfNeeded(`step${authLoginStep}_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)); + await setStepStatus(authLoginStep, 'failed'); + await addLog(`步骤 ${authLoginStep} 失败:${getErrorMessage(err)}`, 'error'); + await appendManualAccountRunRecordIfNeeded(`step${authLoginStep}_failed`, latestState, getErrorMessage(err)); throw err; } diff --git a/background/logging-status.js b/background/logging-status.js index 47d3c2c..15aeff7 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -70,7 +70,6 @@ } function getLoginAuthStateLabel(state) { - state = state === 'oauth_consent_page' ? 'unknown' : state; switch (state) { case 'verification_page': return '登录验证码页'; diff --git a/background/message-router.js b/background/message-router.js index 8261b53..a95bf59 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -137,6 +137,31 @@ return ''; } + function isStepProtectedFromAutoSkip(status) { + return status === 'running' + || status === 'completed' + || status === 'manual_completed' + || status === 'skipped'; + } + + function findStepByKeyAfter(currentStep, targetKey, state = {}) { + const activeStepIds = typeof getStepIdsForState === 'function' + ? getStepIdsForState(state) + : []; + const candidates = activeStepIds.length ? activeStepIds : [Number(currentStep) + 1, 8]; + return candidates.find((stepId) => { + const numericStep = Number(stepId); + if (!Number.isFinite(numericStep) || numericStep <= Number(currentStep)) { + return false; + } + const stepKey = getStepKeyForState(numericStep, state); + if (stepKey) { + return stepKey === targetKey; + } + return targetKey === 'fetch-login-code' && Number(currentStep) === 7 && numericStep === 8; + }) || null; + } + async function handlePlatformVerifyStepData(payload) { if (payload.localhostUrl) { await closeLocalhostCallbackTabs(payload.localhostUrl); @@ -180,7 +205,18 @@ const stepKey = getStepKeyForState(step, stateForStep); if (stepKey === 'oauth-login') { - if (payload.loginVerificationRequestedAt) { + if (payload.skipLoginVerificationStep) { + await setState({ loginVerificationRequestedAt: null }); + const latestState = await getState(); + const loginCodeStep = findStepByKeyAfter(step, 'fetch-login-code', latestState); + if (loginCodeStep) { + const currentStatus = latestState.stepStatuses?.[loginCodeStep]; + if (!isStepProtectedFromAutoSkip(currentStatus)) { + await setStepStatus(loginCodeStep, 'skipped'); + await addLog(`步骤 ${step}:认证页已直接进入 OAuth 授权页,已自动跳过步骤 ${loginCodeStep} 的登录验证码。`, 'warn'); + } + } + } else if (payload.loginVerificationRequestedAt) { await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); } return; @@ -484,7 +520,8 @@ await setPersistentSettings({ emailPrefix: message.payload.emailPrefix }); await setState({ emailPrefix: message.payload.emailPrefix }); } - if (doesStepUseCompletionSignal(step)) { + const executionState = await getState(); + if (doesStepUseCompletionSignal(step, executionState)) { await executeStepViaCompletionSignal(step); } else { await executeStep(step); diff --git a/background/steps/confirm-oauth.js b/background/steps/confirm-oauth.js index 88153a7..88607c9 100644 --- a/background/steps/confirm-oauth.js +++ b/background/steps/confirm-oauth.js @@ -38,10 +38,14 @@ return visibleStep > 0 ? visibleStep : fallback; } + function getAuthLoginStepForVisibleStep(visibleStep) { + return visibleStep >= 12 ? 10 : 7; + } + async function executeStep9(state) { const visibleStep = getVisibleStep(state, 9); if (!state.oauthUrl) { - const authLoginStep = visibleStep === 11 ? 10 : 7; + const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep); throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`); } diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index a379a77..c25b2f4 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -31,25 +31,34 @@ throwIfStopped, } = deps; - async function getStep8ReadyTimeoutMs(actionLabel, expectedOauthUrl = '') { + function getVisibleStep(state, fallback = 8) { + const visibleStep = Math.floor(Number(state?.visibleStep) || 0); + return visibleStep > 0 ? visibleStep : fallback; + } + + function getAuthLoginStepForVisibleStep(visibleStep) { + return visibleStep >= 11 ? 10 : 7; + } + + async function getStep8ReadyTimeoutMs(actionLabel, expectedOauthUrl = '', visibleStep = 8) { if (typeof getOAuthFlowStepTimeoutMs !== 'function') { return 15000; } return getOAuthFlowStepTimeoutMs(15000, { - step: 8, + step: visibleStep, actionLabel, oauthUrl: expectedOauthUrl, }); } - function getStep8RemainingTimeResolver(expectedOauthUrl = '') { + function getStep8RemainingTimeResolver(expectedOauthUrl = '', visibleStep = 8) { if (typeof getOAuthFlowRemainingMs !== 'function') { return undefined; } return async (details = {}) => getOAuthFlowRemainingMs({ - step: 8, + step: visibleStep, actionLabel: details.actionLabel || '登录验证码流程', oauthUrl: expectedOauthUrl, }); @@ -96,6 +105,7 @@ } async function runStep8Attempt(state) { + const visibleStep = getVisibleStep(state, 8); const mail = getMailConfig(state); if (mail.error) throw new Error(mail.error); @@ -110,14 +120,16 @@ await chrome.tabs.update(authTabId, { active: true }); } else { if (!state.oauthUrl) { - throw new Error('缺少登录用 OAuth 链接,请先完成步骤 7。'); + throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`); } await reuseOrCreateTab('signup-page', state.oauthUrl); } throwIfStopped(); const pageState = await ensureStep8VerificationPageReady({ - timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || ''), + visibleStep, + authLoginStep: getAuthLoginStepForVisibleStep(visibleStep), + timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep), }); const shouldCompareVerificationEmail = mail.provider !== '2925'; const displayedVerificationEmail = shouldCompareVerificationEmail @@ -131,22 +143,25 @@ step8VerificationTargetEmail: displayedVerificationEmail || '', }); - await addLog('步骤 8:登录验证码页面已就绪,开始获取验证码。', 'info'); + await addLog(`步骤 ${visibleStep}:登录验证码页面已就绪,开始获取验证码。`, 'info'); if (shouldCompareVerificationEmail && displayedVerificationEmail) { - await addLog(`步骤 8:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info'); + await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info'); } if (shouldUseCustomRegistrationEmail(state)) { - await confirmCustomVerificationStepBypass(8); + await confirmCustomVerificationStepBypass(8, { + completionStep: visibleStep, + promptStep: visibleStep, + }); return; } if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { - await addLog('步骤 8:正在确认 iCloud 邮箱登录态...', 'info'); + await addLog(`步骤 ${visibleStep}:正在确认 iCloud 邮箱登录态...`, 'info'); await ensureIcloudMailSession({ state, step: 8, - actionLabel: '步骤 8:确认 iCloud 邮箱登录态', + actionLabel: `步骤 ${visibleStep}:确认 iCloud 邮箱登录态`, }); } @@ -156,22 +171,22 @@ || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER ) { - await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`); + await addLog(`步骤 ${visibleStep}:正在通过 ${mail.label} 轮询验证码...`); } else { - await addLog(`步骤 8:正在打开${mail.label}...`); + await addLog(`步骤 ${visibleStep}:正在打开${mail.label}...`); if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') { await ensureMail2925MailboxSession({ accountId: state.currentMail2925AccountId || null, forceRelogin: false, allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool), expectedMailboxEmail: getExpectedMail2925MailboxEmail(state), - actionLabel: 'Step 8: ensure 2925 mailbox session', + actionLabel: `Step ${visibleStep}: ensure 2925 mailbox session`, }); } else { await focusOrOpenMailTab(mail); } if (mail.provider === '2925') { - await addLog(`步骤 8:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info'); + await addLog(`步骤 ${visibleStep}:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info'); } } @@ -179,10 +194,11 @@ ...state, step8VerificationTargetEmail: displayedVerificationEmail || '', }, mail, { + completionStep: visibleStep, filterAfterTimestamp: verificationFilterAfterTimestamp, sessionKey: verificationSessionKey, disableTimeBudgetCap: mail.provider === '2925', - getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || ''), + getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep), requestFreshCodeFirst: false, targetEmail: fixedTargetEmail, resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') @@ -206,6 +222,8 @@ await runStep8Attempt(currentState); return; } catch (err) { + const visibleStep = getVisibleStep(currentState, 8); + const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep); if (!isVerificationMailPollingError(err) && !isStep8RestartStep7Error(err)) { throw err; } @@ -218,26 +236,27 @@ mailPollingAttempt += 1; await addLog( isStep8RestartStep7Error(err) - ? `步骤 8:检测到认证页进入重试/超时报错状态,准备从步骤 7 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...` - : `步骤 8:检测到邮箱轮询类失败,准备从步骤 7 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`, + ? `步骤 ${visibleStep}:检测到认证页进入重试/超时报错状态,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...` + : `步骤 ${visibleStep}:检测到邮箱轮询类失败,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`, 'warn' ); await rerunStep7ForStep8Recovery({ logMessage: isStep8RestartStep7Error(err) - ? '步骤 8:认证页进入重试/超时报错状态,正在回到步骤 7 重新发起登录流程...' - : '步骤 8:正在回到步骤 7,重新发起登录验证码流程...', + ? `步骤 ${visibleStep}:认证页进入重试/超时报错状态,正在回到步骤 ${authLoginStep} 重新发起登录流程...` + : `步骤 ${visibleStep}:正在回到步骤 ${authLoginStep},重新发起登录验证码流程...`, }); currentState = await getState(); } } + const visibleStep = getVisibleStep(currentState, 8); if (lastMailPollingError) { throw new Error( - `步骤 8:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}` + `步骤 ${visibleStep}:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}` ); } - throw new Error('步骤 8:登录验证码流程未成功完成。'); + throw new Error(`步骤 ${visibleStep}:登录验证码流程未成功完成。`); } return { executeStep8 }; diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index d57a261..d12ca49 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -60,7 +60,7 @@ try { const currentState = attempt === 1 ? state : await getState(); const password = currentState.password || currentState.customPassword || ''; - const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState); + const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState, { visibleStep }); if (typeof startOAuthFlowTimeoutWindow === 'function') { await startOAuthFlowTimeoutWindow({ step: visibleStep, oauthUrl }); } @@ -105,9 +105,16 @@ } if (isStep6SuccessResult(result)) { - await completeStepFromBackground(visibleStep, { + const completionPayload = { loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, - }); + }; + if (result.skipLoginVerificationStep) { + completionPayload.skipLoginVerificationStep = true; + } + if (result.directOAuthConsentPage) { + completionPayload.directOAuthConsentPage = true; + } + await completeStepFromBackground(visibleStep, completionPayload); return; } diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js index fefaac5..b88f295 100644 --- a/background/steps/platform-verify.js +++ b/background/steps/platform-verify.js @@ -32,21 +32,25 @@ } function getConfirmStepForVisibleStep(visibleStep) { - return visibleStep === 12 ? 11 : 9; + return visibleStep >= 13 ? 12 : 9; } - function parseLocalhostCallback(rawUrl) { + function getAuthLoginStepForVisibleStep(visibleStep) { + return visibleStep >= 13 ? 10 : 7; + } + + function parseLocalhostCallback(rawUrl, visibleStep = 10, confirmStep = 9) { let parsed; try { parsed = new URL(rawUrl); } catch { - throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 9。'); + throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 ${confirmStep}。`); } const code = normalizeString(parsed.searchParams.get('code')); const state = normalizeString(parsed.searchParams.get('state')); if (!code || !state) { - throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。'); + throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmStep}。`); } return { @@ -191,16 +195,16 @@ throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`); } if (!state.codex2apiSessionId) { - throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${visibleStep === 12 ? 10 : 7}。`); + throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`); } if (!normalizeString(state.codex2apiAdminKey)) { throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。'); } - const callback = parseLocalhostCallback(state.localhostUrl); + const callback = parseLocalhostCallback(state.localhostUrl, visibleStep, confirmStep); const expectedState = normalizeString(state.codex2apiOAuthState); if (expectedState && expectedState !== callback.state) { - throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。'); + throw new Error(`Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`); } const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl); diff --git a/background/verification-flow.js b/background/verification-flow.js index dbc9594..229bed7 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -175,25 +175,32 @@ return Math.max(0, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1) - 1); } - async function confirmCustomVerificationStepBypass(step) { + function getCompletionStep(step, options = {}) { + const completionStep = Number(options.completionStep); + return Number.isFinite(completionStep) && completionStep > 0 ? completionStep : step; + } + + async function confirmCustomVerificationStepBypass(step, options = {}) { + const completionStep = getCompletionStep(step, options); + const promptStep = getCompletionStep(step, { completionStep: options.promptStep ?? completionStep }); const verificationLabel = getVerificationCodeLabel(step); - await addLog(`步骤 ${step}:当前为自定义邮箱模式,请手动在页面中输入${verificationLabel}验证码并进入下一页面。`, 'warn'); + await addLog(`步骤 ${completionStep}:当前为自定义邮箱模式,请手动在页面中输入${verificationLabel}验证码并进入下一页面。`, 'warn'); let response = null; try { - response = await confirmCustomVerificationStepBypassRequest(step); + response = await confirmCustomVerificationStepBypassRequest(promptStep); } catch { - throw new Error(`步骤 ${step}:无法打开确认弹窗,请先保持侧边栏打开后重试。`); + throw new Error(`步骤 ${completionStep}:无法打开确认弹窗,请先保持侧边栏打开后重试。`); } if (response?.error) { throw new Error(response.error); } if (step === 8 && response?.addPhoneDetected) { - throw new Error('步骤 8:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone'); + throw new Error(`步骤 ${completionStep}:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone`); } if (!response?.confirmed) { - throw new Error(`步骤 ${step}:已取消手动${verificationLabel}验证码确认。`); + throw new Error(`步骤 ${completionStep}:已取消手动${verificationLabel}验证码确认。`); } await setState({ @@ -201,8 +208,8 @@ signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, }); - await deps.setStepStatus(step, 'skipped'); - await addLog(`步骤 ${step}:已确认手动完成${verificationLabel}验证码输入,当前步骤已跳过。`, 'warn'); + await deps.setStepStatus(completionStep, 'skipped'); + await addLog(`步骤 ${completionStep}:已确认手动完成${verificationLabel}验证码输入,当前步骤已跳过。`, 'warn'); } function getVerificationPollPayload(step, state, overrides = {}) { @@ -781,6 +788,7 @@ } async function resolveVerificationStep(step, state, mail, options = {}) { + const completionStep = getCompletionStep(step, options); const stateKey = getVerificationCodeStateKey(step); const rejectedCodes = new Set(); const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER @@ -918,7 +926,7 @@ [stateKey]: result.code, }); - await completeStepFromBackground(step, { + await completeStepFromBackground(completionStep, { emailTimestamp: result.emailTimestamp, code: result.code, phoneVerificationRequired: Boolean(submitResult.addPhonePage), diff --git a/content/signup-page.js b/content/signup-page.js index 0c85113..9c6caf6 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -1803,6 +1803,13 @@ function inspectLoginAuthState() { }; } + if (consentReady) { + return { + ...baseState, + state: 'oauth_consent_page', + }; + } + return baseState; } @@ -1830,7 +1837,7 @@ function serializeLoginAuthState(snapshot) { } function getLoginAuthStateLabel(snapshot) { - const state = snapshot?.state === 'oauth_consent_page' ? 'unknown' : snapshot?.state; + const state = snapshot?.state; switch (state) { case 'verification_page': return '登录验证码页'; @@ -1887,13 +1894,32 @@ async function waitForLoginVerificationPageReady(timeout = 10000) { } function createStep6SuccessResult(snapshot, options = {}) { - return { + const result = { step6Outcome: 'success', state: snapshot?.state || 'verification_page', url: snapshot?.url || location.href, via: options.via || '', loginVerificationRequestedAt: options.loginVerificationRequestedAt || null, }; + + if (options.skipLoginVerificationStep) { + result.skipLoginVerificationStep = true; + } + if (options.directOAuthConsentPage) { + result.directOAuthConsentPage = true; + } + + return result; +} + +function createStep6OAuthConsentSuccessResult(snapshot, options = {}) { + return createStep6SuccessResult(snapshot, { + ...options, + via: options.via || 'oauth_consent_page', + loginVerificationRequestedAt: null, + skipLoginVerificationStep: true, + directOAuthConsentPage: true, + }); } function createStep6RecoverableResult(reason, snapshot, options = {}) { @@ -1948,6 +1974,15 @@ async function createStep6LoginTimeoutRecoveryTransition(reason, snapshot, messa }; } + if (resolvedSnapshot.state === 'oauth_consent_page') { + return { + action: 'done', + result: createStep6OAuthConsentSuccessResult(resolvedSnapshot, { + via, + }), + }; + } + if (resolvedSnapshot.state === 'password_page') { log('步骤 7:登录超时报错页恢复后已进入密码页,继续当前登录流程。', 'warn'); return { action: 'password', snapshot: resolvedSnapshot }; @@ -2007,6 +2042,13 @@ async function finalizeStep6VerificationReady(options = {}) { }); } + if (snapshot.state === 'oauth_consent_page') { + log(`${logLabel}:认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。`, 'ok'); + return createStep6OAuthConsentSuccessResult(snapshot, { + via: `${via}_oauth_consent`, + }); + } + if (snapshot.state === 'login_timeout_error_page') { log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn'); return createStep6LoginTimeoutRecoverableResult( @@ -2037,6 +2079,12 @@ async function finalizeStep6VerificationReady(options = {}) { loginVerificationRequestedAt, }); } + if (snapshot.state === 'oauth_consent_page') { + log(`${logLabel}:认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。`, 'ok'); + return createStep6OAuthConsentSuccessResult(snapshot, { + via: `${via}_oauth_consent`, + }); + } if (snapshot.state === 'login_timeout_error_page') { log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn'); return createStep6LoginTimeoutRecoverableResult( @@ -2059,21 +2107,14 @@ async function finalizeStep6VerificationReady(options = {}) { } function normalizeStep6Snapshot(snapshot) { - if (snapshot?.state !== 'oauth_consent_page') { - return snapshot; - } - - return { - ...snapshot, - state: 'unknown', - }; + return snapshot; } function throwForStep6FatalState(snapshot) { snapshot = normalizeStep6Snapshot(snapshot); switch (snapshot?.state) { case 'oauth_consent_page': - throw new Error(`当前页面已进入 OAuth 授权页,未经过登录验证码页,无法完成步骤 7。URL: ${snapshot.url}`); + return; case 'add_phone_page': throw new Error(`当前页面已进入手机号页面,未经过登录验证码页,无法完成步骤 7。URL: ${snapshot.url}`); case 'unknown': @@ -2595,88 +2636,55 @@ async function fillVerificationCode(step, payload) { // Step 7: Login with registered account (on OAuth auth page) // ============================================================ -async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000) { - const start = Date.now(); - let snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); +function getStep6OptionMessage(value, snapshot) { + return typeof value === 'function' ? value(snapshot) : String(value || ''); +} - while (Date.now() - start < timeout) { - throwIfStopped(); - snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); +async function resolveStep6PostSubmitSnapshot(snapshot, options = {}) { + const normalizedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); + const { + via = 'post_submit', + loginVerificationRequestedAt = null, + oauthConsentVia = `${via}_oauth_consent`, + timeoutRecoveryReason = 'login_timeout_error_page', + timeoutRecoveryMessage = '登录提交后进入登录超时报错页。', + timeoutRecoveryVia = `${via}_timeout_recovered`, + allowPasswordAction = false, + allowEmailAction = false, + allowFinalPasswordAction = false, + allowFinalEmailAction = false, + allowFinalSwitchAction = false, + final = false, + addPhoneMessage, + } = options; - if (snapshot.state === 'verification_page') { - return { - action: 'done', - result: createStep6SuccessResult(snapshot, { - via: 'email_submit', - loginVerificationRequestedAt: emailSubmittedAt, - }), - }; - } - - if (snapshot.state === 'password_page') { - return { action: 'password', snapshot }; - } - - if (snapshot.state === 'login_timeout_error_page') { - const transition = await createStep6LoginTimeoutRecoveryTransition( - 'login_timeout_error_page', - snapshot, - '提交邮箱后进入登录超时报错页。', - { - loginVerificationRequestedAt: emailSubmittedAt, - via: 'email_submit_timeout_recovered', - } - ); - if (transition.action === 'done') { - return { - action: 'done', - result: transition.result, - }; - } - if (transition.action === 'password') { - return { action: 'password', snapshot: transition.snapshot }; - } - if (transition.action === 'email') { - return { action: 'email', snapshot: transition.snapshot }; - } - return { - action: 'recoverable', - result: transition.result, - }; - } - - if (snapshot.state === 'oauth_consent_page') { - throw new Error(`提交邮箱后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); - } - - if (snapshot.state === 'add_phone_page') { - throw new Error(`提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); - } - - await sleep(250); - } - - snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); - if (snapshot.state === 'verification_page') { + if (normalizedSnapshot.state === 'verification_page') { return { action: 'done', - result: createStep6SuccessResult(snapshot, { - via: 'email_submit', - loginVerificationRequestedAt: emailSubmittedAt, + result: createStep6SuccessResult(normalizedSnapshot, { + via, + loginVerificationRequestedAt, }), }; } - if (snapshot.state === 'password_page') { - return { action: 'password', snapshot }; + + if (normalizedSnapshot.state === 'oauth_consent_page') { + return { + action: 'done', + result: createStep6OAuthConsentSuccessResult(normalizedSnapshot, { + via: oauthConsentVia, + }), + }; } - if (snapshot.state === 'login_timeout_error_page') { + + if (normalizedSnapshot.state === 'login_timeout_error_page') { const transition = await createStep6LoginTimeoutRecoveryTransition( - 'login_timeout_error_page', - snapshot, - '提交邮箱后进入登录超时报错页。', + timeoutRecoveryReason, + normalizedSnapshot, + timeoutRecoveryMessage, { - loginVerificationRequestedAt: emailSubmittedAt, - via: 'email_submit_timeout_recovered', + loginVerificationRequestedAt, + via: timeoutRecoveryVia, } ); if (transition.action === 'done') { @@ -2696,213 +2704,116 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120 result: transition.result, }; } - if (snapshot.state === 'oauth_consent_page') { - throw new Error(`提交邮箱后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); + + if (normalizedSnapshot.state === 'password_page') { + if (allowPasswordAction || (final && allowFinalPasswordAction)) { + return { action: 'password', snapshot: normalizedSnapshot }; + } + if (final && allowFinalSwitchAction && normalizedSnapshot.switchTrigger) { + return { action: 'switch', snapshot: normalizedSnapshot }; + } } - if (snapshot.state === 'add_phone_page') { - throw new Error(`提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); + + if (normalizedSnapshot.state === 'email_page' && (allowEmailAction || (final && allowFinalEmailAction))) { + return { action: 'email', snapshot: normalizedSnapshot }; + } + + if (normalizedSnapshot.state === 'add_phone_page') { + const message = getStep6OptionMessage(addPhoneMessage, normalizedSnapshot) + || `登录提交后页面进入手机号页面。URL: ${normalizedSnapshot.url || location.href}`; + throw new Error(message); + } + + return null; +} + +async function waitForStep6PostSubmitTransition(options = {}) { + const { + timeout = 10000, + stalledReason = 'post_submit_stalled', + stalledMessage = '登录提交后未进入可识别的下一页。', + } = options; + const start = Date.now(); + let snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); + + while (Date.now() - start < timeout) { + throwIfStopped(); + snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); + const transition = await resolveStep6PostSubmitSnapshot(snapshot, { + ...options, + final: false, + }); + if (transition) { + return transition; + } + await sleep(250); + } + + snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); + const transition = await resolveStep6PostSubmitSnapshot(snapshot, { + ...options, + final: true, + }); + if (transition) { + return transition; } return { action: 'recoverable', - result: createStep6RecoverableResult('email_submit_stalled', snapshot, { - message: '提交邮箱后长时间未进入密码页或登录验证码页。', + result: createStep6RecoverableResult(stalledReason, snapshot, { + message: stalledMessage, + loginVerificationRequestedAt: options.loginVerificationRequestedAt || null, }), }; } +async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000) { + return waitForStep6PostSubmitTransition({ + timeout, + via: 'email_submit', + oauthConsentVia: 'email_submit_oauth_consent', + loginVerificationRequestedAt: emailSubmittedAt, + timeoutRecoveryMessage: '提交邮箱后进入登录超时报错页。', + timeoutRecoveryVia: 'email_submit_timeout_recovered', + allowPasswordAction: true, + stalledReason: 'email_submit_stalled', + stalledMessage: '提交邮箱后长时间未进入密码页或登录验证码页。', + addPhoneMessage: (snapshot) => `提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`, + }); +} + async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout = 10000) { - const start = Date.now(); - let snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); - - while (Date.now() - start < timeout) { - throwIfStopped(); - snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); - - if (snapshot.state === 'verification_page') { - return { - action: 'done', - result: createStep6SuccessResult(snapshot, { - via: 'password_submit', - loginVerificationRequestedAt: passwordSubmittedAt, - }), - }; - } - - if (snapshot.state === 'login_timeout_error_page') { - const transition = await createStep6LoginTimeoutRecoveryTransition( - 'login_timeout_error_page', - snapshot, - '提交密码后进入登录超时报错页。', - { - loginVerificationRequestedAt: passwordSubmittedAt, - via: 'password_submit_timeout_recovered', - } - ); - if (transition.action === 'done') { - return { - action: 'done', - result: transition.result, - }; - } - if (transition.action === 'password') { - return { action: 'password', snapshot: transition.snapshot }; - } - if (transition.action === 'email') { - return { action: 'email', snapshot: transition.snapshot }; - } - return { - action: 'recoverable', - result: transition.result, - }; - } - - if (snapshot.state === 'oauth_consent_page') { - throw new Error(`提交密码后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); - } - - if (snapshot.state === 'add_phone_page') { - throw new Error(`提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); - } - - await sleep(250); - } - - snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); - if (snapshot.state === 'verification_page') { - return { - action: 'done', - result: createStep6SuccessResult(snapshot, { - via: 'password_submit', - loginVerificationRequestedAt: passwordSubmittedAt, - }), - }; - } - if (snapshot.state === 'login_timeout_error_page') { - const transition = await createStep6LoginTimeoutRecoveryTransition( - 'login_timeout_error_page', - snapshot, - '提交密码后进入登录超时报错页。', - { - loginVerificationRequestedAt: passwordSubmittedAt, - via: 'password_submit_timeout_recovered', - } - ); - if (transition.action === 'done') { - return { - action: 'done', - result: transition.result, - }; - } - if (transition.action === 'password') { - return { action: 'password', snapshot: transition.snapshot }; - } - if (transition.action === 'email') { - return { action: 'email', snapshot: transition.snapshot }; - } - return { - action: 'recoverable', - result: transition.result, - }; - } - if (snapshot.state === 'oauth_consent_page') { - throw new Error(`提交密码后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); - } - if (snapshot.state === 'add_phone_page') { - throw new Error(`提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); - } - if (snapshot.state === 'password_page' && snapshot.switchTrigger) { - return { action: 'switch', snapshot }; - } - - return { - action: 'recoverable', - result: createStep6RecoverableResult('password_submit_stalled', snapshot, { - message: '提交密码后仍未进入登录验证码页。', - }), - }; + return waitForStep6PostSubmitTransition({ + timeout, + via: 'password_submit', + oauthConsentVia: 'password_submit_oauth_consent', + loginVerificationRequestedAt: passwordSubmittedAt, + timeoutRecoveryMessage: '提交密码后进入登录超时报错页。', + timeoutRecoveryVia: 'password_submit_timeout_recovered', + allowFinalSwitchAction: true, + stalledReason: 'password_submit_stalled', + stalledMessage: '提交密码后仍未进入登录验证码页。', + addPhoneMessage: (snapshot) => `提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`, + }); } async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeout = 10000) { - const start = Date.now(); - let snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); + const transition = await waitForStep6PostSubmitTransition({ + timeout, + via: 'switch_to_one_time_code_login', + oauthConsentVia: 'switch_to_one_time_code_oauth_consent', + loginVerificationRequestedAt, + timeoutRecoveryMessage: '切换到一次性验证码登录后进入登录超时报错页。', + timeoutRecoveryVia: 'switch_to_one_time_code_timeout_recovered', + stalledReason: 'one_time_code_switch_stalled', + stalledMessage: '点击一次性验证码登录后仍未进入登录验证码页。', + addPhoneMessage: (snapshot) => `切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`, + }); - while (Date.now() - start < timeout) { - throwIfStopped(); - snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); - - if (snapshot.state === 'verification_page') { - return createStep6SuccessResult(snapshot, { - via: 'switch_to_one_time_code_login', - loginVerificationRequestedAt, - }); - } - - if (snapshot.state === 'login_timeout_error_page') { - const transition = await createStep6LoginTimeoutRecoveryTransition( - 'login_timeout_error_page', - snapshot, - '切换到一次性验证码登录后进入登录超时报错页。', - { - loginVerificationRequestedAt, - via: 'switch_to_one_time_code_timeout_recovered', - } - ); - if (transition.action === 'done') { - return transition.result; - } - if (transition.action === 'password' || transition.action === 'email') { - return transition; - } - return transition.result; - } - - if (snapshot.state === 'oauth_consent_page') { - throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); - } - - if (snapshot.state === 'add_phone_page') { - throw new Error(`切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); - } - - await sleep(250); - } - - snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); - if (snapshot.state === 'verification_page') { - return createStep6SuccessResult(snapshot, { - via: 'switch_to_one_time_code_login', - loginVerificationRequestedAt, - }); - } - if (snapshot.state === 'login_timeout_error_page') { - const transition = await createStep6LoginTimeoutRecoveryTransition( - 'login_timeout_error_page', - snapshot, - '切换到一次性验证码登录后进入登录超时报错页。', - { - loginVerificationRequestedAt, - via: 'switch_to_one_time_code_timeout_recovered', - } - ); - if (transition.action === 'done') { - return transition.result; - } - if (transition.action === 'password' || transition.action === 'email') { - return transition; - } + if (transition.action === 'done' || transition.action === 'recoverable') { return transition.result; } - if (snapshot.state === 'oauth_consent_page') { - throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); - } - if (snapshot.state === 'add_phone_page') { - throw new Error(`切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); - } - - return createStep6RecoverableResult('one_time_code_switch_stalled', snapshot, { - message: '点击一次性验证码登录后仍未进入登录验证码页。', - }); + return transition; } async function step6SwitchToOneTimeCodeLogin(payload, snapshot) { @@ -2921,6 +2832,9 @@ async function step6SwitchToOneTimeCodeLogin(payload, snapshot) { await sleep(1200); const result = await waitForStep6SwitchTransition(loginVerificationRequestedAt); if (result?.step6Outcome === 'success') { + if (result.skipLoginVerificationStep) { + return result; + } return finalizeStep6VerificationReady({ logLabel: '步骤 7 收尾', loginVerificationRequestedAt: result.loginVerificationRequestedAt || loginVerificationRequestedAt, @@ -2964,6 +2878,9 @@ async function step6LoginFromPasswordPage(payload, snapshot) { const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt); if (transition.action === 'done') { + if (transition.result?.skipLoginVerificationStep) { + return transition.result; + } return finalizeStep6VerificationReady({ logLabel: '步骤 7 收尾', loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || passwordSubmittedAt, @@ -3020,6 +2937,9 @@ async function step6LoginFromEmailPage(payload, snapshot) { const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt); if (transition.action === 'done') { + if (transition.result?.skipLoginVerificationStep) { + return transition.result; + } return finalizeStep6VerificationReady({ logLabel: '步骤 7 收尾', loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || emailSubmittedAt, @@ -3057,6 +2977,13 @@ async function step6_login(payload) { }); } + if (snapshot.state === 'oauth_consent_page') { + log('步骤 7:认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。', 'ok'); + return createStep6OAuthConsentSuccessResult(snapshot, { + via: 'already_on_oauth_consent_page', + }); + } + if (snapshot.state === 'login_timeout_error_page') { log('步骤 7:检测到登录超时报错页,先尝试恢复当前页面。', 'warn'); const transition = await createStep6LoginTimeoutRecoveryTransition( @@ -3069,6 +2996,9 @@ async function step6_login(payload) { } ); if (transition.action === 'done') { + if (transition.result?.skipLoginVerificationStep) { + return transition.result; + } return finalizeStep6VerificationReady({ logLabel: '步骤 7 收尾', loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || null, diff --git a/content/vps-panel.js b/content/vps-panel.js index 41094f2..0a590ee 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -1012,7 +1012,7 @@ async function step1_getOAuthLink(payload, options = {}) { async function step9_vpsVerify(payload) { const visibleStep = Number(payload?.visibleStep) || 10; - const confirmStep = visibleStep === 12 ? 11 : 9; + const confirmStep = visibleStep >= 13 ? 12 : 9; await ensureOAuthManagementPage(payload?.vpsPassword, confirmStep); // 优先从 payload 读取 localhostUrl;没有时再回退到全局状态 diff --git a/data/step-definitions.js b/data/step-definitions.js index 0fec026..3852332 100644 --- a/data/step-definitions.js +++ b/data/step-definitions.js @@ -25,8 +25,9 @@ { id: 8, order: 80, key: 'paypal-approve', title: 'PayPal 登录与授权' }, { id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认' }, { id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' }, - { id: 11, order: 110, key: 'confirm-oauth', title: '自动确认 OAuth' }, - { id: 12, order: 120, key: 'platform-verify', title: '平台回调验证' }, + { id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码' }, + { id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth' }, + { id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' }, ]; function isPlusModeEnabled(options = {}) { diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 7792b30..156bd46 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -2965,6 +2965,7 @@ function getCustomMailProviderUiCopy() { function getCustomVerificationPromptCopy(step) { const verificationLabel = step === 4 ? '注册验证码' : '登录验证码'; + const isLoginVerificationStep = step === 8 || step === 11; return { title: `手动处理${verificationLabel}`, message: `当前邮箱服务为“自定义邮箱”。请先在页面中手动输入${verificationLabel},并确认已经进入下一页面后,再点击确认。`, @@ -2972,7 +2973,7 @@ function getCustomVerificationPromptCopy(step) { text: `点击确认后会跳过步骤 ${step}。`, tone: 'danger', }, - ...(step === 8 ? { + ...(isLoginVerificationStep ? { phoneActionLabel: '出现手机号验证', phoneActionAlert: { text: '如果当前页面已经进入手机号验证,可直接标记为失败并继续下一个邮箱。', @@ -2984,7 +2985,7 @@ function getCustomVerificationPromptCopy(step) { async function openCustomVerificationConfirmDialog(step) { const promptCopy = getCustomVerificationPromptCopy(step); - if (step === 8) { + if (step === 8 || step === 11) { return openActionModal({ title: promptCopy.title, message: promptCopy.message, diff --git a/tests/background-logging-status-module.test.js b/tests/background-logging-status-module.test.js index b5f94f6..5e66201 100644 --- a/tests/background-logging-status-module.test.js +++ b/tests/background-logging-status-module.test.js @@ -44,4 +44,5 @@ test('logging/status add-phone detection ignores step 2 phone-entry switch failu true ); assert.equal(loggingStatus.getLoginAuthStateLabel('phone_verification_page'), '手机验证码页'); + assert.equal(loggingStatus.getLoginAuthStateLabel('oauth_consent_page'), 'OAuth 授权页'); }); diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index a90bf63..9ca45f6 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -59,6 +59,8 @@ function createRouter(overrides = {}) { getPendingAutoRunTimerPlan: () => null, getSourceLabel: () => '', getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } }, + getStepDefinitionForState: overrides.getStepDefinitionForState, + getStepIdsForState: overrides.getStepIdsForState, getTabId: overrides.getTabId || (async () => null), getStopRequested: () => false, handleAutoRunLoopUnhandledError: async () => {}, @@ -184,6 +186,49 @@ test('message router skips step 5 when step 4 reports already logged-in transiti assert.equal(events.logs[0]?.message, '步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。'); }); +test('message router skips login-code step when oauth login lands on consent page', async () => { + const stepKeys = { + 7: 'oauth-login', + 8: 'fetch-login-code', + 9: 'confirm-oauth', + }; + const { router, events } = createRouter({ + state: { stepStatuses: { 7: 'completed', 8: 'pending', 9: 'pending' } }, + getStepDefinitionForState: (step) => ({ id: step, key: stepKeys[step] || '' }), + getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + }); + + await router.handleStepData(7, { + skipLoginVerificationStep: true, + directOAuthConsentPage: true, + }); + + assert.deepStrictEqual(events.stepStatuses, [{ step: 8, status: 'skipped' }]); + assert.equal(events.logs.some(({ message }) => /OAuth 授权页.*步骤 8/.test(message)), true); +}); + +test('message router skips Plus login-code step when oauth login lands on consent page', async () => { + const stepKeys = { + 10: 'oauth-login', + 11: 'fetch-login-code', + 12: 'confirm-oauth', + 13: 'platform-verify', + }; + const { router, events } = createRouter({ + state: { plusModeEnabled: true, stepStatuses: { 10: 'completed', 11: 'pending', 12: 'pending', 13: 'pending' } }, + getStepDefinitionForState: (step) => ({ id: step, key: stepKeys[step] || '' }), + getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], + }); + + await router.handleStepData(10, { + skipLoginVerificationStep: true, + directOAuthConsentPage: true, + }); + + assert.deepStrictEqual(events.stepStatuses, [{ step: 11, status: 'skipped' }]); + assert.equal(events.logs.some(({ message }) => /OAuth 授权页.*步骤 11/.test(message)), true); +}); + test('message router finalizes step 3 before marking it completed', async () => { const { router, events } = createRouter(); diff --git a/tests/background-step6-retry-limit.test.js b/tests/background-step6-retry-limit.test.js index 091a579..39c0ca9 100644 --- a/tests/background-step6-retry-limit.test.js +++ b/tests/background-step6-retry-limit.test.js @@ -178,6 +178,55 @@ test('step 7 starts a new oauth timeout window for each refreshed oauth url', as ]); }); +test('step 7 forwards direct OAuth consent skip metadata when completing', async () => { + const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); + + const events = { + completions: [], + }; + + const executor = api.createStep7Executor({ + addLog: async () => {}, + completeStepFromBackground: async (step, payload) => { + events.completions.push({ step, payload }); + }, + getErrorMessage: (error) => error?.message || String(error || ''), + getLoginAuthStateLabel: (state) => state || 'unknown', + getState: async () => ({ email: 'user@example.com', password: 'secret' }), + isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable', + isStep6SuccessResult: (result) => result?.step6Outcome === 'success', + refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest', + reuseOrCreateTab: async () => {}, + sendToContentScriptResilient: async () => ({ + step6Outcome: 'success', + state: 'oauth_consent_page', + skipLoginVerificationStep: true, + directOAuthConsentPage: true, + }), + STEP6_MAX_ATTEMPTS: 3, + throwIfStopped: () => {}, + }); + + await executor.executeStep7({ + email: 'user@example.com', + password: 'secret', + visibleStep: 10, + }); + + assert.deepStrictEqual(events.completions, [ + { + step: 10, + payload: { + loginVerificationRequestedAt: null, + skipLoginVerificationStep: true, + directOAuthConsentPage: true, + }, + }, + ]); +}); + test('step 7 stops immediately when management secret is missing', async () => { const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); const globalScope = {}; diff --git a/tests/background-step7-recovery.test.js b/tests/background-step7-recovery.test.js index d1a2613..6b28446 100644 --- a/tests/background-step7-recovery.test.js +++ b/tests/background-step7-recovery.test.js @@ -85,8 +85,78 @@ test('step 8 submits login verification directly without replaying step 7', asyn { step8VerificationTargetEmail: 'display.user@example.com' }, ]); assert.deepStrictEqual(calls.ensureReadyOptions, [ - { timeoutMs: 5000 }, + { visibleStep: 8, authLoginStep: 7, timeoutMs: 5000 }, ]); + assert.equal(calls.resolveOptions.completionStep, 8); +}); + +test('Plus login-code step reuses step 8 verification logic but completes visible step 11', async () => { + let resolvedStep = null; + let resolvedOptions = null; + let readyOptions = null; + const remainingStepCalls = []; + + const executor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + confirmCustomVerificationStepBypass: async () => {}, + ensureStep8VerificationPageReady: async (options) => { + readyOptions = options; + return { state: 'verification_page', displayedEmail: 'plus.user@example.com' }; + }, + rerunStep7ForStep8Recovery: async () => {}, + getOAuthFlowRemainingMs: async (details) => { + remainingStepCalls.push(details.step); + return 9000; + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs, details) => { + remainingStepCalls.push(details.step); + return Math.min(defaultTimeoutMs, 9000); + }, + getMailConfig: () => ({ + provider: 'qq', + label: 'QQ 邮箱', + source: 'mail-qq', + url: 'https://mail.qq.com', + navigateOnReuse: false, + }), + getState: async () => ({ email: 'user@example.com', password: 'secret', plusModeEnabled: true }), + getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2), + HOTMAIL_PROVIDER: 'hotmail-api', + isTabAlive: async () => true, + isVerificationMailPollingError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + resolveVerificationStep: async (step, _state, _mail, options) => { + resolvedStep = step; + resolvedOptions = options; + await options.getRemainingTimeMs({ actionLabel: '登录验证码流程' }); + }, + reuseOrCreateTab: async () => {}, + setState: async () => {}, + shouldUseCustomRegistrationEmail: () => false, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, + STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8, + throwIfStopped: () => {}, + }); + + await executor.executeStep8({ + visibleStep: 11, + plusModeEnabled: true, + email: 'user@example.com', + password: 'secret', + oauthUrl: 'https://oauth.example/latest', + }); + + assert.equal(resolvedStep, 8); + assert.equal(resolvedOptions.completionStep, 11); + assert.equal(resolvedOptions.targetEmail, 'plus.user@example.com'); + assert.deepStrictEqual(readyOptions, { visibleStep: 11, authLoginStep: 10, timeoutMs: 9000 }); + assert.deepStrictEqual(remainingStepCalls, [11, 11]); }); test('step 8 uses a fixed 10-minute lookback window and disables resend interval for 2925 mailbox polling', async () => { diff --git a/tests/sidepanel-custom-email-pool.test.js b/tests/sidepanel-custom-email-pool.test.js index a8e7b6e..75e5c89 100644 --- a/tests/sidepanel-custom-email-pool.test.js +++ b/tests/sidepanel-custom-email-pool.test.js @@ -216,3 +216,43 @@ return { assert.equal(modalPayload.actions[1].id, 'add_phone'); assert.equal(modalPayload.actions[1].label, '出现手机号验证'); }); + +test('sidepanel custom verification dialog exposes add-phone action for Plus login code step', async () => { + const bundle = [ + extractFunction('getCustomVerificationPromptCopy'), + extractFunction('openCustomVerificationConfirmDialog'), + ].join('\n'); + + const api = new Function(` +let openActionModalPayload = null; + +async function openActionModal(options) { + openActionModalPayload = options; + return options.buildResult('add_phone'); +} + +async function openConfirmModal() { + throw new Error('Plus login code step should use action modal'); +} + +${bundle} + +return { + getCustomVerificationPromptCopy, + openCustomVerificationConfirmDialog, + getOpenActionModalPayload: () => openActionModalPayload, +}; +`)(); + + const prompt = api.getCustomVerificationPromptCopy(11); + assert.equal(prompt.phoneActionLabel, '出现手机号验证'); + + const result = await api.openCustomVerificationConfirmDialog(11); + assert.deepEqual(result, { + confirmed: false, + addPhoneDetected: true, + }); + + const modalPayload = api.getOpenActionModalPayload(); + assert.equal(modalPayload.actions[1].id, 'add_phone'); +}); diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js index 9ac6f7b..ea6390b 100644 --- a/tests/step-definitions-module.test.js +++ b/tests/step-definitions-module.test.js @@ -44,14 +44,15 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'paypal-approve', 'plus-checkout-return', 'oauth-login', + 'fetch-login-code', 'confirm-oauth', 'platform-verify', ] ); assert.equal(plusSteps.some((step) => step.key === 'clear-login-cookies'), false); - assert.equal(plusSteps.some((step) => step.key === 'fetch-login-code'), false); - assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - assert.equal(api.getLastStepId({ plusModeEnabled: true }), 12); + assert.equal(plusSteps.some((step) => step.key === 'fetch-login-code'), true); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); + assert.equal(api.getLastStepId({ plusModeEnabled: true }), 13); }); test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => { diff --git a/tests/step6-login-state.test.js b/tests/step6-login-state.test.js index c274f60..f9f2f95 100644 --- a/tests/step6-login-state.test.js +++ b/tests/step6-login-state.test.js @@ -215,17 +215,20 @@ return { consentReady: true, }); + const inspected = api.inspectLoginAuthState(); + assert.strictEqual(inspected.state, 'oauth_consent_page'); + const snapshot = api.normalizeStep6Snapshot({ state: 'oauth_consent_page', url: 'https://auth.openai.com/authorize', }); - assert.strictEqual(snapshot.state, 'unknown', '第六步应忽略 oauth_consent_page 状态'); + assert.strictEqual(snapshot.state, 'oauth_consent_page', '第六步应保留 oauth_consent_page 状态'); } assert.ok( - !extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"), - 'inspectLoginAuthState 不应再产出 oauth_consent_page 状态' + extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"), + 'inspectLoginAuthState 应产出 oauth_consent_page 状态' ); console.log('step6 login state tests passed'); diff --git a/tests/step6-oauth-consent-skip.test.js b/tests/step6-oauth-consent-skip.test.js new file mode 100644 index 0000000..d8ad6bb --- /dev/null +++ b/tests/step6-oauth-consent-skip.test.js @@ -0,0 +1,134 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('content/signup-page.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 index = start; index < source.length; index += 1) { + const char = source[index]; + if (char === '(') { + parenDepth += 1; + } else if (char === ')') { + parenDepth -= 1; + if (parenDepth === 0) { + signatureEnded = true; + } + } else if (char === '{' && signatureEnded) { + braceStart = index; + 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 char = source[end]; + if (char === '{') depth += 1; + if (char === '}') { + depth -= 1; + if (depth === 0) { + end += 1; + break; + } + } + } + + return source.slice(start, end); +} + +test('password submit treats direct OAuth consent as a login-code skip', async () => { + const api = new Function(` +const location = { href: 'https://auth.openai.com/authorize' }; + +function inspectLoginAuthState() { + return { + state: 'oauth_consent_page', + url: location.href, + }; +} + +function throwIfStopped() {} +async function sleep() { + throw new Error('should not wait once oauth consent is detected'); +} + +${extractFunction('createStep6SuccessResult')} +${extractFunction('createStep6OAuthConsentSuccessResult')} +${extractFunction('createStep6RecoverableResult')} +${extractFunction('normalizeStep6Snapshot')} +${extractFunction('getStep6OptionMessage')} +${extractFunction('resolveStep6PostSubmitSnapshot')} +${extractFunction('waitForStep6PostSubmitTransition')} +${extractFunction('waitForStep6PasswordSubmitTransition')} + +return { + run() { + return waitForStep6PasswordSubmitTransition(123, 1000); + }, +}; +`)(); + + const transition = await api.run(); + + assert.equal(transition.action, 'done'); + assert.equal(transition.result.state, 'oauth_consent_page'); + assert.equal(transition.result.skipLoginVerificationStep, true); + assert.equal(transition.result.directOAuthConsentPage, true); + assert.equal(transition.result.loginVerificationRequestedAt, null); +}); + +test('step 7 entry succeeds when the auth page is already on OAuth consent', async () => { + const logs = []; + const api = new Function(` +const location = { href: 'https://auth.openai.com/authorize' }; +const logs = arguments[0]; + +function inspectLoginAuthState() { + return { + state: 'oauth_consent_page', + url: location.href, + }; +} + +function throwIfStopped() {} +async function sleep() {} +function log(message, level = 'info') { + logs.push({ message, level }); +} + +${extractFunction('createStep6SuccessResult')} +${extractFunction('createStep6OAuthConsentSuccessResult')} +${extractFunction('normalizeStep6Snapshot')} +${extractFunction('waitForKnownLoginAuthState')} +${extractFunction('step6_login')} + +return { + run() { + return step6_login({ email: 'user@example.com' }); + }, +}; +`)(logs); + + const result = await api.run(); + + assert.equal(result.step6Outcome, 'success'); + assert.equal(result.state, 'oauth_consent_page'); + assert.equal(result.skipLoginVerificationStep, true); + assert.equal(result.directOAuthConsentPage, true); + assert.equal(logs.some(({ level }) => level === 'ok'), true); +}); diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js index df4f6d8..426b828 100644 --- a/tests/verification-flow-polling.test.js +++ b/tests/verification-flow-polling.test.js @@ -833,6 +833,69 @@ test('verification flow uses configured login resend count for step 8', async () assert.equal(pollCalls, 3); }); +test('verification flow can complete Plus visible login-code step with shared step 8 semantics', async () => { + const completed = []; + const fillMessages = []; + + const helpers = api.createVerificationFlowHelpers({ + addLog: async () => {}, + chrome: { tabs: { update: async () => {} } }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + completeStepFromBackground: async (step, payload) => { + completed.push({ step, payload }); + }, + confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }), + getHotmailVerificationPollConfig: () => ({}), + getHotmailVerificationRequestTimestamp: () => 0, + getState: async () => ({}), + getTabId: async () => 1, + HOTMAIL_PROVIDER: 'hotmail-api', + isStopError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + MAIL_2925_VERIFICATION_INTERVAL_MS: 15000, + MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15, + pollCloudflareTempEmailVerificationCode: async () => ({}), + pollHotmailVerificationCode: async () => ({}), + pollLuckmailVerificationCode: async () => ({}), + sendToContentScript: async (_source, message) => { + if (message.type === 'FILL_CODE') { + fillMessages.push(message); + } + return {}; + }, + sendToMailContentScriptResilient: async () => ({ code: '654321', emailTimestamp: 456 }), + setState: async () => {}, + setStepStatus: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + VERIFICATION_POLL_MAX_ROUNDS: 5, + }); + + await helpers.resolveVerificationStep( + 8, + { email: 'user@example.com', lastLoginCode: null }, + { provider: 'qq', label: 'QQ 邮箱' }, + { + completionStep: 11, + requestFreshCodeFirst: false, + maxResendRequests: 0, + resendIntervalMs: 0, + } + ); + + assert.deepStrictEqual(fillMessages.map((message) => message.step), [8]); + assert.deepStrictEqual(completed, [ + { + step: 11, + payload: { + emailTimestamp: 456, + code: '654321', + phoneVerificationRequired: false, + }, + }, + ]); +}); + test('verification flow waits during resend cooldown instead of tight-looping', async () => { const sleepCalls = []; let pollCalls = 0;