diff --git a/background.js b/background.js index e789fc8..b56bbbe 100644 --- a/background.js +++ b/background.js @@ -10802,6 +10802,28 @@ const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.crea DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS, DEFAULT_PHONE_CODE_POLL_ROUNDS, ensureStep8SignupPageReady, + navigateAuthTabToAddPhone: async (tabId, options = {}) => { + const visibleStep = Math.floor(Number(options.visibleStep || options.step) || 0) || 9; + const requestedTimeoutMs = Number(options.timeoutMs); + const timeoutMs = Number.isFinite(requestedTimeoutMs) && requestedTimeoutMs > 0 + ? requestedTimeoutMs + : await getOAuthFlowStepTimeoutMs(30000, { + step: visibleStep, + actionLabel: 'direct add-phone navigation', + }); + await chrome.tabs.update(tabId, { url: 'https://auth.openai.com/add-phone', active: true }); + await ensureStep8SignupPageReady(tabId, { + timeoutMs, + visibleStep, + logStepKey: options.logStepKey || 'phone-verification', + logMessage: options.logMessage || '步骤 9:认证页已失联,直接打开添加手机号页面后等待脚本恢复。', + }); + return { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }; + }, generateRandomBirthday, generateRandomName, getOAuthFlowRemainingMs, diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index 62331d4..e445e0b 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -13,6 +13,7 @@ requestStop = null, sendToContentScript, sendToContentScriptResilient, + navigateAuthTabToAddPhone = null, setState, broadcastDataUpdate = null, sleepWithStop, @@ -1381,6 +1382,11 @@ || /flow\s+was\s+stopped|stopped\s+by\s+user/i.test(message); } + function isAuthContentScriptUnreachableError(error) { + const message = String(error?.message || error || '').trim(); + return /Receiving end does not exist|Could not establish connection|Frame with ID \d+ is showing error page/i.test(message); + } + function buildPhoneRestartStep7Error(phoneNumber = '') { const suffix = phoneNumber ? ` 当前号码:${phoneNumber}。` : ''; return new Error( @@ -5627,11 +5633,37 @@ return matched?.label || `Country #${normalizedCountryId}`; }; - const ensureAddPhonePageBeforeSubmit = async (attemptLabel = 'before submit') => { + const directNavigateToAddPhone = async (attemptLabel = 'after replace-number rotation') => { + if (typeof navigateAuthTabToAddPhone !== 'function') { + return null; + } + const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; + const result = await navigateAuthTabToAddPhone(tabId, { + visibleStep, + timeoutMs: 30000, + logMessage: '步骤 9:认证页已失联,直接打开添加手机号页面后等待脚本恢复。', + logStepKey: 'phone-verification', + attemptLabel, + }); + if (result?.error) { + throw new Error(result.error); + } + return { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + ...(result || {}), + }; + }; + + const ensureAddPhonePageBeforeSubmit = async (attemptLabel = 'before submit', options = {}) => { + const allowDirectNavigation = Boolean(options.allowDirectNavigation); let snapshot = null; + let snapshotError = null; try { snapshot = await readPhonePageState(tabId, 12000); } catch (error) { + snapshotError = error; await addLog( `Step 9: failed to inspect auth page ${attemptLabel}. ${error.message}`, 'warn' @@ -5643,6 +5675,7 @@ return snapshot; } + let returnError = null; try { const returned = await returnToAddPhone(tabId); const merged = { @@ -5653,13 +5686,38 @@ return merged; } } catch (error) { + returnError = error; await addLog( `Step 9: failed to return to add-phone page ${attemptLabel}. ${error.message}`, 'warn' ); } - const latest = await readPhonePageState(tabId, 15000); + if ( + allowDirectNavigation + && ( + isAuthContentScriptUnreachableError(snapshotError) + || isAuthContentScriptUnreachableError(returnError) + ) + ) { + const navigated = await directNavigateToAddPhone(attemptLabel); + if (navigated) { + return navigated; + } + } + + let latest = null; + try { + latest = await readPhonePageState(tabId, 15000); + } catch (error) { + if (allowDirectNavigation && isAuthContentScriptUnreachableError(error)) { + const navigated = await directNavigateToAddPhone(attemptLabel); + if (navigated) { + return navigated; + } + } + throw error; + } if (!latest?.addPhonePage) { throw new Error( `Step 9: auth page is not on add-phone before phone submit (${attemptLabel}). URL: ${latest?.url || 'unknown'}` @@ -6205,11 +6263,7 @@ shouldCancelActivation = false; addPhoneReentryWithSameActivation = 0; - let returnResult = { - addPhonePage: true, - phoneVerificationPage: false, - url: 'https://auth.openai.com/add-phone', - }; + let returnResult = null; try { returnResult = await returnToAddPhone(tabId); } catch (returnError) { @@ -6221,7 +6275,7 @@ const stateSnapshot = await readPhonePageState(tabId, 12000); if (stateSnapshot?.addPhonePage) { returnResult = { - ...returnResult, + ...(returnResult || {}), ...stateSnapshot, addPhonePage: true, phoneVerificationPage: false, @@ -6231,20 +6285,16 @@ // Best effort: keep fallback state for compatibility with tests and older flows. } } - try { - const verifiedAddPhoneState = await ensureAddPhonePageBeforeSubmit('after replace-number rotation'); - returnResult = { - ...returnResult, - ...verifiedAddPhoneState, - addPhonePage: true, - phoneVerificationPage: false, - }; - } catch (verifyError) { - await addLog( - `Step 9: failed to verify add-phone page after number replacement. ${verifyError.message}`, - 'warn' - ); - } + const verifiedAddPhoneState = await ensureAddPhonePageBeforeSubmit( + 'after replace-number rotation', + { allowDirectNavigation: true } + ); + returnResult = { + ...(returnResult || {}), + ...verifiedAddPhoneState, + addPhonePage: true, + phoneVerificationPage: false, + }; await addLog( `步骤 9:正在更换号码并在步骤 9 内重试(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index 89d853b..bf5a29f 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -2435,6 +2435,12 @@ test('phone verification helper replaces numbers in step 9 and stops after repla url: 'https://auth.openai.com/phone-verification', }; } + if (message.type === 'RETURN_TO_ADD_PHONE') { + return { + addPhonePage: true, + url: 'https://auth.openai.com/add-phone', + }; + } throw new Error(`Unexpected content-script message: ${message.type}`); }, setState: async (updates) => { @@ -4677,6 +4683,288 @@ test('phone verification helper replaces number immediately when phone-verificat assert.equal(messages.includes('RETURN_TO_ADD_PHONE'), true); }); +test('phone verification helper directly navigates back to add-phone when replace-number recovery loses the auth page', async () => { + const requests = []; + const messages = []; + const navigationCalls = []; + const submittedNumbers = []; + const statusCallsById = {}; + let addPhoneRouteReady = false; + let currentState = { + heroSmsApiKey: 'demo-key', + heroSmsCountryId: 52, + heroSmsCountryLabel: 'Thailand', + verificationResendCount: 0, + phoneVerificationReplacementLimit: 2, + phoneCodeWaitSeconds: 60, + phoneCodeTimeoutWindows: 2, + phoneCodePollIntervalSeconds: 1, + phoneCodePollMaxRounds: 1, + currentPhoneActivation: null, + reusablePhoneActivation: null, + }; + const numbers = [ + { activationId: '930001', phoneNumber: '66953330001' }, + { activationId: '930002', phoneNumber: '66953330002' }, + ]; + let numberIndex = 0; + const realDateNow = Date.now; + let fakeNow = 0; + Date.now = () => fakeNow; + + try { + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + requests.push(parsedUrl); + const action = parsedUrl.searchParams.get('action'); + const id = parsedUrl.searchParams.get('id'); + + if (action === 'getPrices') { + return { + ok: true, + text: async () => buildHeroSmsPricesPayload(), + }; + } + + if (action === 'getNumber') { + const nextNumber = numbers[numberIndex]; + numberIndex += 1; + return { + ok: true, + text: async () => `ACCESS_NUMBER:${nextNumber.activationId}:${nextNumber.phoneNumber}`, + }; + } + + if (action === 'getStatus') { + statusCallsById[id] = (statusCallsById[id] || 0) + 1; + return { + ok: true, + text: async () => (id === '930001' ? 'STATUS_WAIT_CODE' : 'STATUS_OK:654321'), + }; + } + + if (action === 'setStatus') { + return { + ok: true, + text: async () => `STATUS_UPDATED:${id}`, + }; + } + + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...currentState }), + navigateAuthTabToAddPhone: async (tabId) => { + navigationCalls.push(tabId); + addPhoneRouteReady = true; + return { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }; + }, + sendToContentScriptResilient: async (_source, message) => { + messages.push(message.type); + if (message.type === 'SUBMIT_PHONE_NUMBER') { + submittedNumbers.push(message.payload.phoneNumber); + if (submittedNumbers.length === 1) { + return { + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + }; + } + if (!addPhoneRouteReady) { + throw new Error('Could not establish connection. Receiving end does not exist.'); + } + return { + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + }; + } + if (message.type === 'RESEND_PHONE_VERIFICATION_CODE') { + addPhoneRouteReady = false; + throw new Error('Could not establish connection. Receiving end does not exist.'); + } + if (message.type === 'RETURN_TO_ADD_PHONE') { + addPhoneRouteReady = false; + throw new Error('Could not establish connection. Receiving end does not exist.'); + } + if (message.type === 'STEP8_GET_STATE') { + if (!addPhoneRouteReady) { + throw new Error('Frame with ID 0 is showing error page.'); + } + return { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + return { + success: true, + consentReady: true, + url: 'https://auth.openai.com/authorize', + }; + } + throw new Error(`Unexpected content-script message: ${message.type}`); + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => { + fakeNow += 61000; + }, + throwIfStopped: () => {}, + }); + + const result = await helpers.completePhoneVerificationFlow(1, { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }); + + assert.deepStrictEqual(result, { + success: true, + consentReady: true, + url: 'https://auth.openai.com/authorize', + }); + assert.deepStrictEqual(submittedNumbers, ['66953330001', '66953330002']); + assert.deepStrictEqual(navigationCalls, [1]); + assert.ok(statusCallsById['930001'] >= 2, 'first number should be polled across both timeout windows'); + assert.equal(messages.includes('RETURN_TO_ADD_PHONE'), true); + } finally { + Date.now = realDateNow; + } +}); + +test('phone verification helper stops when add-phone recovery cannot be verified after number replacement', async () => { + const messages = []; + const submittedNumbers = []; + let currentState = { + heroSmsApiKey: 'demo-key', + heroSmsCountryId: 52, + heroSmsCountryLabel: 'Thailand', + verificationResendCount: 0, + phoneVerificationReplacementLimit: 2, + phoneCodeWaitSeconds: 60, + phoneCodeTimeoutWindows: 2, + phoneCodePollIntervalSeconds: 1, + phoneCodePollMaxRounds: 1, + currentPhoneActivation: null, + reusablePhoneActivation: null, + }; + const numbers = [ + { activationId: '940001', phoneNumber: '66954440001' }, + { activationId: '940002', phoneNumber: '66954440002' }, + ]; + let numberIndex = 0; + const realDateNow = Date.now; + let fakeNow = 0; + Date.now = () => fakeNow; + + try { + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + const action = parsedUrl.searchParams.get('action'); + const id = parsedUrl.searchParams.get('id'); + + if (action === 'getPrices') { + return { + ok: true, + text: async () => buildHeroSmsPricesPayload(), + }; + } + + if (action === 'getNumber') { + const nextNumber = numbers[numberIndex]; + numberIndex += 1; + return { + ok: true, + text: async () => `ACCESS_NUMBER:${nextNumber.activationId}:${nextNumber.phoneNumber}`, + }; + } + + if (action === 'getStatus') { + return { + ok: true, + text: async () => (id === '940001' ? 'STATUS_WAIT_CODE' : 'STATUS_OK:654321'), + }; + } + + if (action === 'setStatus') { + return { + ok: true, + text: async () => `STATUS_UPDATED:${id}`, + }; + } + + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...currentState }), + navigateAuthTabToAddPhone: async () => { + throw new Error('direct navigation failed'); + }, + sendToContentScriptResilient: async (_source, message) => { + messages.push(message.type); + if (message.type === 'SUBMIT_PHONE_NUMBER') { + submittedNumbers.push(message.payload.phoneNumber); + if (submittedNumbers.length === 1) { + return { + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + }; + } + throw new Error('Could not establish connection. Receiving end does not exist.'); + } + if (message.type === 'RESEND_PHONE_VERIFICATION_CODE') { + throw new Error('Could not establish connection. Receiving end does not exist.'); + } + if (message.type === 'RETURN_TO_ADD_PHONE') { + throw new Error('Could not establish connection. Receiving end does not exist.'); + } + if (message.type === 'STEP8_GET_STATE') { + throw new Error('Frame with ID 0 is showing error page.'); + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + return { + success: true, + consentReady: true, + url: 'https://auth.openai.com/authorize', + }; + } + throw new Error(`Unexpected content-script message: ${message.type}`); + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => { + fakeNow += 61000; + }, + throwIfStopped: () => {}, + }); + + await assert.rejects( + helpers.completePhoneVerificationFlow(1, { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }), + /direct navigation failed/ + ); + assert.deepStrictEqual(submittedNumbers, ['66954440001']); + assert.equal(messages.includes('RETURN_TO_ADD_PHONE'), true); + } finally { + Date.now = realDateNow; + } +}); + test('phone verification helper skips page resend for 5sim timeouts and rotates number directly', async () => { const requests = []; const messages = [];