diff --git a/content/signup-page.js b/content/signup-page.js index 53881b7..0b74b3e 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -1621,8 +1621,13 @@ function createStep6RecoverableResult(reason, snapshot, options = {}) { }; } -async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message) { - const resolvedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); +async function createStep6LoginTimeoutRecoveryTransition(reason, snapshot, message, options = {}) { + const { + loginVerificationRequestedAt = null, + via = 'login_timeout_recovered', + } = options; + let resolvedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); + let recovered = false; if (resolvedSnapshot?.state === 'login_timeout_error_page') { try { const recoveryResult = await recoverCurrentAuthRetryPage({ @@ -1631,8 +1636,9 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag step: 7, timeoutMs: 12000, }); - if (recoveryResult?.recovered) { - log('步骤 7:登录超时报错页已点击“重试”,准备重新执行当前步骤。', 'warn'); + recovered = Boolean(recoveryResult?.recovered); + if (recovered) { + log('步骤 7:登录超时报错页已点击“重试”,正在按恢复后的页面状态继续当前流程。', 'warn'); } } catch (error) { if (/CF_SECURITY_BLOCKED::/i.test(String(error?.message || error || ''))) { @@ -1642,7 +1648,46 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag } } - return createStep6RecoverableResult(reason, resolvedSnapshot, { + resolvedSnapshot = recovered + ? normalizeStep6Snapshot(await waitForKnownLoginAuthState(4000)) + : normalizeStep6Snapshot(inspectLoginAuthState()); + + if (resolvedSnapshot.state === 'verification_page') { + return { + action: 'done', + result: createStep6SuccessResult(resolvedSnapshot, { + via, + loginVerificationRequestedAt, + }), + }; + } + + if (resolvedSnapshot.state === 'password_page') { + log('步骤 7:登录超时报错页恢复后已进入密码页,继续当前登录流程。', 'warn'); + return { action: 'password', snapshot: resolvedSnapshot }; + } + + if (resolvedSnapshot.state === 'email_page') { + log('步骤 7:登录超时报错页恢复后已回到邮箱输入页,继续当前登录流程。', 'warn'); + return { action: 'email', snapshot: resolvedSnapshot }; + } + + return { + action: 'recoverable', + result: createStep6RecoverableResult(reason, resolvedSnapshot, { + message, + loginVerificationRequestedAt, + }), + }; +} + +async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message) { + const transition = await createStep6LoginTimeoutRecoveryTransition(reason, snapshot, message); + if (transition?.action === 'done' || transition?.action === 'recoverable') { + return transition.result; + } + + return createStep6RecoverableResult(reason, transition?.snapshot || normalizeStep6Snapshot(inspectLoginAuthState()), { message, }); } @@ -2222,13 +2267,30 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120 } 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: await createStep6LoginTimeoutRecoverableResult( - 'login_timeout_error_page', - snapshot, - '提交邮箱后进入登录超时报错页。' - ), + result: transition.result, }; } @@ -2257,13 +2319,30 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120 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: await createStep6LoginTimeoutRecoverableResult( - 'login_timeout_error_page', - snapshot, - '提交邮箱后进入登录超时报错页。' - ), + result: transition.result, }; } if (snapshot.state === 'oauth_consent_page') { @@ -2300,13 +2379,30 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout } 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: await createStep6LoginTimeoutRecoverableResult( - 'login_timeout_error_page', - snapshot, - '提交密码后进入登录超时报错页。' - ), + result: transition.result, }; } @@ -2332,13 +2428,30 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout }; } 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: await createStep6LoginTimeoutRecoverableResult( - 'login_timeout_error_page', - snapshot, - '提交密码后进入登录超时报错页。' - ), + result: transition.result, }; } if (snapshot.state === 'oauth_consent_page') { @@ -2375,11 +2488,22 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou } if (snapshot.state === 'login_timeout_error_page') { - return await createStep6LoginTimeoutRecoverableResult( + 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') { @@ -2401,11 +2525,22 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou }); } if (snapshot.state === 'login_timeout_error_page') { - return await createStep6LoginTimeoutRecoverableResult( + 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}`); @@ -2419,7 +2554,7 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou }); } -async function step6SwitchToOneTimeCodeLogin(snapshot) { +async function step6SwitchToOneTimeCodeLogin(payload, snapshot) { const switchTrigger = snapshot?.switchTrigger || findOneTimeCodeLoginTrigger(); if (!switchTrigger || !isActionEnabled(switchTrigger)) { return createStep6RecoverableResult('missing_one_time_code_trigger', normalizeStep6Snapshot(inspectLoginAuthState()), { @@ -2441,6 +2576,12 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) { via: result.via || 'switch_to_one_time_code_login', }); } + if (result?.action === 'password') { + return step6LoginFromPasswordPage(payload, result.snapshot); + } + if (result?.action === 'email') { + return step6LoginFromEmailPage(payload, result.snapshot); + } return result; } @@ -2452,7 +2593,7 @@ async function step6LoginFromPasswordPage(payload, snapshot) { if (!hasPassword) { if (currentSnapshot.switchTrigger) { log('步骤 7:当前未提供密码,改走一次性验证码登录。', 'warn'); - return step6SwitchToOneTimeCodeLogin(currentSnapshot); + return step6SwitchToOneTimeCodeLogin(payload, currentSnapshot); } return createStep6RecoverableResult('missing_password_and_one_time_code_trigger', currentSnapshot, { @@ -2482,8 +2623,14 @@ async function step6LoginFromPasswordPage(payload, snapshot) { log(`步骤 7:${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 7。'}`, 'warn'); return transition.result; } + if (transition.action === 'password') { + return step6LoginFromPasswordPage(payload, transition.snapshot); + } + if (transition.action === 'email') { + return step6LoginFromEmailPage(payload, transition.snapshot); + } if (transition.action === 'switch') { - return step6SwitchToOneTimeCodeLogin(transition.snapshot); + return step6SwitchToOneTimeCodeLogin(payload, transition.snapshot); } return createStep6RecoverableResult('password_submit_unknown', normalizeStep6Snapshot(inspectLoginAuthState()), { @@ -2492,7 +2639,7 @@ async function step6LoginFromPasswordPage(payload, snapshot) { } if (currentSnapshot.switchTrigger) { - return step6SwitchToOneTimeCodeLogin(currentSnapshot); + return step6SwitchToOneTimeCodeLogin(payload, currentSnapshot); } return createStep6RecoverableResult('password_page_unactionable', currentSnapshot, { @@ -2532,6 +2679,9 @@ async function step6LoginFromEmailPage(payload, snapshot) { log(`步骤 7:${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 7。'}`, 'warn'); return transition.result; } + if (transition.action === 'email') { + return step6LoginFromEmailPage(payload, transition.snapshot); + } if (transition.action === 'password') { return step6LoginFromPasswordPage(payload, transition.snapshot); } @@ -2545,11 +2695,10 @@ async function step6_login(payload) { const { email } = payload; if (!email) throw new Error('登录时缺少邮箱地址。'); - log(`步骤 7:正在使用 ${email} 登录...`); - const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000)); if (snapshot.state === 'verification_page') { + log('步骤 7:认证页已在登录验证码页,开始确认页面是否稳定。'); return finalizeStep6VerificationReady({ logLabel: '步骤 7 收尾', loginVerificationRequestedAt: null, @@ -2558,19 +2707,39 @@ async function step6_login(payload) { } if (snapshot.state === 'login_timeout_error_page') { - log('步骤 7:检测到登录超时报错,准备重新执行步骤 7。', 'warn'); - return await createStep6LoginTimeoutRecoverableResult( + log('步骤 7:检测到登录超时报错页,先尝试恢复当前页面。', 'warn'); + const transition = await createStep6LoginTimeoutRecoveryTransition( 'login_timeout_error_page', snapshot, - '当前页面处于登录超时报错页。' + '当前页面处于登录超时报错页。', + { + loginVerificationRequestedAt: null, + via: 'login_timeout_initial_recovered', + } ); + if (transition.action === 'done') { + return finalizeStep6VerificationReady({ + logLabel: '步骤 7 收尾', + loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || null, + via: transition.result.via || 'login_timeout_initial_recovered', + }); + } + if (transition.action === 'email') { + return step6LoginFromEmailPage(payload, transition.snapshot); + } + if (transition.action === 'password') { + return step6LoginFromPasswordPage(payload, transition.snapshot); + } + return transition.result; } if (snapshot.state === 'email_page') { + log(`步骤 7:正在使用 ${email} 登录...`); return step6LoginFromEmailPage(payload, snapshot); } if (snapshot.state === 'password_page') { + log('步骤 7:认证页已在密码页,继续当前登录流程。'); return step6LoginFromPasswordPage(payload, snapshot); } diff --git a/tests/step6-passwordless-otp-login.test.js b/tests/step6-passwordless-otp-login.test.js index ee19863..f576863 100644 --- a/tests/step6-passwordless-otp-login.test.js +++ b/tests/step6-passwordless-otp-login.test.js @@ -87,7 +87,8 @@ test('step6LoginFromPasswordPage switches to one-time-code login when password i globalThis.log = (message, level = 'info') => { logs.push({ message, level }); }; - globalThis.step6SwitchToOneTimeCodeLogin = async (value) => { + globalThis.step6SwitchToOneTimeCodeLogin = async (payload, value) => { + assert.deepStrictEqual(payload, { email: 'user@example.com', password: '' }); assert.strictEqual(value, snapshot); return { step6Outcome: 'success', via: 'switch_to_one_time_code_login' }; }; diff --git a/tests/step6-timeout-recovery.test.js b/tests/step6-timeout-recovery.test.js index ca741eb..f6e5d01 100644 --- a/tests/step6-timeout-recovery.test.js +++ b/tests/step6-timeout-recovery.test.js @@ -72,12 +72,18 @@ async function recoverCurrentAuthRetryPage() { return { recovered: true }; } +function throwIfStopped() {} +async function sleep() {} + function log(message, level = 'info') { logs.push({ message, level }); } +${extractFunction('createStep6SuccessResult')} ${extractFunction('createStep6RecoverableResult')} ${extractFunction('normalizeStep6Snapshot')} +${extractFunction('waitForKnownLoginAuthState')} +${extractFunction('createStep6LoginTimeoutRecoveryTransition')} ${extractFunction('createStep6LoginTimeoutRecoverableResult')} return { @@ -104,6 +110,141 @@ return { assert.equal(result.message, '当前页面处于登录超时报错页。'); }); +test('step 7 timeout recovery transition continues from password page after retry succeeds', async () => { + const api = new Function(` +const logs = []; +let recoverCalls = 0; +let currentState = 'login_timeout_error_page'; + +const location = { + href: 'https://auth.openai.com/log-in', +}; + +function inspectLoginAuthState() { + return { + state: currentState, + url: location.href, + }; +} + +async function recoverCurrentAuthRetryPage() { + recoverCalls += 1; + currentState = 'password_page'; + return { recovered: true }; +} + +function throwIfStopped() {} +async function sleep() {} + +function log(message, level = 'info') { + logs.push({ message, level }); +} + +${extractFunction('createStep6SuccessResult')} +${extractFunction('createStep6RecoverableResult')} +${extractFunction('normalizeStep6Snapshot')} +${extractFunction('waitForKnownLoginAuthState')} +${extractFunction('createStep6LoginTimeoutRecoveryTransition')} + +return { + async run() { + return createStep6LoginTimeoutRecoveryTransition( + 'login_timeout_error_page', + { state: 'login_timeout_error_page', url: location.href }, + '当前页面处于登录超时报错页。', + { + via: 'login_timeout_initial_recovered', + } + ); + }, + snapshot() { + return { logs, recoverCalls }; + }, +}; +`)(); + + const result = await api.run(); + const snapshot = api.snapshot(); + + assert.equal(snapshot.recoverCalls, 1); + assert.equal(result.action, 'password'); + assert.equal(result.snapshot.state, 'password_page'); + assert.equal(snapshot.logs.some(({ message }) => /密码页/.test(message)), true); +}); + +test('step 7 entry resumes password flow after retry page recovery reaches password page', async () => { + const api = new Function(` +const logs = []; +let recoverCalls = 0; +let currentState = 'login_timeout_error_page'; + +const location = { + href: 'https://auth.openai.com/log-in', +}; + +function inspectLoginAuthState() { + return { + state: currentState, + url: location.href, + }; +} + +async function recoverCurrentAuthRetryPage() { + recoverCalls += 1; + currentState = 'password_page'; + return { recovered: true }; +} + +function throwIfStopped() {} +async function sleep() {} + +function log(message, level = 'info') { + logs.push({ message, level }); +} + +async function step6LoginFromPasswordPage(payload, snapshot) { + return { branch: 'password', payload, snapshot }; +} + +async function step6LoginFromEmailPage(payload, snapshot) { + return { branch: 'email', payload, snapshot }; +} + +async function finalizeStep6VerificationReady(options) { + return { branch: 'verification', options }; +} + +function throwForStep6FatalState() {} + +${extractFunction('createStep6SuccessResult')} +${extractFunction('createStep6RecoverableResult')} +${extractFunction('normalizeStep6Snapshot')} +${extractFunction('waitForKnownLoginAuthState')} +${extractFunction('createStep6LoginTimeoutRecoveryTransition')} +${extractFunction('step6_login')} + +return { + async run() { + return step6_login({ + email: 'user@example.com', + password: 'secret', + }); + }, + snapshot() { + return { logs, recoverCalls }; + }, +}; +`)(); + + const result = await api.run(); + const snapshot = api.snapshot(); + + assert.equal(snapshot.recoverCalls, 1); + assert.equal(result.branch, 'password'); + assert.equal(result.snapshot.state, 'password_page'); + assert.equal(snapshot.logs.some(({ message }) => /密码页/.test(message)), true); +}); + test('step 7 finalize converts verification page that falls into retry page into recoverable result', async () => { const api = new Function(` const logs = []; @@ -150,6 +291,8 @@ function getLoginAuthStateLabel(snapshot) { ${extractFunction('createStep6SuccessResult')} ${extractFunction('createStep6RecoverableResult')} ${extractFunction('normalizeStep6Snapshot')} +${extractFunction('waitForKnownLoginAuthState')} +${extractFunction('createStep6LoginTimeoutRecoveryTransition')} ${extractFunction('createStep6LoginTimeoutRecoverableResult')} ${extractFunction('finalizeStep6VerificationReady')} diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 889d113..9ab4f83 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -704,7 +704,7 @@ - 新增共享恢复层:`content/auth-page-recovery.js`。 - Step 4 在等待注册验证码页时,如果命中认证页 `Try again / 重试` 页,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续回到密码页重提和验证码页确认流程。 - 但如果 Step 4 的认证重试页正文中出现 `user_already_exists`,则会直接视为“当前用户已存在”:不点击 `重试`,不再回到步骤 1 重开当前轮,而是立即结束当前轮;开启自动重试时直接进入下一轮。 -- Step 7 在识别到登录超时报错页时,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复当前页面;若仍未恢复,则按原有可恢复失败逻辑重跑 Step 7。 +- Step 7 在识别到登录超时报错页时,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复当前页面;恢复成功后会优先按当前页面状态继续当前登录流程,例如直接续跑邮箱页或密码页;只有仍未恢复到可继续状态时,才按原有可恢复失败逻辑重跑 Step 7。 - Step 7 在首次识别到登录验证码页后,不会立刻把步骤判定为成功;还会额外做一轮“收尾确认”,确保页面稳定停留在登录验证码页。如果只是短暂进入验证码页、随后又掉进登录重试页,则会先走共享恢复逻辑,再按既有可恢复失败逻辑重跑 Step 7。 - Step 7 的这轮收尾确认是主要责任边界;Step 8 默认建立在“登录验证码页已经由 Step 7 稳定确认”的前提上,只在后台入口保留防御性回退判断,不替代 Step 7 收尾。 - Step 8 如果发现认证页已经进入登录超时报错/重试页,会直接报错并回到 Step 7 重新开始,而不是在 Step 8 内部点击 `重试`。