diff --git a/background/message-router.js b/background/message-router.js index 1fadb689..25e47e1c 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -523,6 +523,129 @@ || status === 'skipped'; } + function isPhoneSignupStepPayload(payload = {}, state = {}) { + const payloadIdentifierType = String(payload?.accountIdentifierType || '').trim().toLowerCase(); + if (payloadIdentifierType === 'email') { + return false; + } + + const stateIdentifierType = String(state?.accountIdentifierType || '').trim().toLowerCase(); + return payloadIdentifierType === 'phone' + || Boolean(resolveSignupPhonePayload(payload)) + || stateIdentifierType === 'phone' + || Boolean(String(state?.signupPhoneNumber || '').trim()) + || Boolean(state?.signupPhoneActivation) + || Boolean(state?.signupPhoneCompletedActivation); + } + + function isLoginPasswordPagePayload(payload = {}) { + const passwordPageMode = String(payload?.passwordPageMode || '').trim().toLowerCase(); + if (passwordPageMode === 'login') { + return true; + } + const rawPath = String(payload?.passwordPagePath || '').trim(); + if (/\/log-in\/password(?:[/?#]|$)/i.test(rawPath)) { + return true; + } + const rawUrl = String(payload?.passwordPageUrl || '').trim(); + if (!rawUrl) { + return false; + } + try { + return /\/log-in\/password(?:[/?#]|$)/i.test(new URL(rawUrl).pathname || ''); + } catch { + return /\/log-in\/password(?:[/?#]|$)/i.test(rawUrl); + } + } + + function isSignupPasswordPagePayload(payload = {}) { + const passwordPageMode = String(payload?.passwordPageMode || '').trim().toLowerCase(); + if (passwordPageMode === 'signup') { + return true; + } + const rawPath = String(payload?.passwordPagePath || '').trim(); + if (/\/(?:create-account|u\/signup|signup)\/password(?:[/?#]|$)/i.test(rawPath)) { + return true; + } + const rawUrl = String(payload?.passwordPageUrl || '').trim(); + if (!rawUrl) { + return false; + } + try { + return /\/(?:create-account|u\/signup|signup)\/password(?:[/?#]|$)/i.test(new URL(rawUrl).pathname || ''); + } catch { + return /\/(?:create-account|u\/signup|signup)\/password(?:[/?#]|$)/i.test(rawUrl); + } + } + + function isSuccessfulLoginPasswordFlowPayload(payload = {}) { + if (!isLoginPasswordPagePayload(payload)) { + return false; + } + const state = String(payload?.state || payload?.successState || '').trim().toLowerCase(); + return Boolean(payload?.ready) + || Boolean(payload?.alreadyVerified) + || Boolean(payload?.passwordLoginFlow) + || state === 'verification' + || state === 'verification_page' + || state === 'phone_verification_page' + || state === 'oauth_consent_page' + || state === 'logged_in_home'; + } + + function shouldSkipPhoneSignupRegistrationTailAfterPassword(payload = {}, state = {}) { + if (!isPhoneSignupStepPayload(payload, state)) { + return false; + } + if (isSignupPasswordPagePayload(payload)) { + return false; + } + return isSuccessfulLoginPasswordFlowPayload(payload); + } + + function shouldSkipPhoneSignupRegistrationTailBeforePasswordFinalize(payload = {}, state = {}) { + if (!isPhoneSignupStepPayload(payload, state)) { + return false; + } + if (!Boolean(payload?.deferredSubmit)) { + return false; + } + if (isSignupPasswordPagePayload(payload)) { + return false; + } + return isLoginPasswordPagePayload(payload); + } + + async function skipPhoneSignupRegistrationTailAfterPassword(currentStep, payload = {}) { + const latestState = await getState(); + const skippedSteps = []; + for (const stepKey of ['fetch-signup-code', 'fill-profile', 'wait-registration-success']) { + const skippedStep = findStepByKeyAfter(currentStep, stepKey, latestState); + if (!skippedStep) { + continue; + } + const status = getNodeStatusByStep(skippedStep, latestState); + if (isStepProtectedFromAutoSkip(status)) { + continue; + } + await setNodeStatusByStep(skippedStep, 'skipped', latestState); + skippedSteps.push(skippedStep); + } + + await setState({ signupVerificationRequestedAt: null }); + if (skippedSteps.length) { + await addLog( + `步骤 ${currentStep}:手机号密码提交后已确认账号进入登录后续状态,已自动跳过步骤 ${skippedSteps.join('/')},流程将直接进入 OAuth 后续节点。`, + 'warn', + { step: currentStep, stepKey: 'fill-password' } + ); + } + return { + ...payload, + skipRegistrationFlow: true, + }; + } + function findStepByKeyAfter(currentOrder, targetKey, state = {}) { const activeStepIds = typeof getStepIdsForState === 'function' ? getStepIdsForState(state) @@ -847,15 +970,20 @@ break; case 3: await syncStepAccountIdentityFromPayload(payload); - if (payload.signupVerificationRequestedAt) { - await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt }); - } - if (payload.skipProfileStep) { + { const latestState = await getState(); - const step5Status = getNodeStatusByStep(5, latestState); - if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') { - await setNodeStatusByStep(5, 'skipped', latestState); - await addLog('步骤 3:页面已直接进入已登录态,已自动跳过步骤 5。', 'warn'); + const skipRegistrationTail = shouldSkipPhoneSignupRegistrationTailAfterPassword(payload, latestState); + if (payload.signupVerificationRequestedAt && !skipRegistrationTail) { + await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt }); + } + if (skipRegistrationTail) { + await skipPhoneSignupRegistrationTailAfterPassword(step, payload); + } else if (payload.skipProfileStep) { + const step5Status = getNodeStatusByStep(5, latestState); + if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') { + await setNodeStatusByStep(5, 'skipped', latestState); + await addLog('步骤 3:页面已直接进入已登录态,已自动跳过步骤 5。', 'warn'); + } } } if (payload.loginVerificationRequestedAt) { @@ -967,9 +1095,26 @@ notifyNodeError(nodeId, '流程已被用户停止。'); return { ok: true }; } + let completionPayload = message.payload || {}; try { - if (nodeId === 'fill-password' && typeof finalizeStep3Completion === 'function') { - await finalizeStep3Completion(message.payload || {}); + const skipStep3FinalizeForPhoneLoginPassword = nodeId === 'fill-password' + && shouldSkipPhoneSignupRegistrationTailBeforePasswordFinalize(completionPayload, await getState()); + if (skipStep3FinalizeForPhoneLoginPassword) { + completionPayload = { + ...completionPayload, + signupVerificationRequestedAt: null, + passwordLoginFlow: true, + skipRegistrationFlow: true, + state: 'login_password_page', + }; + } else if (nodeId === 'fill-password' && typeof finalizeStep3Completion === 'function') { + const finalizedPayload = await finalizeStep3Completion(message.payload || {}); + if (finalizedPayload && typeof finalizedPayload === 'object') { + completionPayload = { + ...completionPayload, + ...finalizedPayload, + }; + } } } catch (error) { if (typeof isCloudflareSecurityBlockedError === 'function' && isCloudflareSecurityBlockedError(error)) { @@ -1004,11 +1149,11 @@ stepKey: nodeId, }); } - await handleStepData(resolvedStep, message.payload); + await handleStepData(resolvedStep, completionPayload); if (isFinalNode && typeof appendAccountRunRecord === 'function') { await appendAccountRunRecord('success', completionState); } - notifyNodeComplete(nodeId, message.payload); + notifyNodeComplete(nodeId, completionPayload); return { ok: true }; } diff --git a/flows/openai/content/openai-auth.js b/flows/openai/content/openai-auth.js index cde7b69b..fea4ee9a 100644 --- a/flows/openai/content/openai-auth.js +++ b/flows/openai/content/openai-auth.js @@ -2624,6 +2624,19 @@ async function step2_clickRegister(payload = {}) { // ============================================================ async function step3_fillEmailPassword(payload) { + const resolvePasswordPageInfo = (rawUrl = '') => { + const url = String(rawUrl || '').trim(); + let path = ''; + try { + path = new URL(url || location.href, 'https://auth.openai.com').pathname || ''; + } catch { + path = String(location.pathname || ''); + } + const mode = /\/log-in\/password(?:[/?#]|$)/i.test(path) + ? 'login' + : (/\/(?:create-account|u\/signup|signup)\/password(?:[/?#]|$)/i.test(path) ? 'signup' : ''); + return { url: url || location.href, path, mode }; + }; const performOperationWithDelay = typeof getOperationDelayRunner === 'function' ? getOperationDelayRunner() : async (metadata, operation) => { @@ -2693,6 +2706,7 @@ async function step3_fillEmailPassword(payload) { throw new Error(`当前密码页邮箱为 ${snapshot.displayedEmail},与目标邮箱 ${email} 不一致,请先回到步骤 1 重新开始。`); } + const passwordPageInfo = resolvePasswordPageInfo(snapshot.url || location.href); await humanPause(600, 1500); await performOperationWithDelay({ stepKey: 'fill-password', kind: 'fill', label: 'signup-password' }, async () => { fillInput(snapshot.passwordInput, password); @@ -2709,7 +2723,8 @@ async function step3_fillEmailPassword(payload) { logSignupPasswordDiagnostics('步骤 3:当前密码页同时存在一次性验证码入口', 'info'); } - const signupVerificationRequestedAt = submitBtn ? Date.now() : null; + const isLoginPasswordPage = passwordPageInfo.mode === 'login'; + const signupVerificationRequestedAt = submitBtn && !isLoginPasswordPage ? Date.now() : null; const completionPayload = { email, phoneNumber: String(payload?.phoneNumber || '').trim(), @@ -2717,6 +2732,10 @@ async function step3_fillEmailPassword(payload) { accountIdentifier, signupVerificationRequestedAt, deferredSubmit: Boolean(submitBtn), + passwordPageUrl: passwordPageInfo.url, + passwordPagePath: passwordPageInfo.path, + passwordPageMode: passwordPageInfo.mode, + ...(isLoginPasswordPage ? { passwordLoginFlow: true } : {}), }; reportComplete(3, completionPayload); @@ -4762,6 +4781,13 @@ function inspectSignupVerificationState() { }; } + if (typeof isOAuthConsentPage === 'function' && isOAuthConsentPage()) { + return { + state: 'oauth_consent_page', + url: location.href, + }; + } + if (typeof isPhoneVerificationPageReady === 'function' && isPhoneVerificationPageReady()) { return { state: 'verification', @@ -4804,6 +4830,7 @@ async function waitForSignupVerificationTransition(timeout = 5000) { if ( snapshot.state === 'step5' || snapshot.state === 'logged_in_home' + || snapshot.state === 'oauth_consent_page' || snapshot.state === 'verification' || snapshot.state === 'contact_verification_server_error' || snapshot.state === 'error' @@ -4901,6 +4928,19 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { }; } + if (snapshot.state === 'oauth_consent_page') { + log(`${prepareLogLabel}:页面已直接进入 OAuth 授权页,本步骤按已完成处理。`, 'ok'); + return { + ready: true, + alreadyVerified: true, + state: 'oauth_consent_page', + skipLoginVerificationStep: true, + directOAuthConsentPage: true, + retried: recoveryRound, + prepareSource, + }; + } + if (snapshot.state === 'verification') { await waitForDocumentLoadComplete(15000, `${prepareLogLabel}:注册验证码页面`); await waitForVerificationCodeTarget(15000); diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index c969047d..8f8df169 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -574,6 +574,213 @@ test('message router marks step 3 failed when post-submit finalize fails', async assert.deepStrictEqual(response, { ok: true, error: '步骤 3 提交后仍停留在密码页。' }); }); +test('message router skips signup tail before finalizing when phone password submit used login password page', async () => { + const { router, events } = createRouter({ + state: { + signupMethod: 'phone', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + stepStatuses: { + 3: 'running', + 4: 'pending', + 5: 'pending', + 6: 'pending', + 7: 'pending', + }, + }, + getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + finalizeStep3Completion: async (payload) => { + events.finalizePayloads.push(payload); + throw new Error('should not finalize login password page as signup verification'); + }, + }); + + const response = await router.handleMessage({ + type: 'NODE_COMPLETE', + nodeId: 'fill-password', + source: 'openai-auth', + payload: { + nodeId: 'fill-password', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + signupVerificationRequestedAt: 123456, + deferredSubmit: true, + passwordPageUrl: 'https://auth.openai.com/log-in/password', + passwordPagePath: '/log-in/password', + passwordPageMode: 'login', + }, + }, {}); + + assert.deepStrictEqual(response, { ok: true }); + assert.deepStrictEqual(events.finalizePayloads, []); + assert.deepStrictEqual(events.stepStatuses, [ + { step: 3, status: 'completed' }, + { step: 4, status: 'skipped' }, + { step: 5, status: 'skipped' }, + { step: 6, status: 'skipped' }, + ]); + assert.deepStrictEqual(events.signupPhoneSilentStates, ['+66959916439']); + assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456), false); + assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === null), true); + assert.equal(events.logs.some(({ message }) => /手机号密码提交后已确认账号进入登录后续状态/.test(message)), true); + assert.deepStrictEqual(events.notifyCompletions, [ + { + step: 3, + nodeId: 'fill-password', + payload: { + nodeId: 'fill-password', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + signupVerificationRequestedAt: null, + deferredSubmit: true, + passwordPageUrl: 'https://auth.openai.com/log-in/password', + passwordPagePath: '/log-in/password', + passwordPageMode: 'login', + step: 3, + passwordLoginFlow: true, + skipRegistrationFlow: true, + state: 'login_password_page', + }, + }, + ]); +}); + +test('message router keeps signup tail when phone password submit used create-account page', async () => { + const finalizeResult = { + ready: true, + state: 'verification', + }; + const { router, events } = createRouter({ + state: { + signupMethod: 'phone', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + stepStatuses: { + 3: 'running', + 4: 'pending', + 5: 'pending', + 6: 'pending', + 7: 'pending', + }, + }, + getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + finalizeStep3Completion: async (payload) => { + events.finalizePayloads.push(payload); + return finalizeResult; + }, + }); + + const response = await router.handleMessage({ + type: 'NODE_COMPLETE', + nodeId: 'fill-password', + source: 'openai-auth', + payload: { + nodeId: 'fill-password', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + signupVerificationRequestedAt: 123456, + passwordPageUrl: 'https://auth.openai.com/create-account/password', + passwordPagePath: '/create-account/password', + passwordPageMode: 'signup', + }, + }, {}); + + assert.deepStrictEqual(response, { ok: true }); + assert.deepStrictEqual(events.stepStatuses, [ + { step: 3, status: 'completed' }, + ]); + assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456), true); + assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === null), false); + assert.equal(events.logs.some(({ message }) => /手机号密码提交后已确认账号进入登录后续状态/.test(message)), false); + assert.deepStrictEqual(events.notifyCompletions, [ + { + step: 3, + nodeId: 'fill-password', + payload: { + nodeId: 'fill-password', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + signupVerificationRequestedAt: 123456, + passwordPageUrl: 'https://auth.openai.com/create-account/password', + passwordPagePath: '/create-account/password', + passwordPageMode: 'signup', + step: 3, + ...finalizeResult, + }, + }, + ]); +}); + +test('message router keeps signup tail when phone password submit lacks login password page url', async () => { + const finalizeResult = { + ready: true, + state: 'oauth_consent_page', + directOAuthConsentPage: true, + }; + const { router, events } = createRouter({ + state: { + signupMethod: 'phone', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + stepStatuses: { + 3: 'running', + 4: 'pending', + 5: 'pending', + 6: 'pending', + 7: 'pending', + }, + }, + getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + finalizeStep3Completion: async (payload) => { + events.finalizePayloads.push(payload); + return finalizeResult; + }, + }); + + const response = await router.handleMessage({ + type: 'NODE_COMPLETE', + nodeId: 'fill-password', + source: 'openai-auth', + payload: { + nodeId: 'fill-password', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + signupVerificationRequestedAt: 123456, + }, + }, {}); + + assert.deepStrictEqual(response, { ok: true }); + assert.deepStrictEqual(events.stepStatuses, [ + { step: 3, status: 'completed' }, + ]); + assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456), true); + assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === null), false); + assert.equal(events.logs.some(({ message }) => /手机号密码提交后已确认账号进入登录后续状态/.test(message)), false); + assert.deepStrictEqual(events.notifyCompletions, [ + { + step: 3, + nodeId: 'fill-password', + payload: { + nodeId: 'fill-password', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + signupVerificationRequestedAt: 123456, + step: 3, + ...finalizeResult, + }, + }, + ]); +}); + test('message router does not duplicate step 3 mismatch failure log after finalize already failed', async () => { const mismatchError = 'SIGNUP_PHONE_PASSWORD_MISMATCH::步骤 3:检测到注册手机号或密码不正确,需要重新开始当前轮。页面提示:Incorrect phone number or password'; const state = { diff --git a/tests/step3-direct-complete.test.js b/tests/step3-direct-complete.test.js index aa2290e9..34165029 100644 --- a/tests/step3-direct-complete.test.js +++ b/tests/step3-direct-complete.test.js @@ -211,6 +211,9 @@ return { assert.deepStrictEqual(result, beforeSubmit.completions[0].payload); assert.equal(result.email, 'user@example.com'); assert.equal(result.deferredSubmit, true); + assert.equal(result.passwordPageUrl, 'https://auth.openai.com/create-account/password'); + assert.equal(result.passwordPagePath, '/create-account/password'); + assert.equal(result.passwordPageMode, 'signup'); assert.equal(typeof result.signupVerificationRequestedAt, 'number'); assert.equal(beforeSubmit.events.includes('report:true'), true); assert.equal(beforeSubmit.events.includes('operation:submit-signup-password:start'), false); @@ -233,3 +236,71 @@ return { assert.deepStrictEqual(afterSubmit.clicks, ['Continue']); assert.equal(afterSubmit.events.includes('delay:submit-signup-password:2000'), true); }); + +test('step 3 marks login password page from log-in URL', async () => { + const api = new Function(` +const completions = []; +const scheduled = []; +const snapshot = { + state: 'password_page', + passwordInput: { value: '', hidden: false }, + submitButton: { textContent: 'Continue', hidden: false }, + displayedEmail: '', + url: 'https://auth.openai.com/log-in/password', +}; +const window = { + setTimeout(fn, ms) { + scheduled.push({ fn, ms }); + return scheduled.length; + }, + CodexOperationDelay: { + async performOperationWithDelay(metadata, operation) { + return operation(); + }, + }, +}; +const location = { + href: 'https://auth.openai.com/log-in/password', + pathname: '/log-in/password', +}; +function inspectSignupEntryState() { return snapshot; } +function ensureSignupPasswordPageReady() { return { ready: true }; } +function getSignupPasswordSubmitButton() { return snapshot.submitButton; } +async function waitForElementByText() { return null; } +function fillInput(input, value) { input.value = value; } +async function humanPause() {} +async function sleep() {} +function throwIfStopped() {} +function isStopError() { return false; } +function log() {} +function logSignupPasswordDiagnostics() {} +function reportComplete(step, payload) { completions.push({ step, payload }); } +function simulateClick() {} +function getOperationDelayRunner() { return window.CodexOperationDelay.performOperationWithDelay; } + +${extractFunction('step3_fillEmailPassword')} + +return { + run() { + return step3_fillEmailPassword({ + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + phoneNumber: '+66959916439', + password: 'Secret123!', + }); + }, + completions, +}; +`)(); + + const result = await api.run(); + + assert.equal(result.accountIdentifierType, 'phone'); + assert.equal(result.accountIdentifier, '+66959916439'); + assert.equal(result.passwordPageUrl, 'https://auth.openai.com/log-in/password'); + assert.equal(result.passwordPagePath, '/log-in/password'); + assert.equal(result.passwordPageMode, 'login'); + assert.equal(result.passwordLoginFlow, true); + assert.equal(result.signupVerificationRequestedAt, null); + assert.equal(api.completions[0].payload.passwordPageMode, 'login'); +}); diff --git "a/\351\241\271\347\233\256\345\256\214\346\225\264\351\223\276\350\267\257\350\257\264\346\230\216.md" "b/\351\241\271\347\233\256\345\256\214\346\225\264\351\223\276\350\267\257\350\257\264\346\230\216.md" index 80078ddb..9b055359 100644 --- "a/\351\241\271\347\233\256\345\256\214\346\225\264\351\223\276\350\267\257\350\257\264\346\230\216.md" +++ "b/\351\241\271\347\233\256\345\256\214\346\225\264\351\223\276\350\267\257\350\257\264\346\230\216.md" @@ -452,6 +452,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功 8. 后台在真正确认 Step 3 完成前,会额外检查提交后是否切换页面;如果出现认证页 `Try again / 重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路 9. Step 3 收尾阶段如果页面切换导致旧内容脚本失联,后台会把单次消息等待收口到当前收尾预算内,优先尽快重试重连;若最终仍未恢复,则输出中文的步骤级错误,而不是直接暴露底层英文通信超时 10. 手机号注册时,如果 Step 3 收尾或 Step 4 准备验证码阶段在密码页检测到 `Incorrect phone number or password`,后台会把它视为当前注册手机号/密码不匹配:清空本轮 `signupPhoneNumber / signupPhoneActivation / signupPhoneCompletedActivation` 和手机号账号身份,回到 Step 1 重新获取号码并重开当前轮,避免继续在密码页重复点击 +11. 手机号注册时,如果 Step 3 实际落在 `/log-in/password` 登录密码页(已注册手机号),并且密码提交后的后台收尾确认已进入登录验证码、OAuth 授权页或 ChatGPT 已登录态,则自动跳过注册验证码、资料填写和注册成功等待节点,直接进入 OAuth 尾链 补充: