diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index 3f55172..751091f 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -79,6 +79,10 @@ signupProfile, }); + if (result?.emailVerificationRequired || result?.emailVerificationPage) { + return result || {}; + } + await completeStepFromBackground(4, { phoneVerification: true, code: result?.code || '', @@ -88,6 +92,77 @@ return result || {}; } + async function executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey) { + if (shouldUseCustomRegistrationEmail(state)) { + await confirmCustomVerificationStepBypass(4); + return; + } + + const mail = getMailConfig(state); + if (mail.error) throw new Error(mail.error); + + const verificationFilterAfterTimestamp = mail.provider === '2925' + ? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS) + : stepStartedAt; + + if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { + await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info'); + await ensureIcloudMailSession({ + state, + step: 4, + actionLabel: '步骤 4:确认 iCloud 邮箱登录态', + }); + } + + throwIfStopped(); + if ( + mail.provider === HOTMAIL_PROVIDER + || mail.provider === LUCKMAIL_PROVIDER + || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER + || mail.provider === CLOUD_MAIL_PROVIDER + ) { + await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); + } else if (mail.provider === '2925') { + await addLog(`步骤 4:正在打开${mail.label}...`); + if (typeof ensureMail2925MailboxSession === 'function') { + await ensureMail2925MailboxSession({ + accountId: state.currentMail2925AccountId || null, + forceRelogin: false, + allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool), + expectedMailboxEmail: getExpectedMail2925MailboxEmail(state), + actionLabel: '步骤 4:确认 2925 邮箱登录态', + }); + } else { + await focusOrOpenMailTab(mail); + } + await addLog(`步骤 4:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info'); + } else { + await addLog(`步骤 4:正在打开${mail.label}...`); + await focusOrOpenMailTab(mail); + } + + const shouldRequestFreshCodeFirst = ![ + HOTMAIL_PROVIDER, + LUCKMAIL_PROVIDER, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + CLOUD_MAIL_PROVIDER, + ].includes(mail.provider); + const signupProfile = buildSignupProfileForVerificationStep(); + + await resolveVerificationStep(4, state, mail, { + filterAfterTimestamp: verificationFilterAfterTimestamp, + sessionKey: verificationSessionKey, + disableTimeBudgetCap: mail.provider === '2925', + requestFreshCodeFirst: shouldRequestFreshCodeFirst, + signupProfile, + resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER + ? 15000 + : ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') + ? 0 + : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS), + }); + } + async function focusOrOpenMailTab(mail) { const alive = await isTabAlive(mail.source); if (alive) { @@ -210,77 +285,15 @@ } if (isPhoneSignupState(state)) { - return executeSignupPhoneCodeStep(state, signupTabId); - } - - if (shouldUseCustomRegistrationEmail(state)) { - await confirmCustomVerificationStepBypass(4); - return; - } - - const mail = getMailConfig(state); - if (mail.error) throw new Error(mail.error); - - const verificationFilterAfterTimestamp = mail.provider === '2925' - ? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS) - : stepStartedAt; - - if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { - await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info'); - await ensureIcloudMailSession({ - state, - step: 4, - actionLabel: '步骤 4:确认 iCloud 邮箱登录态', - }); - } - - throwIfStopped(); - if ( - mail.provider === HOTMAIL_PROVIDER - || mail.provider === LUCKMAIL_PROVIDER - || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER - || mail.provider === CLOUD_MAIL_PROVIDER - ) { - await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); - } else if (mail.provider === '2925') { - await addLog(`步骤 4:正在打开${mail.label}...`); - if (typeof ensureMail2925MailboxSession === 'function') { - await ensureMail2925MailboxSession({ - accountId: state.currentMail2925AccountId || null, - forceRelogin: false, - allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool), - expectedMailboxEmail: getExpectedMail2925MailboxEmail(state), - actionLabel: '步骤 4:确认 2925 邮箱登录态', - }); - } else { - await focusOrOpenMailTab(mail); + const phoneResult = await executeSignupPhoneCodeStep(state, signupTabId); + if (phoneResult?.emailVerificationRequired || phoneResult?.emailVerificationPage) { + await addLog('步骤 4:手机验证码已通过,OpenAI 要求继续邮箱验证,切换到邮箱验证码轮询。', 'info'); + return executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey); } - await addLog(`步骤 4:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info'); - } else { - await addLog(`步骤 4:正在打开${mail.label}...`); - await focusOrOpenMailTab(mail); + return phoneResult; } - const shouldRequestFreshCodeFirst = ![ - HOTMAIL_PROVIDER, - LUCKMAIL_PROVIDER, - CLOUDFLARE_TEMP_EMAIL_PROVIDER, - CLOUD_MAIL_PROVIDER, - ].includes(mail.provider); - const signupProfile = buildSignupProfileForVerificationStep(); - - await resolveVerificationStep(4, state, mail, { - filterAfterTimestamp: verificationFilterAfterTimestamp, - sessionKey: verificationSessionKey, - disableTimeBudgetCap: mail.provider === '2925', - requestFreshCodeFirst: shouldRequestFreshCodeFirst, - signupProfile, - resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER - ? 15000 - : ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') - ? 0 - : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS), - }); + return executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey); } return { executeStep4 }; diff --git a/content/signup-page.js b/content/signup-page.js index 121a343..7bbb7b2 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -4714,8 +4714,9 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { } -async function waitForVerificationSubmitOutcome(step, timeout) { +async function waitForVerificationSubmitOutcome(step, timeout, options = {}) { const resolvedTimeout = timeout ?? (step === 8 ? 30000 : 12000); + const purpose = options?.purpose || ''; const start = Date.now(); let recoveryCount = 0; const maxRecoveryCount = 2; @@ -4761,6 +4762,14 @@ async function waitForVerificationSubmitOutcome(step, timeout) { if (postVerificationState?.state === 'step5') { return { success: true }; } + if (purpose === 'signup' && isEmailVerificationPage()) { + return { + success: true, + emailVerificationRequired: true, + emailVerificationPage: true, + url: location.href, + }; + } } const errorText = getVerificationErrorText(); @@ -4796,6 +4805,14 @@ async function waitForVerificationSubmitOutcome(step, timeout) { if (postVerificationState?.state === 'step5') { return { success: true }; } + if (purpose === 'signup' && isEmailVerificationPage()) { + return { + success: true, + emailVerificationRequired: true, + emailVerificationPage: true, + url: location.href, + }; + } } if (isVerificationPageStillVisible()) { @@ -5034,9 +5051,11 @@ async function fillVerificationCode(step, payload) { log(`步骤 ${step}:分格验证码页面未找到可点击提交按钮,继续等待页面自动推进。`, 'info'); } - const outcome = await waitForVerificationSubmitOutcome(step); + const outcome = await waitForVerificationSubmitOutcome(step, undefined, payload); if (outcome.invalidCode) { log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn'); + } else if (outcome.emailVerificationRequired) { + log(`步骤 ${step}:手机验证码已通过,页面进入邮箱验证码验证。`, 'ok'); } else if (outcome.addPhonePage) { log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn'); } else { @@ -5073,9 +5092,11 @@ async function fillVerificationCode(step, payload) { log(`步骤 ${step}:未找到可提交的验证码按钮,先等待页面自动推进或反馈结果。`, 'warn'); } - const outcome = await waitForVerificationSubmitOutcome(step); + const outcome = await waitForVerificationSubmitOutcome(step, undefined, payload); if (outcome.invalidCode) { log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn'); + } else if (outcome.emailVerificationRequired) { + log(`步骤 ${step}:手机验证码已通过,页面进入邮箱验证码验证。`, 'ok'); } else if (outcome.addPhonePage) { log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn'); } else { diff --git a/tests/background-step4-filter-window.test.js b/tests/background-step4-filter-window.test.js index 7f4fb41..5d57e9d 100644 --- a/tests/background-step4-filter-window.test.js +++ b/tests/background-step4-filter-window.test.js @@ -307,6 +307,87 @@ test('step 4 phone signup branch uses SMS helper and does not poll mailbox', asy ]); }); +test('step 4 phone signup email-verification handoff polls mailbox instead of completing phone-only', async () => { + const completions = []; + const phoneCalls = []; + const mailConfigCalls = []; + const resolvedCalls = []; + const tabReuses = []; + + const executor = api.createStep4Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + completeStepFromBackground: async (step, payload) => { + completions.push({ step, payload }); + }, + confirmCustomVerificationStepBypass: async () => {}, + ensureMail2925MailboxSession: async () => {}, + getMailConfig: (state) => { + mailConfigCalls.push({ state }); + return { + provider: '163', + label: '163 邮箱', + source: 'mail-163', + url: 'https://mail.163.com', + }; + }, + getTabId: async () => 1, + HOTMAIL_PROVIDER: 'hotmail-api', + isTabAlive: async () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + phoneVerificationHelpers: { + completeSignupPhoneVerificationFlow: async (tabId, options) => { + phoneCalls.push({ tabId, options }); + return { + success: true, + code: '123456', + emailVerificationRequired: true, + emailVerificationPage: true, + url: 'https://auth.openai.com/email-verification', + }; + }, + }, + resolveSignupMethod: () => 'phone', + resolveVerificationStep: async (step, state, mail, options) => { + resolvedCalls.push({ step, state, mail, options }); + }, + reuseOrCreateTab: async (source, url) => { + tabReuses.push({ source, url }); + }, + sendToContentScript: async () => ({ ready: true }), + sendToContentScriptResilient: async () => ({ ready: true }), + isRetryableContentScriptTransportError: () => false, + shouldUseCustomRegistrationEmail: () => false, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, + throwIfStopped: () => {}, + }); + + await executor.executeStep4({ + email: 'user@example.com', + resolvedSignupMethod: 'phone', + accountIdentifierType: 'phone', + signupPhoneNumber: '66959916439', + signupPhoneActivation: { activationId: 'signup-123', phoneNumber: '66959916439' }, + }); + + assert.equal(phoneCalls.length, 1); + assert.equal(mailConfigCalls.length, 1); + assert.equal(resolvedCalls.length, 1); + assert.deepStrictEqual(completions, []); + assert.deepStrictEqual(tabReuses, [ + { source: 'mail-163', url: 'https://mail.163.com' }, + ]); + assert.equal(resolvedCalls[0].step, 4); + assert.equal(resolvedCalls[0].mail.label, '163 邮箱'); + assert.equal(resolvedCalls[0].options.requestFreshCodeFirst, true); + assert.equal(Object.prototype.hasOwnProperty.call(resolvedCalls[0].options, 'signupProfile'), true); +}); + test('step 4 prepare retries transport by recovering retry page without replaying full prepare loop', async () => { let sendToContentScriptCalls = 0; let recoverCalls = 0; diff --git a/tests/step4-split-code-submit.test.js b/tests/step4-split-code-submit.test.js index 24b6c55..c33ff06 100644 --- a/tests/step4-split-code-submit.test.js +++ b/tests/step4-split-code-submit.test.js @@ -206,6 +206,141 @@ return { assert.equal(snapshot.splitCodeEvents.filter((event) => event.startsWith('delay:split-code')).length, 1); }); +test('fillVerificationCode treats phone signup SMS landing on email verification as handoff success', async () => { + const api = new Function(` +let now = 0; +const logs = []; +const clicks = []; +const VERIFICATION_CODE_INPUT_SELECTOR = 'input[name="code"]'; +const VERIFICATION_PAGE_PATTERN = /verification code/i; +const location = { + href: 'https://auth.openai.com/phone-verification', + pathname: '/phone-verification', +}; +function KeyboardEvent(type, init = {}) { + this.type = type; + Object.assign(this, init); +} + +const codeInput = { + value: '', + disabled: false, + form: null, + getAttribute(name) { + if (name === 'maxlength') return '6'; + if (name === 'aria-disabled') return 'false'; + if (name === 'name') return 'code'; + return ''; + }, + closest() { return null; }, + focus() {}, + dispatchEvent() {}, +}; +const submitBtn = { + tagName: 'BUTTON', + textContent: 'Continue', + disabled: false, + getAttribute(name) { + if (name === 'type') return 'submit'; + if (name === 'aria-disabled') return 'false'; + return ''; + }, + click() { + location.href = 'https://auth.openai.com/email-verification'; + location.pathname = '/email-verification'; + }, +}; + +const document = { + readyState: 'complete', + body: { + textContent: 'Enter the verification code we just sent to your email', + innerText: 'Enter the verification code we just sent to your email', + }, + querySelector(selector) { + if (selector === VERIFICATION_CODE_INPUT_SELECTOR && location.pathname === '/phone-verification') return codeInput; + if (selector === 'button[type="submit"]' && location.pathname === '/phone-verification') return submitBtn; + if (selector === 'form[action*="email-verification" i]' && location.pathname === '/email-verification') return {}; + return null; + }, + querySelectorAll(selector) { + if (selector === 'input[maxlength="1"]') return []; + if (location.pathname === '/phone-verification' && selector === 'button[type="submit"], input[type="submit"]') return [submitBtn]; + if (location.pathname === '/phone-verification' && selector === 'button, [role="button"], input[type="button"], input[type="submit"]') return [submitBtn]; + if (location.pathname === '/email-verification' && selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') { + return [{ textContent: 'Resend code', disabled: false, getAttribute() { return ''; } }]; + } + return []; + }, +}; + +const Date = { now: () => now }; +function throwIfStopped() {} +function log(message, level = 'info') { logs.push({ message, level }); } +async function waitForLoginVerificationPageReady() {} +function is405MethodNotAllowedPage() { return false; } +async function handle405ResendError() {} +function fillInput(el, value) { el.value = value; } +async function sleep(ms = 0) { now += ms || 15000; } +async function waitForDocumentLoadComplete() {} +function isStep5Ready() { return false; } +function isStep8Ready() { return false; } +function isAddPhonePageReady() { return false; } +function isVisibleElement() { return true; } +function isActionEnabled(el) { return Boolean(el) && !el.disabled; } +function getActionText(el) { return el?.textContent || ''; } +async function humanPause() {} +function simulateClick(el) { el.click(); clicks.push(el.textContent); } +function getVerificationErrorText() { return ''; } +function getCurrentAuthRetryPageState() { return null; } +async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); } +function createSignupUserAlreadyExistsError() { return new Error('user already exists'); } +function isPhoneVerificationPageReady() { return location.pathname === '/phone-verification'; } +function getPageTextSnapshot() { return document.body.textContent; } +function findResendVerificationCodeTrigger() { + return location.pathname === '/email-verification' + ? { textContent: 'Resend code', disabled: false, getAttribute() { return ''; } } + : null; +} + +${extractFunction('getVisibleSplitVerificationInputs')} +${extractFunction('getVerificationCodeTarget')} +${extractFunction('isEmailVerificationPage')} +${extractFunction('isVerificationPageStillVisible')} +${extractFunction('isSignupProfilePageUrl')} +${extractFunction('isLikelyLoggedInChatgptHomeUrl')} +${extractFunction('getStep4PostVerificationState')} +${extractFunction('getVerificationSubmitButtonForTarget')} +${extractFunction('waitForVerificationSubmitButton')} +${extractFunction('waitForVerificationCodeTarget')} +${extractFunction('waitForSplitVerificationInputsFilled')} +${extractFunction('isCombinedSignupVerificationProfilePage')} +${extractFunction('waitForCombinedSignupVerificationProfilePage')} +${extractFunction('waitForVerificationSubmitOutcome')} +${extractFunction('fillVerificationCode')} + +return { + run() { + return fillVerificationCode(4, { code: '123456', purpose: 'signup' }); + }, + snapshot() { + return { clicks, logs, href: location.href }; + }, +}; +`)(); + + const result = await api.run(); + const snapshot = api.snapshot(); + + assert.equal(snapshot.href, 'https://auth.openai.com/email-verification'); + assert.deepStrictEqual(snapshot.clicks, ['Continue']); + assert.equal(result.success, true); + assert.equal(result.emailVerificationRequired, true); + assert.equal(result.emailVerificationPage, true); + assert.equal(result.url, 'https://auth.openai.com/email-verification'); + assert.equal(Object.prototype.hasOwnProperty.call(result, 'invalidCode'), false); +}); + test('fillVerificationCode does not short-circuit on mixed email-verification profile page before verification exits', async () => { const api = new Function(` const logs = [];