From 3f3f9275cccab65f87254428e077863f1f9bb55c Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Fri, 15 May 2026 23:03:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=20OAuth=20=E5=90=8E=E7=BD=AE?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 84 +-- background/logging-status.js | 2 +- background/message-router.js | 46 +- background/phone-verification-flow.js | 475 ++++++++++++----- background/steps/fetch-login-code.js | 418 ++++++++++----- background/steps/fill-plus-checkout.js | 2 +- background/steps/oauth-login.js | 142 +++-- background/steps/submit-signup-email.js | 2 +- background/tab-runtime.js | 4 +- background/verification-flow.js | 2 +- content/signup-page.js | 6 +- content/utils.js | 10 +- data/step-definitions.js | 84 ++- phone-sms/providers/five-sim.js | 2 +- phone-sms/providers/hero-sms.js | 8 +- phone-sms/providers/registry.js | 2 +- shared/source-registry.js | 3 + sidepanel/sidepanel.js | 20 +- tests/background-step6-retry-limit.test.js | 69 +-- tests/background-step7-recovery.test.js | 577 ++++++++++++++++++--- tests/five-sim-provider.test.js | 18 +- tests/phone-sms-provider-registry.test.js | 2 +- tests/phone-verification-flow.test.js | 95 +++- tests/source-registry-module.test.js | 3 + tests/step-definitions-module.test.js | 57 +- tests/step8-retry-page-recovery.test.js | 29 +- 26 files changed, 1578 insertions(+), 584 deletions(-) diff --git a/background.js b/background.js index 931dc03..f7e22a7 100644 --- a/background.js +++ b/background.js @@ -9596,6 +9596,9 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([ 'plus-checkout-return', 'oauth-login', 'fetch-login-code', + 'post-login-phone-verification', + 'bind-email', + 'fetch-bind-email-code', 'confirm-oauth', ]); const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([ @@ -10047,6 +10050,9 @@ async function waitForRunningStepsToFinish(payload = {}) { const AUTH_CHAIN_NODE_IDS = new Set([ 'oauth-login', 'fetch-login-code', + 'post-login-phone-verification', + 'bind-email', + 'fetch-bind-email-code', 'confirm-oauth', 'platform-verify', ]); @@ -12243,7 +12249,6 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ resolveSignupMethod, reuseOrCreateTab, sendToContentScriptResilient, - buildRegistrationEmailStateUpdates, setState, shouldUseCustomRegistrationEmail, sleepWithStop, @@ -12376,6 +12381,9 @@ const stepExecutorsByKey = { 'plus-checkout-return': (state) => plusReturnConfirmExecutor.executePlusReturnConfirm(state), 'oauth-login': (state) => step7Executor.executeStep7(state), 'fetch-login-code': (state) => step8Executor.executeStep8(state), + 'post-login-phone-verification': (state) => step8Executor.executePostLoginPhoneVerification(state), + 'bind-email': (state) => step8Executor.executeBindEmail(state), + 'fetch-bind-email-code': (state) => step8Executor.executeFetchBindEmailCode(state), 'confirm-oauth': (state) => step9Executor.executeStep9(state), 'platform-verify': (state) => executeStep10(state), }; @@ -13610,22 +13618,12 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS, throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`); } if (pageState?.addPhonePage || pageState?.phoneVerificationPage) { - const latestState = await getState(); - if (!Boolean(latestState?.phoneVerificationEnabled)) { - const urlPart = pageState?.url ? ` URL: ${pageState.url}` : ''; - throw new Error( - pageState?.phoneVerificationPage - ? `步骤 ${visibleStep}:当前认证页进入手机验证码页,但未开启接码功能,无法继续自动授权。${urlPart}`.trim() - : `步骤 ${visibleStep}:当前认证页进入手机号页面,但未开启接码功能,无法继续自动授权。${urlPart}`.trim() - ); - } - await phoneVerificationHelpers.completePhoneVerificationFlow(tabId, pageState, { visibleStep }); - recovered = false; - await sleepWithStop(250); - continue; - } - if (pageState?.addPhonePage) { - throw new Error(`步骤 ${visibleStep}:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。`); + const urlPart = pageState?.url ? ` URL: ${pageState.url}` : ''; + throw new Error( + pageState?.phoneVerificationPage + ? `步骤 ${visibleStep}:自动确认 OAuth 只处理 OAuth 授权页,当前仍在手机验证码页。${urlPart}`.trim() + : `步骤 ${visibleStep}:自动确认 OAuth 只处理 OAuth 授权页,当前仍在添加手机号页。${urlPart}`.trim() + ); } if (pageState?.retryPage) { const retryUrl = String(pageState?.url || '').trim(); @@ -13913,7 +13911,8 @@ async function recoverOAuthLocalhostTimeout(details = {}) { const authLoginStep = typeof getAuthChainStartStepId === 'function' ? getAuthChainStartStepId(state || {}) : FINAL_OAUTH_CHAIN_START_STEP; - const loginCodeStep = Number(visibleStep) >= 12 ? 11 : 8; + const authLoginNodeId = String(getNodeIdByStepForState(authLoginStep, state || {}) || 'oauth-login').trim(); + const confirmNodeId = String(getNodeIdByStepForState(visibleStep, state || {}) || 'confirm-oauth').trim(); await addLog( `检测到 OAuth localhost 回调等待窗口已过期,正在复核认证页并回到步骤 ${authLoginStep} 重拉授权链路。`, @@ -13932,7 +13931,7 @@ async function recoverOAuthLocalhostTimeout(details = {}) { }); } catch (inspectError) { await addLog( - `复核认证页状态失败(${getErrorMessage(inspectError)}),将先尝试按步骤 ${loginCodeStep} 收尾恢复。`, + `复核认证页状态失败(${getErrorMessage(inspectError)}),将按当前 OAuth 流程图重新执行授权前置节点。`, 'warn', { step: visibleStep, stepKey: 'confirm-oauth' } ); @@ -13958,22 +13957,45 @@ async function recoverOAuthLocalhostTimeout(details = {}) { if (!step7Executor?.executeStep7 || !step8Executor?.executeStep8) { return null; } + const workflowNodeIds = getAutoRunWorkflowNodeIds(latestState); + const authStartIndex = workflowNodeIds.indexOf(authLoginNodeId); + const confirmIndex = workflowNodeIds.indexOf(confirmNodeId); + if (authStartIndex < 0 || confirmIndex < 0 || authStartIndex >= confirmIndex) { + return null; + } + const recoveryNodeIds = workflowNodeIds.slice(authStartIndex, confirmIndex); + const runRecoveryNode = async (nodeId) => { + const recoveryState = await getState(); + const recoveryStep = getStepIdByNodeIdForState(nodeId, recoveryState); + const payload = { + ...recoveryState, + visibleStep: recoveryStep, + nodeId, + }; + switch (nodeId) { + case 'oauth-login': + return step7Executor.executeStep7(payload); + case 'fetch-login-code': + return step8Executor.executeStep8(payload); + case 'post-login-phone-verification': + return step8Executor.executePostLoginPhoneVerification(payload); + case 'bind-email': + return step8Executor.executeBindEmail(payload); + case 'fetch-bind-email-code': + return step8Executor.executeFetchBindEmailCode(payload); + default: + throw new Error(`OAuth localhost 恢复不支持节点 ${nodeId}。`); + } + }; await addLog( - `正在自动重开步骤 ${authLoginStep} -> ${loginCodeStep},恢复到可继续捕获 localhost 回调的状态。`, + `正在自动重开 OAuth 前置节点:${recoveryNodeIds.join(' -> ')}。`, 'warn', { step: visibleStep, stepKey: 'confirm-oauth' } ); - await step7Executor.executeStep7({ - ...latestState, - visibleStep: authLoginStep, - }); - - const stateAfterStep7 = await getState(); - await step8Executor.executeStep8({ - ...stateAfterStep7, - visibleStep: loginCodeStep, - }); + for (const nodeId of recoveryNodeIds) { + await runRecoveryNode(nodeId); + } const recoveredState = await getState(); const oauthUrl = String(recoveredState?.oauthUrl || state?.oauthUrl || '').trim(); @@ -13989,7 +14011,7 @@ async function recoverOAuthLocalhostTimeout(details = {}) { }); await addLog( - `已恢复到步骤 ${authLoginStep} -> ${loginCodeStep} 收尾状态,并刷新 OAuth localhost 回调等待窗口,准备重试当前步骤。`, + `已恢复到自动确认 OAuth 前置状态,并刷新 OAuth localhost 回调等待窗口,准备重试当前步骤。`, 'warn', { step: visibleStep, stepKey: 'confirm-oauth' } ); diff --git a/background/logging-status.js b/background/logging-status.js index c444873..e0c41cc 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -99,7 +99,7 @@ function isVerificationMailPollingError(error) { const message = getErrorMessage(error); - return /未在 .*邮箱中找到新的匹配邮件|未在 Hotmail 收件箱中找到新的匹配验证码|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|did not respond in \d+s|405\s+method\s+not\s+allowed|route\s+error.*405|did\s+not\s+provide\s+an?\s+[`'"]?action|post\s+request\s+to\s+["']?\/(?:email|phone)-verification/i.test(message); + return /未在 .*邮箱中找到新的匹配邮件|未在 Hotmail 收件箱中找到新的匹配验证码|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|内容脚本\s+\d+(?:\.\d+)?\s*秒内未响应|did not respond in \d+s|405\s+method\s+not\s+allowed|route\s+error.*405|did\s+not\s+provide\s+an?\s+[`'"]?action|post\s+request\s+to\s+["']?\/(?:email|phone)-verification/i.test(message); } function isAddPhoneAuthFailure(error) { diff --git a/background/message-router.js b/background/message-router.js index 41d0033..cec1633 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -263,11 +263,13 @@ 6: 'wait-registration-success', 7: 'oauth-login', 8: 'fetch-login-code', - 9: 'confirm-oauth', - 10: 'platform-verify', + 9: 'post-login-phone-verification', + 10: 'confirm-oauth', 11: 'fetch-login-code', - 12: 'confirm-oauth', - 13: 'platform-verify', + 12: 'post-login-phone-verification', + 13: 'confirm-oauth', + 14: 'platform-verify', + 15: 'platform-verify', }); function getStepKeyForState(step, state = {}) { @@ -647,6 +649,42 @@ return; } + if (stepKey === 'post-login-phone-verification') { + await setState({ + currentPhoneVerificationCode: '', + signupPhoneVerificationRequestedAt: null, + signupPhoneVerificationPurpose: '', + }); + return; + } + + if (stepKey === 'bind-email') { + const updates = {}; + if (payload.bindEmailSubmitted !== undefined) { + updates.bindEmailSubmitted = Boolean(payload.bindEmailSubmitted); + } + if (payload.email !== undefined) { + updates.email = payload.email || null; + } + if (payload.step8VerificationTargetEmail !== undefined) { + updates.step8VerificationTargetEmail = payload.step8VerificationTargetEmail || ''; + } + if (Object.keys(updates).length) { + await setState(updates); + } + return; + } + + if (stepKey === 'fetch-bind-email-code') { + await setState({ + lastEmailTimestamp: payload.emailTimestamp || null, + loginVerificationRequestedAt: null, + step8VerificationTargetEmail: '', + bindEmailSubmitted: false, + }); + return; + } + if (stepKey === 'confirm-oauth') { if (payload.localhostUrl) { if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) { diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index 694ca02..d5c452c 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -381,7 +381,7 @@ function assertFiveSimMaxPriceCompatibleWithOperator(operator, maxPriceLimit) { const normalizedOperator = normalizeFiveSimCountryCode(operator, DEFAULT_FIVE_SIM_OPERATOR); if (maxPriceLimit !== null && maxPriceLimit !== undefined && normalizedOperator !== DEFAULT_FIVE_SIM_OPERATOR) { - throw new Error('5sim maxPrice only works when operator is "any"; clear the price limit or switch operator to any before buying a number.'); + throw new Error('5sim 价格上限仅支持运营商为 "any" 时使用;请清空价格上限,或先把运营商切换为 any。'); } } @@ -910,6 +910,12 @@ sms_timeout: '短信超时', resend_throttled: '重发短信被限流', code_rejected: '验证码被拒绝', + add_phone_rejected: '添加手机号被拒绝', + activation_not_found: '接码订单不存在或已失效', + resend_phone_banned: 'OpenAI 无法向该号码发送短信', + phone_max_usage_exceeded: '手机号达到使用上限', + resend_server_error: '重发短信后进入服务器错误页', + whatsapp_resend_channel: '页面重发入口切换为 WhatsApp 通道', unknown: '未知', }; if (reasonMap[normalized]) { @@ -922,6 +928,184 @@ return text; } + function formatPhoneSmsApiFailureReason(reason = '') { + const text = String(reason || '').trim(); + if (!text) { + return '未知错误'; + } + if (/\bBAD_KEY\b|\bWRONG_KEY\b|\bINVALID_KEY\b/i.test(text)) { + return 'API Key 无效(BAD_KEY)'; + } + if (/\bNO_BALANCE\b|\bNOT_ENOUGH_BALANCE\b/i.test(text)) { + return '余额不足'; + } + if (/\bBANNED\b|\bACCOUNT_BANNED\b/i.test(text)) { + return '账号已被封禁'; + } + if (/\bNO_NUMBERS\b/i.test(text)) { + return '暂无可用号码(NO_NUMBERS)'; + } + if (/no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+available|no\s+numbers\s+within|暂无可用号码|均无可用号码|无可用号码/i.test(text)) { + return '暂无可用号码'; + } + const wrongMaxPrice = text.match(/\bWRONG_MAX_PRICE(?::|\s+requires\s+)?(\d+(?:\.\d+)?)?\b/i); + if (wrongMaxPrice) { + return wrongMaxPrice[1] + ? `价格上限过低,平台要求至少 ${wrongMaxPrice[1]}(WRONG_MAX_PRICE)` + : '价格上限不符合平台要求(WRONG_MAX_PRICE)'; + } + if (/rate\s*limit|too\s+many\s+requests|限流/i.test(text)) { + return '请求限流'; + } + if (/unauthorized|forbidden|invalid\s+token|bad\s+key|wrong\s+key/i.test(text)) { + return 'API Key 无效'; + } + if (/order\s+not\s+found|activation\s+not\s+found|no\s+such\s+order/i.test(text)) { + return '订单不存在或已失效'; + } + if (/timed\s*out|timeout/i.test(text)) { + return '请求超时'; + } + if (/failed\s+to\s+fetch|networkerror|load\s+failed/i.test(text)) { + return '网络请求失败'; + } + if (/empty\s+response/i.test(text)) { + return '空响应'; + } + if (/unknown\s+terminal\s+error/i.test(text)) { + return '未知终止错误'; + } + return text; + } + + function formatHeroSmsActionName(action = '') { + const normalized = String(action || '').trim().toLowerCase(); + if (normalized === 'getnumber' || normalized === 'getnumberv2') { + return '获取手机号'; + } + if (normalized === 'getstatus' || normalized === 'getstatusv2') { + return '查询短信状态'; + } + if (normalized === 'setstatus') { + return '更新订单状态'; + } + if (normalized === 'getprices' || normalized === 'getpricesextended') { + return '查询价格'; + } + return action ? `${action} 请求` : '请求'; + } + + function formatPhoneSmsActionLabel(actionLabel = '') { + const text = String(actionLabel || '').trim(); + if (!text) { + return '接码平台请求'; + } + const normalized = text.toLowerCase(); + const heroMatch = text.match(/^HeroSMS\s+(.+)$/i); + if (heroMatch) { + return `HeroSMS ${formatHeroSmsActionName(heroMatch[1])}`; + } + if (normalized === '5sim guest prices') { + return '5sim 查询游客价格'; + } + if (normalized === '5sim user prices') { + return '5sim 查询账号价格'; + } + if (normalized === '5sim buy activation') { + return '5sim 购买手机号'; + } + if (normalized === '5sim check activation') { + return '5sim 查询短信状态'; + } + if (normalized === '5sim reuse activation') { + return '5sim 复用手机号'; + } + if (normalized === 'nexsms getcountrybyservice') { + return 'NexSMS 查询服务国家'; + } + if (normalized === 'nexsms price lookup') { + return 'NexSMS 查询价格'; + } + if (normalized === 'nexsms purchase') { + return 'NexSMS 购买手机号'; + } + if (normalized === 'nexsms close activation') { + return 'NexSMS 关闭订单'; + } + if (normalized === 'nexsms get sms messages') { + return 'NexSMS 查询短信'; + } + return text; + } + + function createPhoneSmsActionFailureError(actionLabel, reason = '', payload = null, status = 0) { + const message = `${formatPhoneSmsActionLabel(actionLabel)}失败:${formatPhoneSmsApiFailureReason(reason || status)}`; + const error = new Error(message); + if (payload !== null && payload !== undefined) { + error.payload = payload; + } + if (status) { + error.status = status; + } + return error; + } + + function stripRepeatedHeroSmsFailurePrefix(action, reason = '') { + const actionText = String(action || '').trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + if (!actionText) { + return String(reason || '').trim(); + } + let text = String(reason || '').trim(); + const prefixPattern = new RegExp(`^HeroSMS\\s+${actionText}\\s+failed\\s*:\\s*`, 'i'); + while (prefixPattern.test(text)) { + text = text.replace(prefixPattern, '').trim(); + } + return text; + } + + function createHeroSmsActionFailureError(action, reason = '') { + const normalizedReason = stripRepeatedHeroSmsFailurePrefix(action, describeHeroSmsPayload(reason)); + const error = new Error(`HeroSMS ${formatHeroSmsActionName(action)}失败:${formatPhoneSmsApiFailureReason(normalizedReason)}`); + error.localizedPhoneSmsFailure = true; + return error; + } + + function formatProviderAcquireFailure(providerId, message = '') { + const providerLabel = getPhoneSmsProviderLabel(providerId); + let text = String(message || '').trim(); + if (!text) { + return '未知错误'; + } + text = text.replace(/^Step\s+\d+\s*[::]\s*/i, '').trim(); + const heroFailureMatch = text.match(/^HeroSMS\s+([A-Za-z0-9]+)\s+failed\s*:\s*(.+)$/i); + if (heroFailureMatch) { + return `${formatHeroSmsActionName(heroFailureMatch[1])}失败:${formatPhoneSmsApiFailureReason(stripRepeatedHeroSmsFailurePrefix(heroFailureMatch[1], heroFailureMatch[2]))}`; + } + if (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_HERO && /^HeroSMS\s+.+失败:/.test(text)) { + return text.replace(/^HeroSMS\s+/, '').trim(); + } + if (/countries\s+are\s+empty|未选择国家/i.test(text)) { + return '未选择国家,请先在接码设置中至少选择 1 个国家'; + } + if (/failed\s+to\s+acquire\s+(?:a\s+)?phone(?:\s+number|\s+activation)?/i.test(text)) { + return '获取手机号失败'; + } + if (/no\s+numbers\s+available\s+across|no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+within|暂无可用号码|均无可用号码|无可用号码|\bNO_NUMBERS\b/i.test(text)) { + return formatPhoneSmsApiFailureReason(text); + } + if (/buy activation failed|purchase failed|price lookup failed|check activation failed/i.test(text)) { + return text + .replace(/^5sim\s+buy activation failed\s*:\s*/i, '购买手机号失败:') + .replace(/^5sim\s+check activation failed\s*:\s*/i, '查询短信状态失败:') + .replace(/^NexSMS\s+purchase failed\s*:\s*/i, '购买手机号失败:') + .replace(/^NexSMS\s+price lookup failed\s*:\s*/i, '查询价格失败:'); + } + if (providerLabel && text.startsWith(`${providerLabel}:`)) { + return text.slice(providerLabel.length + 1).trim() || text; + } + return text; + } + function isPhoneSmsReuseEnabled(state = {}) { if (isPhoneSignupIdentityState(state)) { return false; @@ -1472,7 +1656,7 @@ if (message.startsWith(PHONE_RESEND_SERVER_ERROR_PREFIX)) { return new Error(message); } - return new Error(`${PHONE_RESEND_SERVER_ERROR_PREFIX}${message || 'OpenAI contact-verification page returned HTTP ERROR 500 after resend.'}`); + return new Error(`${PHONE_RESEND_SERVER_ERROR_PREFIX}${message || 'OpenAI contact-verification 页面在重发短信后返回 HTTP ERROR 500。'}`); } function getPhoneResendServerErrorFromSnapshot(snapshot = {}) { @@ -1490,7 +1674,7 @@ .trim(); const titleText = String(snapshot?.title || '').replace(/\s+/g, ' ').trim(); if (!bodyText) { - return isPhoneResendServerError(titleText) ? (titleText || 'OpenAI contact-verification page returned HTTP ERROR 500 after resend.') : ''; + return isPhoneResendServerError(titleText) ? (titleText || 'OpenAI contact-verification 页面在重发短信后返回 HTTP ERROR 500。') : ''; } const combined = [ bodyText, @@ -1503,7 +1687,7 @@ if (!isPhoneResendServerError(combined)) { return ''; } - return combined || 'OpenAI contact-verification page returned HTTP ERROR 500 after resend.'; + return combined || 'OpenAI contact-verification 页面在重发短信后返回 HTTP ERROR 500。'; } async function readPhoneResendServerErrorFromAuthTab(tabId) { @@ -1529,11 +1713,11 @@ } function buildHighRiskResendThrottledError(message = '') { - return new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${message || 'OpenAI resend is throttled and configured as high-probability banned phone.'}`); + return new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${message || 'OpenAI 重发短信被限流,且当前配置会按高概率封禁手机号处理。'}`); } function buildPhoneMaxUsageExceededError(message = '') { - return new Error(`PHONE_MAX_USAGE_EXCEEDED::${message || 'OpenAI reported phone_max_usage_exceeded for this phone number.'}`); + return new Error(`PHONE_MAX_USAGE_EXCEEDED::${message || 'OpenAI 返回 phone_max_usage_exceeded,当前手机号已达到使用上限。'}`); } function isPhoneMaxUsageExceededFlowError(error) { @@ -1559,9 +1743,9 @@ } const normalizedProvider = normalizePhoneSmsProvider(provider); if (normalizedProvider === PHONE_SMS_PROVIDER_5SIM) { - return /5sim\s+check\s+activation\s+failed.*order\s+not\s+found|order\s+not\s+found|activation\s+not\s+found|no\s+such\s+order/i.test(message); + return /5sim\s+check\s+activation\s+failed.*order\s+not\s+found|order\s+not\s+found|activation\s+not\s+found|no\s+such\s+order|订单不存在|订单.*失效/i.test(message); } - return /activation\s+not\s+found|order\s+not\s+found|no\s+such\s+order/i.test(message); + return /activation\s+not\s+found|order\s+not\s+found|no\s+such\s+order|订单不存在|订单.*失效/i.test(message); } function isStopRequestedError(error) { @@ -1590,8 +1774,7 @@ const safeMax = Math.max(0, Math.floor(Number(maxNumberReplacementAttempts) || 0)); const safeReason = String(reason || 'unknown').trim() || 'unknown'; return new Error( - `步骤 9:更换 ${safeMax} 次号码后手机号验证仍未成功。最后原因:${safeReason}. ` - + `Step 9: phone verification did not succeed after ${safeMax} number replacements. Last reason: ${safeReason}.` + `步骤 9:更换 ${safeMax} 次号码后手机号验证仍未成功。最后原因:${formatStep9Reason(safeReason)}。` ); } @@ -1632,15 +1815,17 @@ const text = await response.text(); const payload = parseHeroSmsPayload(text); if (!response.ok) { - const requestError = new Error(`${actionLabel} failed: ${describeHeroSmsPayload(payload) || response.status}`); - requestError.payload = payload; - requestError.status = response.status; - throw requestError; + throw createPhoneSmsActionFailureError( + actionLabel, + describeHeroSmsPayload(payload) || response.status, + payload, + response.status + ); } return payload; } catch (error) { if (error?.name === 'AbortError') { - throw new Error(`${actionLabel} timed out.`); + throw new Error(`${formatPhoneSmsActionLabel(actionLabel)}超时。`); } throw error; } finally { @@ -1707,15 +1892,17 @@ const text = await response.text(); const payload = parseFiveSimPayload(text); if (!response.ok) { - const requestError = new Error(`${actionLabel} failed: ${describeFiveSimPayload(payload) || response.status}`); - requestError.payload = payload; - requestError.status = response.status; - throw requestError; + throw createPhoneSmsActionFailureError( + actionLabel, + describeFiveSimPayload(payload) || response.status, + payload, + response.status + ); } return payload; } catch (error) { if (error?.name === 'AbortError') { - throw new Error(`${actionLabel} timed out.`); + throw new Error(`${formatPhoneSmsActionLabel(actionLabel)}超时。`); } throw error; } finally { @@ -1800,15 +1987,17 @@ const text = await response.text(); const payload = parseNexSmsPayload(text); if (!response.ok) { - const requestError = new Error(`${actionLabel} failed: ${describeNexSmsPayload(payload) || response.status}`); - requestError.payload = payload; - requestError.status = response.status; - throw requestError; + throw createPhoneSmsActionFailureError( + actionLabel, + describeNexSmsPayload(payload) || response.status, + payload, + response.status + ); } return payload; } catch (error) { if (error?.name === 'AbortError') { - throw new Error(`${actionLabel} timed out.`); + throw new Error(`${formatPhoneSmsActionLabel(actionLabel)}超时。`); } throw error; } finally { @@ -1823,7 +2012,7 @@ if (provider === PHONE_SMS_PROVIDER_5SIM) { const apiKey = normalizeApiKey(state.fiveSimApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('5sim API key is missing. Save it in the side panel before running the phone flow.'); + throw new Error('5sim API Key 缺失,请先在侧边栏保存接码 API Key。'); } const configuredMaxPrice = normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice); const operator = normalizeFiveSimCountryCode(state.fiveSimOperator, DEFAULT_FIVE_SIM_OPERATOR); @@ -1845,7 +2034,7 @@ if (provider === PHONE_SMS_PROVIDER_NEXSMS) { const apiKey = normalizeApiKey(state.nexSmsApiKey || state.heroSmsApiKey); if (!apiKey) { - throw new Error('NexSMS API key is missing. Save it in the side panel before running the phone flow.'); + throw new Error('NexSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); } return { provider, @@ -1858,7 +2047,7 @@ const apiKey = normalizeApiKey(state.heroSmsApiKey); if (!apiKey) { - throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.'); + throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); } return { provider, @@ -1871,7 +2060,7 @@ function resolveHeroSmsPhoneConfig(state = {}) { const apiKey = normalizeApiKey(state.heroSmsApiKey); if (!apiKey) { - throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.'); + throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); } return { provider: PHONE_SMS_PROVIDER_HERO, @@ -2087,7 +2276,7 @@ if (!text) { return false; } - return /no\s+numbers\s+available\s+across|no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+within\s+(?:maxprice|price\s+range)|price\s+range\s+is\s+invalid|step\s*9:\s*(?:5sim|nexsms)\s+countries\s+are\s+empty|\bNO_NUMBERS\b/i.test(text); + return /no\s+numbers\s+available\s+across|no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+within\s+(?:maxprice|price\s+range)|price\s+range\s+is\s+invalid|step\s*9:\s*(?:5sim|nexsms)\s+countries\s+are\s+empty|暂无可用号码|均无可用号码|无可用号码|价格区间|未选择国家|\bNO_NUMBERS\b/i.test(text); } function resolveNoSupplyDiagnosticsContext(state = {}, providerOrder = []) { @@ -2208,7 +2397,7 @@ const providerOrderText = context.order.join(' > '); const suggestion = formatNoSupplySuggestion(context); await addLog( - `Step 9 diagnostics: 无号连续失败 ${nextStreak} 次;priceRange=${priceRangeText};minPrice=${minPriceText};maxPrice=${maxPriceText};providerOrder=${providerOrderText};国家数 HeroSMS=${context.heroCountryCount}, 5sim=${context.fiveSimCountryCount}, NexSMS=${context.nexSmsCountryCount}。建议:${suggestion}。`, + `步骤 9 诊断:无号连续失败 ${nextStreak} 次;价格区间=${priceRangeText};最低价=${minPriceText};最高价=${maxPriceText};平台顺序=${providerOrderText};国家数 HeroSMS=${context.heroCountryCount}, 5sim=${context.fiveSimCountryCount}, NexSMS=${context.nexSmsCountryCount}。建议:${suggestion}。`, nextStreak >= 2 ? 'warn' : 'info' ); return true; @@ -2333,12 +2522,12 @@ ) { if (userLimit !== null && updatedMaxPrice > userLimit) { throw new Error( - `HeroSMS ${action} failed: WRONG_MAX_PRICE requires ${updatedMaxPrice}, which exceeds configured maxPrice=${userLimit}.` + `HeroSMS ${formatHeroSmsActionName(action)}失败:价格上限过低,平台要求至少 ${updatedMaxPrice},已超过当前配置的价格上限 ${userLimit}。` ); } if (userMinLimit !== null && updatedMaxPrice < userMinLimit) { throw new Error( - `HeroSMS ${action} failed: WRONG_MAX_PRICE requires ${updatedMaxPrice}, which is below configured minPrice=${userMinLimit}.` + `HeroSMS ${formatHeroSmsActionName(action)}失败:平台要求价格 ${updatedMaxPrice} 低于当前配置的最低购买价 ${userMinLimit}。` ); } nextMaxPrice = updatedMaxPrice; @@ -2543,7 +2732,7 @@ ? config.countryCandidates : []; if (!allCountryCandidates.length) { - throw new Error(`Step ${getActivePhoneVerificationVisibleStep()}: 5sim countries are empty. Please select at least one country in 接码设置。`); + throw new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:5sim 未选择国家,请先在接码设置中至少选择 1 个国家。`); } const blockedCountryIds = new Set( (Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : []) @@ -2557,7 +2746,7 @@ countryCandidates = allCountryCandidates; if (blockedCountryIds.size) { await addLog( - 'Step 9: all selected countries reached the temporary SMS-failure skip threshold, lifting skip for this acquire round.', + '步骤 9:已选国家均达到临时收码失败跳过阈值,本轮解除跳过并重新尝试。', 'warn' ); } @@ -2566,7 +2755,7 @@ const priceRange = resolvePhonePriceRange(state, PHONE_SMS_PROVIDER_5SIM); if (priceRange.invalidRange) { throw new Error( - `5sim price range is invalid: minPrice=${priceRange.minPriceLimit} exceeds maxPrice=${priceRange.maxPriceLimit}.` + `5sim 价格区间无效:最低购买价 ${priceRange.minPriceLimit} 高于价格上限 ${priceRange.maxPriceLimit}。` ); } const maxPriceLimit = priceRange.maxPriceLimit; @@ -2589,7 +2778,7 @@ for (let round = 1; round <= maxAcquireRounds; round += 1) { if (maxAcquireRounds > 1) { await addLog( - `Step 9: 5sim acquiring phone number (round ${round}/${maxAcquireRounds})...`, + `步骤 9:5sim 正在获取手机号(第 ${round}/${maxAcquireRounds} 轮)...`, 'info' ); } @@ -2629,16 +2818,16 @@ return left.index - right.index; }); orderedCountryCandidates = rankedCandidates.map((entry) => entry.countryConfig); - const rankedSummary = rankedCandidates - .map((entry) => { - const countryCode = normalizeFiveSimCountryCode(entry.countryConfig.code || entry.countryConfig.id || '', 'thailand'); - const countryLabel = String(entry.countryConfig.label || countryCode).trim() || countryCode; - return Number.isFinite(entry.lowestPrice) - ? `${countryLabel}:${entry.lowestPrice}` - : `${countryLabel}:n/a`; - }) + const rankedSummary = rankedCandidates + .map((entry) => { + const countryCode = normalizeFiveSimCountryCode(entry.countryConfig.code || entry.countryConfig.id || '', 'thailand'); + const countryLabel = String(entry.countryConfig.label || countryCode).trim() || countryCode; + return Number.isFinite(entry.lowestPrice) + ? `${countryLabel}:${entry.lowestPrice}` + : `${countryLabel}:无`; + }) .join(' | '); - await addLog(`Step 9: 5sim price-priority ranking: ${rankedSummary}`, 'info'); + await addLog(`步骤 9:5sim 价格优先排序:${rankedSummary}`, 'info'); } for (const countryConfig of orderedCountryCandidates) { @@ -2752,7 +2941,7 @@ && rawPriceCandidates.length ) { noNumbersByCountry.push( - `${countryLabel}: no numbers within price range ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)}; visible tiers=${rawPriceCandidates.join(', ')}` + `${countryLabel}: 价格区间 ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)} 内暂无可用号码;可见档位=${rawPriceCandidates.join(', ')}` ); } else if ( maxPriceLimit !== null @@ -2760,18 +2949,18 @@ && Number(lowestCatalog) > Number(maxPriceLimit) ) { noNumbersByCountry.push( - `${countryLabel}: no numbers within maxPrice=${maxPriceLimit}; lowest listed=${lowestCatalog}` + `${countryLabel}: 价格上限 ${maxPriceLimit} 内暂无可用号码;平台最低价=${lowestCatalog}` ); } else if (countryPriceFloor !== null && rangeFilteredPriceCandidates.length) { noNumbersByCountry.push( - `${countryLabel}: no higher price tier above ${countryPriceFloor} for current fallback attempt` + `${countryLabel}: 当前回退尝试没有高于 ${countryPriceFloor} 的价格档位` ); } else if (rawPriceCandidates.length) { const tierText = rawPriceCandidates.join(', '); - noNumbersByCountry.push(`${countryLabel}: all visible price tiers unavailable (${tierText})`); + noNumbersByCountry.push(`${countryLabel}: 可见价格档位均不可用(${tierText})`); retryableNoNumberCountries.push(countryLabel); } else { - noNumbersByCountry.push(`${countryLabel}: no free phones`); + noNumbersByCountry.push(`${countryLabel}: 暂无可用号码`); retryableNoNumberCountries.push(countryLabel); } continue; @@ -2809,23 +2998,23 @@ continue; } if (isFiveSimNoNumbersError(payload)) { - countryNoNumbersText = payloadText || countryNoNumbersText || 'no free phones'; + countryNoNumbersText = payloadText || countryNoNumbersText || '暂无可用号码'; continue; } if (isFiveSimTerminalError(payload)) { - throw new Error(`5sim buy activation failed: ${payloadText || 'empty response'}`); + throw createPhoneSmsActionFailureError('5sim buy activation', payloadText || 'empty response'); } - lastError = new Error(`5sim buy activation failed: ${payloadText || 'empty response'}`); + lastError = createPhoneSmsActionFailureError('5sim buy activation', payloadText || 'empty response'); } catch (error) { if (isFiveSimRateLimitError(error?.payload || error?.message, error?.status)) { countryNoNumbersText = describeFiveSimPayload(error?.payload || error?.message) || countryNoNumbersText || 'rate limit'; continue; } if (isFiveSimTerminalError(error?.payload || error?.message, error?.status)) { - throw new Error(`5sim buy activation failed: ${describeFiveSimPayload(error?.payload || error?.message) || 'unknown terminal error'}`); + throw createPhoneSmsActionFailureError('5sim buy activation', describeFiveSimPayload(error?.payload || error?.message) || 'unknown terminal error', error?.payload, error?.status); } if (isFiveSimNoNumbersError(error?.payload || error?.message)) { - countryNoNumbersText = describeFiveSimPayload(error?.payload || error?.message) || countryNoNumbersText || 'no free phones'; + countryNoNumbersText = describeFiveSimPayload(error?.payload || error?.message) || countryNoNumbersText || '暂无可用号码'; continue; } lastError = error; @@ -2845,16 +3034,16 @@ && rawPriceCandidates.length ) { noNumbersByCountry.push( - `${countryLabel}: no numbers within price range ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)}; visible tiers=${rawPriceCandidates.join(', ')}` + `${countryLabel}: 价格区间 ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)} 内暂无可用号码;可见档位=${rawPriceCandidates.join(', ')}` ); } else if (maxPriceLimit !== null && lowestCatalogPrice !== null && Number(lowestCatalogPrice) > Number(maxPriceLimit)) { noNumbersByCountry.push( - `${countryLabel}: no numbers within maxPrice=${maxPriceLimit}; lowest listed=${lowestCatalogPrice}` + `${countryLabel}: 价格上限 ${maxPriceLimit} 内暂无可用号码;平台最低价=${lowestCatalogPrice}` ); } else if (isFiveSimRateLimitError(countryNoNumbersText)) { rateLimitByCountry.push(`${countryLabel}: ${countryNoNumbersText || 'rate limit'}`); } else { - noNumbersByCountry.push(`${countryLabel}: ${countryNoNumbersText || 'no free phones'}`); + noNumbersByCountry.push(`${countryLabel}: ${countryNoNumbersText || '暂无可用号码'}`); retryableNoNumberCountries.push(countryLabel); } continue; @@ -2864,7 +3053,7 @@ continue; } if (isFiveSimTerminalError(error?.payload || error?.message, error?.status)) { - throw new Error(`5sim buy activation failed: ${describeFiveSimPayload(error?.payload || error?.message) || 'unknown terminal error'}`); + throw createPhoneSmsActionFailureError('5sim buy activation', describeFiveSimPayload(error?.payload || error?.message) || 'unknown terminal error', error?.payload, error?.status); } if (isFiveSimNoNumbersError(error?.payload || error?.message)) { const lowestRangePrice = await resolveFiveSimLowestPrice(config, countryCode, priceRange); @@ -2873,14 +3062,14 @@ : lowestRangePrice; if (minPriceLimit !== null && lowestRangePrice === null) { noNumbersByCountry.push( - `${countryLabel}: no numbers within price range ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)}` + `${countryLabel}: 价格区间 ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)} 内暂无可用号码` ); } else if (maxPriceLimit !== null && lowestCatalogPrice !== null && lowestCatalogPrice > maxPriceLimit) { noNumbersByCountry.push( - `${countryLabel}: no numbers within maxPrice=${maxPriceLimit}; lowest listed=${lowestCatalogPrice}` + `${countryLabel}: 价格上限 ${maxPriceLimit} 内暂无可用号码;平台最低价=${lowestCatalogPrice}` ); } else { - noNumbersByCountry.push(`${countryLabel}: ${describeFiveSimPayload(error?.payload || error?.message) || 'no free phones'}`); + noNumbersByCountry.push(`${countryLabel}: ${describeFiveSimPayload(error?.payload || error?.message) || '暂无可用号码'}`); retryableNoNumberCountries.push(countryLabel); } continue; @@ -2903,7 +3092,7 @@ && retryableNoNumberCountries.length > 0 ) { await addLog( - `Step 9: 5sim has no available numbers (round ${round}/${maxAcquireRounds}); retrying in ${Math.ceil(retryDelayMs / 1000)}s. Countries: ${retryableNoNumberCountries.join(', ')}.`, + `步骤 9:5sim 暂无可用号码(第 ${round}/${maxAcquireRounds} 轮);${Math.ceil(retryDelayMs / 1000)} 秒后重试。国家:${retryableNoNumberCountries.join(', ')}。`, 'warn' ); await sleepWithStop(retryDelayMs); @@ -2915,7 +3104,7 @@ if (finalNoNumbersByCountry.length) { throw new Error( - `5sim no numbers available across ${countryCandidates.length} country candidate(s): ${finalNoNumbersByCountry.join(' | ')}.` + `5sim 已尝试 ${countryCandidates.length} 个候选国家,均无可用号码:${finalNoNumbersByCountry.join(' | ')}。` ); } if (finalRateLimitByCountry.length) { @@ -2924,7 +3113,7 @@ if (finalLastError) { throw finalLastError; } - throw new Error('5sim failed to acquire a phone number.'); + throw new Error('5sim 获取手机号失败。'); } function isNexSmsNoNumbersError(payloadOrMessage) { @@ -2974,7 +3163,7 @@ async function resolveNexSmsCountryPricePlan(config, countryConfig, state = {}) { const countryId = normalizeNexSmsCountryId(countryConfig?.id, -1); if (countryId < 0) { - throw new Error(`NexSMS countryId is invalid: ${countryConfig?.id}`); + throw new Error(`NexSMS 国家 ID 无效:${countryConfig?.id}`); } const payload = await fetchNexSmsPayload( config, @@ -2988,7 +3177,7 @@ } ); if (!isNexSmsSuccessPayload(payload)) { - throw new Error(`NexSMS getCountryByService failed: ${describeNexSmsPayload(payload) || 'empty response'}`); + throw createPhoneSmsActionFailureError('NexSMS getCountryByService', describeNexSmsPayload(payload) || 'empty response'); } const countryData = (payload && typeof payload === 'object' && !Array.isArray(payload)) ? (payload.data || {}) @@ -3072,7 +3261,7 @@ ? config.countryCandidates : resolveNexSmsCountryCandidates(state); if (!allCountryCandidates.length) { - throw new Error(`Step ${getActivePhoneVerificationVisibleStep()}: NexSMS countries are empty. Please select at least one country in 接码设置。`); + throw new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:NexSMS 未选择国家,请先在接码设置中至少选择 1 个国家。`); } const blockedCountryIds = new Set( (Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : []) @@ -3087,7 +3276,7 @@ countryCandidates = allCountryCandidates; if (blockedCountryIds.size) { await addLog( - 'Step 9: all selected countries reached the temporary SMS-failure skip threshold, lifting skip for this acquire round.', + '步骤 9:已选国家均达到临时收码失败跳过阈值,本轮解除跳过并重新尝试。', 'warn' ); } @@ -3097,7 +3286,7 @@ const priceRange = resolvePhonePriceRange(state, PHONE_SMS_PROVIDER_NEXSMS); if (priceRange.invalidRange) { throw new Error( - `NexSMS price range is invalid: minPrice=${priceRange.minPriceLimit} exceeds maxPrice=${priceRange.maxPriceLimit}.` + `NexSMS 价格区间无效:最低购买价 ${priceRange.minPriceLimit} 高于价格上限 ${priceRange.maxPriceLimit}。` ); } const minPriceLimit = priceRange.minPriceLimit; @@ -3117,7 +3306,7 @@ for (let round = 1; round <= maxAcquireRounds; round += 1) { if (maxAcquireRounds > 1) { await addLog( - `Step 9: NexSMS acquiring phone number (round ${round}/${maxAcquireRounds})...`, + `步骤 9:NexSMS 正在获取手机号(第 ${round}/${maxAcquireRounds} 轮)...`, 'info' ); } @@ -3171,9 +3360,9 @@ const label = String(attempt.countryConfig.label || `Country #${id}`).trim() || `Country #${id}`; return Number.isFinite(attempt.orderingPrice) ? `${label}:${attempt.orderingPrice}` - : `${label}:n/a`; + : `${label}:无`; }).join(' | '); - await addLog(`Step 9: NexSMS price-priority ranking: ${rankingSummary}`, 'info'); + await addLog(`步骤 9:NexSMS 价格优先排序:${rankingSummary}`, 'info'); } const noNumbersByCountry = []; @@ -3190,7 +3379,7 @@ pricePlan = await resolveNexSmsCountryPricePlan(config, attempt.countryConfig, state); } catch (error) { if (isNexSmsTerminalError(error?.payload || error?.message, error?.status)) { - throw new Error(`NexSMS price lookup failed: ${describeNexSmsPayload(error?.payload || error?.message) || 'unknown terminal error'}`); + throw createPhoneSmsActionFailureError('NexSMS price lookup', describeNexSmsPayload(error?.payload || error?.message) || 'unknown terminal error', error?.payload, error?.status); } lastError = error; continue; @@ -3204,10 +3393,10 @@ && pricePlan.minCatalogPrice > pricePlan.userLimit ) { noNumbersByCountry.push( - `${countryLabel}: no numbers within maxPrice=${pricePlan.userLimit}; lowest listed=${pricePlan.minCatalogPrice}` + `${countryLabel}: 价格上限 ${pricePlan.userLimit} 内暂无可用号码;平台最低价=${pricePlan.minCatalogPrice}` ); } else { - const reason = describeNexSmsPayload(pricePlan.rawPayload) || 'no price candidates'; + const reason = describeNexSmsPayload(pricePlan.rawPayload) || '无可用价格档位'; noNumbersByCountry.push(`${countryLabel}: ${reason}`); retryableNoNumberCountries.push(countryLabel); } @@ -3245,7 +3434,7 @@ if (!pricesToTry.length) { if (priceRange.hasMinPriceLimit && !rangeFilteredPrices.length) { noNumbersByCountry.push( - `${countryLabel}: no numbers within price range ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)}` + `${countryLabel}: 价格区间 ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)} 内暂无可用号码` ); continue; } @@ -3255,10 +3444,10 @@ && pricePlan.prices.length > 0 ) { noNumbersByCountry.push( - `${countryLabel}: no higher price tier above ${countryPriceFloor} for current fallback attempt` + `${countryLabel}: 当前回退尝试没有高于 ${countryPriceFloor} 的价格档位` ); } else { - noNumbersByCountry.push(`${countryLabel}: ${describeNexSmsPayload(pricePlan.rawPayload) || 'no numbers found'}`); + noNumbersByCountry.push(`${countryLabel}: ${describeNexSmsPayload(pricePlan.rawPayload) || '暂无可用号码'}`); retryableNoNumberCountries.push(countryLabel); } continue; @@ -3284,9 +3473,9 @@ continue; } if (isNexSmsTerminalError(payload)) { - throw new Error(`NexSMS purchase failed: ${describeNexSmsPayload(payload) || 'empty response'}`); + throw createPhoneSmsActionFailureError('NexSMS purchase', describeNexSmsPayload(payload) || 'empty response'); } - lastError = new Error(`NexSMS purchase failed: ${describeNexSmsPayload(payload) || 'empty response'}`); + lastError = createPhoneSmsActionFailureError('NexSMS purchase', describeNexSmsPayload(payload) || 'empty response'); continue; } const activation = parseNexSmsActivationPayload(payload, { @@ -3295,7 +3484,7 @@ serviceCode: config.serviceCode, }); if (!activation) { - lastError = new Error('NexSMS purchase succeeded but did not return a phone number.'); + lastError = new Error('NexSMS 购买成功,但未返回手机号。'); continue; } const numericPrice = Number(price); @@ -3303,7 +3492,7 @@ return activation; } catch (error) { if (isNexSmsTerminalError(error?.payload || error?.message, error?.status)) { - throw new Error(`NexSMS purchase failed: ${describeNexSmsPayload(error?.payload || error?.message) || 'unknown terminal error'}`); + throw createPhoneSmsActionFailureError('NexSMS purchase', describeNexSmsPayload(error?.payload || error?.message) || 'unknown terminal error', error?.payload, error?.status); } if (isNexSmsNoNumbersError(error?.payload || error?.message)) { continue; @@ -3312,7 +3501,7 @@ } } - const fallbackReason = describeNexSmsPayload(pricePlan.rawPayload) || 'no numbers found'; + const fallbackReason = describeNexSmsPayload(pricePlan.rawPayload) || '暂无可用号码'; noNumbersByCountry.push(`${countryLabel}: ${fallbackReason}`); retryableNoNumberCountries.push(countryLabel); } @@ -3326,7 +3515,7 @@ && retryableNoNumberCountries.length > 0 ) { await addLog( - `Step 9: NexSMS has no available numbers (round ${round}/${maxAcquireRounds}); retrying in ${Math.ceil(retryDelayMs / 1000)}s. Countries: ${retryableNoNumberCountries.join(', ')}.`, + `步骤 9:NexSMS 暂无可用号码(第 ${round}/${maxAcquireRounds} 轮);${Math.ceil(retryDelayMs / 1000)} 秒后重试。国家:${retryableNoNumberCountries.join(', ')}。`, 'warn' ); await sleepWithStop(retryDelayMs); @@ -3338,13 +3527,13 @@ if (finalNoNumbersByCountry.length) { throw new Error( - `NexSMS no numbers available across ${countryCandidates.length} country candidate(s): ${finalNoNumbersByCountry.join(' | ')}.` + `NexSMS 已尝试 ${countryCandidates.length} 个候选国家,均无可用号码:${finalNoNumbersByCountry.join(' | ')}。` ); } if (finalLastError) { throw finalLastError; } - throw new Error('NexSMS failed to acquire a phone number.'); + throw new Error('NexSMS 获取手机号失败。'); } async function requestPhoneActivation(state = {}, options = {}) { @@ -3365,7 +3554,7 @@ ? config.countryCandidates : resolveCountryCandidates(state); if (!allCountryCandidates.length) { - throw new Error(`Step ${getActivePhoneVerificationVisibleStep()}: HeroSMS countries are empty. Please select at least one country in 接码设置。`); + throw new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:HeroSMS 未选择国家,请先在接码设置中至少选择 1 个国家。`); } const blockedCountryIds = new Set( (Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : []) @@ -3388,7 +3577,7 @@ const priceRange = resolvePhonePriceRange(state, PHONE_SMS_PROVIDER_HERO); if (priceRange.invalidRange) { throw new Error( - `HeroSMS price range is invalid: minPrice=${priceRange.minPriceLimit} exceeds maxPrice=${priceRange.maxPriceLimit}.` + `HeroSMS 价格区间无效:最低购买价 ${priceRange.minPriceLimit} 高于价格上限 ${priceRange.maxPriceLimit}。` ); } const minPriceLimit = priceRange.minPriceLimit; @@ -3523,26 +3712,26 @@ : (floorFilteredPrices.length ? floorFilteredPrices : candidatePrices); const rawTierText = Array.isArray(pricePlan?.prices) && pricePlan.prices.length ? pricePlan.prices - .map((value) => (value === null || value === undefined ? 'auto' : String(value))) + .map((value) => (value === null || value === undefined ? '自动' : String(value))) .join(', ') - : 'none'; + : '无'; await addLog( - `Step 9: HeroSMS ${countryConfig.label} price plan resolved -> tiers=[${rawTierText}], userLimit=${pricePlan?.userLimit ?? 'none'}, minCatalog=${pricePlan?.minCatalogPrice ?? 'n/a'}.`, + `步骤 9:HeroSMS ${countryConfig.label} 价格方案:档位=[${rawTierText}],用户上限=${pricePlan?.userLimit ?? '未设置'},目录最低价=${pricePlan?.minCatalogPrice ?? '未知'}。`, 'info' ); if (pricesToTry.length > 1 || countryPriceFloor !== null) { const tierText = pricesToTry - .map((value) => (value === null || value === undefined ? 'auto' : String(value))) + .map((value) => (value === null || value === undefined ? '自动' : String(value))) .join(', '); await addLog( - `Step 9: HeroSMS ${countryConfig.label} price candidates: ${tierText}${countryPriceFloor !== null ? ` (floor>${countryPriceFloor})` : ''}.`, + `步骤 9:HeroSMS ${countryConfig.label} 本轮候选价格:${tierText}${countryPriceFloor !== null ? `(高于 ${countryPriceFloor})` : ''}。`, 'info' ); } if (!pricesToTry.length) { if (priceRange.hasMinPriceLimit && !rangeFilteredPrices.length) { noNumbersByCountry.push( - `${countryConfig.label}: no numbers within price range ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)}` + `${countryConfig.label}: 价格区间 ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)} 内暂无可用号码` ); continue; } @@ -3552,7 +3741,7 @@ && pricePlan.prices.length > 0 ) { noNumbersByCountry.push( - `${countryConfig.label}: no higher price tier above ${countryPriceFloor} for current fallback attempt` + `${countryConfig.label}: 当前回退尝试没有高于 ${countryPriceFloor} 的价格档位` ); continue; } @@ -3562,7 +3751,7 @@ && pricePlan.minCatalogPrice > pricePlan.userLimit ) { noNumbersByCountry.push( - `${countryConfig.label}: no numbers within maxPrice=${pricePlan.userLimit}; lowest listed=${pricePlan.minCatalogPrice}` + `${countryConfig.label}: 价格上限 ${pricePlan.userLimit} 内暂无可用号码;平台最低价=${pricePlan.minCatalogPrice}` ); } else { noNumbersByCountry.push( @@ -3577,7 +3766,7 @@ try { const fixedPrice = !Boolean(pricePlan.syntheticUserLimitProbe); await addLog( - `Step 9: HeroSMS ${countryConfig.label} trying ${requestAction} at tier ${maxPrice === null || maxPrice === undefined ? 'auto' : maxPrice}.`, + `步骤 9:HeroSMS ${countryConfig.label} 正在尝试${formatHeroSmsActionName(requestAction)},价格档位 ${maxPrice === null || maxPrice === undefined ? '自动' : maxPrice}。`, 'info' ); const payload = await requestPhoneActivationWithPrice( @@ -3607,14 +3796,17 @@ continue; } if (isHeroSmsTerminalError(payload)) { - throw new Error(`HeroSMS ${requestAction} failed: ${payloadText || 'empty response'}`); + throw createHeroSmsActionFailureError(requestAction, payloadText || 'empty response'); } lastFailureText = payloadText || lastFailureText; - lastError = new Error(`HeroSMS ${requestAction} failed: ${payloadText || 'empty response'}`); + lastError = createHeroSmsActionFailureError(requestAction, payloadText || 'empty response'); } catch (error) { + if (error?.localizedPhoneSmsFailure) { + throw error; + } const payloadOrMessage = error?.payload || error?.message; if (isHeroSmsTerminalError(payloadOrMessage)) { - throw new Error(`HeroSMS ${requestAction} failed: ${describeHeroSmsPayload(payloadOrMessage) || 'empty response'}`); + throw createHeroSmsActionFailureError(requestAction, payloadOrMessage || 'empty response'); } if (isHeroSmsNoNumbersPayload(payloadOrMessage)) { noNumbersObservedInCountry = true; @@ -3629,7 +3821,7 @@ if (noNumbersObservedInCountry) { const tiersTriedText = pricesToTry - .map((value) => (value === null || value === undefined ? 'auto' : String(value))) + .map((value) => (value === null || value === undefined ? '自动' : String(value))) .join(', '); if ( pricePlan.userLimit !== null @@ -3637,11 +3829,11 @@ && pricePlan.minCatalogPrice > pricePlan.userLimit ) { noNumbersByCountry.push( - `${countryConfig.label}: no numbers within maxPrice=${pricePlan.userLimit}; lowest listed=${pricePlan.minCatalogPrice}` + `${countryConfig.label}: 价格上限 ${pricePlan.userLimit} 内暂无可用号码;平台最低价=${pricePlan.minCatalogPrice}` ); } else { noNumbersByCountry.push( - `${countryConfig.label}: ${lastFailureText || 'NO_NUMBERS'}${tiersTriedText ? ` (tiers tried: ${tiersTriedText})` : ''}` + `${countryConfig.label}: ${lastFailureText || 'NO_NUMBERS'}${tiersTriedText ? `(已尝试档位:${tiersTriedText})` : ''}` ); retryableNoNumberCountries.push(countryConfig.label); } @@ -3676,7 +3868,6 @@ if (finalNoNumbersByCountry.length) { throw new Error( `HeroSMS 已尝试 ${countryCandidates.length} 个候选国家,均无可用号码:${finalNoNumbersByCountry.join(' | ')}。` - + ` HeroSMS no numbers available across ${countryCandidates.length} country candidate(s): ${finalNoNumbersByCountry.join(' | ')}.` ); } if (finalLastError) { @@ -3705,7 +3896,7 @@ ); const reuseNumber = String(normalizedActivation.phoneNumber || '').replace(/[^\d]/g, ''); if (!reuseNumber) { - throw new Error('5sim reuse activation failed: phone number is missing.'); + throw new Error('5sim 复用手机号失败:手机号缺失。'); } const payload = await fetchFiveSimPayload( config, @@ -3715,12 +3906,12 @@ const nextActivation = parseFiveSimActivationPayload(payload, normalizedActivation); if (!nextActivation) { const text = describeFiveSimPayload(payload); - throw new Error(`5sim reuse activation failed: ${text || 'empty response'}`); + throw createPhoneSmsActionFailureError('5sim reuse activation', text || 'empty response'); } return nextActivation; } if (config.provider === PHONE_SMS_PROVIDER_NEXSMS) { - throw new Error('NexSMS does not support activation reuse for this flow.'); + throw new Error('NexSMS 当前流程不支持复用手机号订单。'); } const payload = await fetchHeroSmsPayload(config, { action: 'reactivate', @@ -3775,7 +3966,7 @@ } ); if (!isNexSmsSuccessPayload(payload)) { - throw new Error(`NexSMS close activation failed: ${describeNexSmsPayload(payload) || 'empty response'}`); + throw createPhoneSmsActionFailureError('NexSMS close activation', describeNexSmsPayload(payload) || 'empty response'); } return describeNexSmsPayload(payload); } @@ -3934,14 +4125,14 @@ return { ok: false, reason: 'missing_free_reusable_activation', - message: 'Free reusable phone activation is missing.', + message: '免费复用手机号激活记录缺失。', }; } if (!String(normalizedActivation.activationId || '').trim()) { return { ok: false, reason: 'missing_activation_id', - message: 'Saved free reusable phone has no HeroSMS activation ID; automatic free reuse cannot reactivate it.', + message: '已保存的免费复用手机号缺少 HeroSMS 激活 ID,无法自动重新激活。', }; } @@ -3963,13 +4154,13 @@ { ...state, phoneSmsProvider: PHONE_SMS_PROVIDER_HERO }, normalizedActivation, 3, - 'HeroSMS setStatus(3) for automatic free reuse' + 'HeroSMS 自动复用设置订单状态' ); } catch (error) { return { ok: false, reason: 'set_status_failed', - message: error.message || 'HeroSMS setStatus(3) failed.', + message: error.message || 'HeroSMS 更新订单状态失败。', lastStatus, prepareRound, }; @@ -3985,11 +4176,11 @@ const payload = await fetchHeroSmsPayload(config, { action: statusAction, id: normalizedActivation.activationId, - }, `HeroSMS ${statusAction} for automatic free reuse`); + }, `HeroSMS 自动复用${statusAction}`); const statusText = describeHeroSmsPayload(payload); lastStatus = statusText; await addLog( - `步骤 9:自动白嫖复用号码 ${normalizedActivation.phoneNumber} 状态:${statusText || 'empty response'}(${prepareRound}/${FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS})。`, + `步骤 9:自动白嫖复用号码 ${normalizedActivation.phoneNumber} 状态:${statusText || '空响应'}(${prepareRound}/${FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS})。`, 'info' ); @@ -4019,7 +4210,7 @@ return { ok: false, reason: 'activation_cancelled', - message: 'HeroSMS activation was cancelled before automatic free reuse.', + message: 'HeroSMS 订单在自动白嫖复用前已被取消。', lastStatus, prepareRound, }; @@ -4028,7 +4219,7 @@ return { ok: false, reason: 'get_status_failed', - message: error.message || 'HeroSMS getStatus failed.', + message: error.message || 'HeroSMS 查询短信状态失败。', lastStatus, prepareRound, }; @@ -4038,7 +4229,7 @@ return { ok: false, reason: 'prepare_timeout', - message: `Timed out waiting for saved phone to enter SMS waiting state. Last status: ${lastStatus || 'unknown'}.`, + message: `等待已保存手机号进入短信等待状态超时。最后状态:${lastStatus || '未知'}。`, lastStatus, prepareRound, }; @@ -4135,10 +4326,10 @@ } if (/^(CANCELED|CANCELLED|BANNED|FINISHED|EXPIRED|TIMEOUT)$/i.test(statusText)) { - throw new Error(`5sim activation ended before receiving SMS: ${statusText}`); + throw new Error(`5sim 订单在收到短信前已结束:${statusText}`); } - throw new Error(`5sim check activation failed: ${text || statusText || 'empty response'}`); + throw createPhoneSmsActionFailureError('5sim check activation', text || statusText || 'empty response'); } throw buildPhoneCodeTimeoutError(lastResponse); @@ -4191,7 +4382,7 @@ continue; } if (isNexSmsTerminalError(payload)) { - throw new Error(`NexSMS get sms messages failed: ${text || 'unknown terminal error'}`); + throw createPhoneSmsActionFailureError('NexSMS get sms messages', text || 'unknown terminal error'); } await emitWaitingForCode(text || 'PENDING'); await sleepWithStop(intervalMs); @@ -4260,10 +4451,10 @@ } if (/^STATUS_CANCEL$/i.test(text)) { - throw new Error('HeroSMS activation was cancelled before the SMS arrived.'); + throw new Error('HeroSMS 订单在短信到达前已被取消。'); } - throw new Error(`HeroSMS ${statusAction} failed: ${text || 'empty response'}`); + throw createHeroSmsActionFailureError(statusAction, text || 'empty response'); } throw buildPhoneCodeTimeoutError(lastResponse); @@ -4808,7 +4999,7 @@ (provider === PHONE_SMS_PROVIDER_5SIM || provider === PHONE_SMS_PROVIDER_NEXSMS) && !countryCandidates.length ) { - throw new Error(`Step ${getActivePhoneVerificationVisibleStep()}: ${provider === PHONE_SMS_PROVIDER_5SIM ? '5sim' : 'NexSMS'} countries are empty. Please select at least one country in 接码设置。`); + throw new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:${provider === PHONE_SMS_PROVIDER_5SIM ? '5sim' : 'NexSMS'} 未选择国家,请先在接码设置中至少选择 1 个国家。`); } const normalizeCountryKey = (value) => ( provider === PHONE_SMS_PROVIDER_5SIM @@ -4987,9 +5178,9 @@ const providerLabel = getPhoneSmsProviderLabel(providerCandidate); if ( providerCandidate !== provider - && /step\s*9:\s*(?:5sim|nexsms)\s+countries\s+are\s+empty/i.test(providerErrorMessage) + && /(?:step|步骤)\s*9\s*[::]\s*(?:5sim|nexsms).*(?:countries\s+are\s+empty|未选择国家)/i.test(providerErrorMessage) ) { - skippedFallbackProviders.push(`${providerLabel}: countries are empty`); + skippedFallbackProviders.push(`${providerLabel}:未选择国家`); await addLog( `步骤 9:跳过回退接码平台 ${providerLabel},因为接码设置中未选择国家。`, 'warn' @@ -4997,18 +5188,18 @@ continue; } lastProviderError = error; - providerErrors.push(`${providerCandidate}: ${providerErrorMessage}`); + providerErrors.push(`${providerLabel}:${formatProviderAcquireFailure(providerCandidate, providerErrorMessage)}`); } } if (providerErrors.length) { await logNoSupplyDiagnostics(state, providerOrder, providerErrors); const skippedSuffix = skippedFallbackProviders.length - ? ` | skipped fallback providers: ${skippedFallbackProviders.join('; ')}` + ? `;已跳过回退平台:${skippedFallbackProviders.join(';')}` : ''; - throw new Error(`Step ${getActivePhoneVerificationVisibleStep()}: all provider candidates failed to acquire number. ${providerErrors.join(' | ')}${skippedSuffix}`); + throw new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:所有接码平台候选均未获取到手机号。${providerErrors.join(';')}${skippedSuffix}`); } - throw lastProviderError || new Error(`Step ${getActivePhoneVerificationVisibleStep()}: failed to acquire phone activation.`); + throw lastProviderError || new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:获取手机号订单失败。`); } async function prepareSignupPhoneActivation(state = {}, options = {}) { @@ -5293,7 +5484,7 @@ if (/^STATUS_(WAIT_CODE|WAIT_RETRY|WAIT_RESEND)(?::.+)?$/i.test(String(statusText || '').trim())) { const pageError = await checkPhoneResendPageError(tabId, state); if (pageError?.reason === 'resend_phone_banned') { - throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${pageError.message || 'OpenAI could not send SMS to this phone number.'}`); + throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${pageError.message || 'OpenAI 无法向此手机号发送短信。'}`); } if (pageError?.reason === 'phone_max_usage_exceeded') { throw buildPhoneMaxUsageExceededError(pageError.message); @@ -5392,7 +5583,7 @@ } if (isPhoneActivationOrderMissingError(error, normalizedActivation.provider)) { await addLog( - `Step 9: ${providerLabel} activation for ${normalizedActivation.phoneNumber} became invalid (${error.message || error}), replacing number immediately.`, + `步骤 9:${providerLabel} 号码 ${normalizedActivation.phoneNumber} 的接码订单已失效(${error.message || error}),立即更换号码。`, 'warn' ); await clearPhoneRuntimeCountdown(); @@ -5412,7 +5603,7 @@ ); if (!usePageResend) { await addLog( - `Step 9: ${providerLabel} keeps the same verification page session and skips page resend to avoid route 405 / resend throttling; continue polling this number.`, + `步骤 9:${providerLabel} 保持当前验证码页会话并跳过页面重发,避免触发 405 或重发限流;继续轮询当前号码。`, 'warn' ); continue; @@ -6171,7 +6362,7 @@ } catch (error) { snapshotError = error; await addLog( - `Step 9: failed to inspect auth page ${attemptLabel}. ${error.message}`, + `步骤 9:检查认证页状态失败(${attemptLabel})。${error.message}`, 'warn' ); snapshot = null; @@ -6194,7 +6385,7 @@ } catch (error) { returnError = error; await addLog( - `Step 9: failed to return to add-phone page ${attemptLabel}. ${error.message}`, + `步骤 9:返回添加手机号页面失败(${attemptLabel})。${error.message}`, 'warn' ); } @@ -6226,7 +6417,7 @@ } if (!latest?.addPhonePage) { throw new Error( - `Step 9: auth page is not on add-phone before phone submit (${attemptLabel}). URL: ${latest?.url || 'unknown'}` + `步骤 9:提交手机号前认证页未停留在添加手机号页面(${attemptLabel})。URL: ${latest?.url || 'unknown'}` ); } return latest; @@ -6339,7 +6530,7 @@ countryPriceFloorByKey.set(countryKey, normalizedFloor); const countryLabel = resolveCountryLabelByFailureKey(countryKey, normalizedActivation.provider); await addLog( - `Step 9: ${countryLabel} will try a higher price tier (> ${normalizedFloor}) due to ${reason || 'sms timeout'}.`, + `步骤 9:${countryLabel} 因 ${formatStep9Reason(reason || 'sms_timeout')} 将尝试更高价格档位(> ${normalizedFloor})。`, 'warn' ); }; @@ -6357,7 +6548,7 @@ } preferredActivationExhausted = true; await addLog( - `Step 9: preferred number ${activation.phoneNumber} failed (${reason || 'unknown reason'}), falling back to a new number.`, + `步骤 9:优先号码 ${activation.phoneNumber} 失败(${formatStep9Reason(reason || 'unknown')}),将改为获取新号码。`, 'warn' ); }; @@ -6369,7 +6560,7 @@ throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, failureCode || 'add_phone_rejected'); } await addLog( - `Step 9: replacing number after add-phone failure (${failureReason}) (${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts}).`, + `步骤 9:添加手机号失败后正在更换号码(${formatStep9Reason(failureReason)},${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, 'warn' ); if (shouldCancelActivation && activation) { @@ -6396,7 +6587,7 @@ }; } catch (returnError) { await addLog( - `Step 9: failed to return to add-phone page after rejection, will continue with best-effort state. ${returnError.message}`, + `步骤 9:号码被拒后返回添加手机号页面失败,将用当前可用状态继续。${returnError.message}`, 'warn' ); } @@ -6410,7 +6601,7 @@ }; } catch (verifyError) { await addLog( - `Step 9: failed to verify add-phone state after rejection. ${verifyError.message}`, + `步骤 9:号码被拒后确认添加手机号页面状态失败。${verifyError.message}`, 'warn' ); } diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 58afece..cf41913 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -2,8 +2,6 @@ root.MultiPageBackgroundStep8 = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() { const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000; - const STEP8_ADD_EMAIL_URL = 'https://auth.openai.com/add-email'; - const STEP8_CURRENT_STEP_RECOVERY_MAX_ATTEMPTS = 3; function createStep8Executor(deps = {}) { const { @@ -30,7 +28,6 @@ rerunStep7ForStep8Recovery, reuseOrCreateTab, sendToContentScriptResilient, - buildRegistrationEmailStateUpdates = null, persistRegistrationEmailState = null, phoneVerificationHelpers = null, setState, @@ -41,6 +38,7 @@ throwIfStopped, } = deps; let activeFetchLoginCodeStep = null; + let activeFetchLoginCodeStepKey = 'fetch-login-code'; function normalizeLogStep(value) { const step = Math.floor(Number(value) || 0); @@ -61,7 +59,7 @@ if (step) { normalizedOptions.step = step; if (!normalizedOptions.stepKey) { - normalizedOptions.stepKey = 'fetch-login-code'; + normalizedOptions.stepKey = activeFetchLoginCodeStepKey || 'fetch-login-code'; } } delete normalizedOptions.visibleStep; @@ -73,6 +71,27 @@ return visibleStep > 0 ? visibleStep : fallback; } + function normalizeSignupMethod(value = '') { + return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'; + } + + function normalizeIdentifierType(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'phone' || normalized === 'email' ? normalized : ''; + } + + function isPhoneLoginCodeMode(state = {}) { + if (normalizeIdentifierType(state?.accountIdentifierType) === 'phone') { + return true; + } + return normalizeSignupMethod(state?.resolvedSignupMethod || state?.signupMethod) === 'phone' + && Boolean( + String(state?.signupPhoneNumber || '').trim() + || String(state?.signupPhoneCompletedActivation?.phoneNumber || '').trim() + || String(state?.signupPhoneActivation?.phoneNumber || '').trim() + ); + } + function getAuthLoginStepForVisibleStep(visibleStep) { return visibleStep >= 11 ? 10 : 7; } @@ -173,7 +192,7 @@ retryDelayMs: 700, logMessage: `步骤 ${visibleStep}:添加邮箱页面正在切换,等待邮箱验证码页就绪...`, logStep: visibleStep, - logStepKey: 'fetch-login-code', + logStepKey: activeFetchLoginCodeStepKey || 'fetch-login-code', } ); @@ -185,7 +204,7 @@ let persistedState = latestState; if (typeof persistRegistrationEmailState === 'function') { await persistRegistrationEmailState(latestState, resolvedEmail, { - source: 'step8_add_email', + source: activeFetchLoginCodeStepKey === 'bind-email' ? 'bind_email' : 'step8_add_email', preserveAccountIdentity: true, }); persistedState = typeof getState === 'function' ? await getState() : latestState; @@ -234,73 +253,48 @@ } } + async function completeStep8WhenDeferredToPostLoginPhone(visibleStep, pageState = {}, options = {}) { + await setState({ + step8VerificationTargetEmail: '', + loginVerificationRequestedAt: null, + }); + await addLog( + `步骤 ${visibleStep}:当前认证页已进入手机号验证流程,跳过登录邮箱验证码,交给后续“手机号验证”步骤处理。`, + 'warn' + ); + if (typeof completeNodeFromBackground === 'function') { + await completeNodeFromBackground(options.nodeId || 'fetch-login-code', { + loginVerificationRequestedAt: null, + skipLoginVerificationStep: true, + addPhonePage: pageState?.state === 'add_phone_page' || Boolean(pageState?.addPhonePage), + phoneVerificationPage: pageState?.state === 'phone_verification_page' || Boolean(pageState?.phoneVerificationPage), + }); + } + } + + async function completeStep8WhenDeferredToBindEmail(visibleStep, options = {}) { + await setState({ + step8VerificationTargetEmail: '', + loginVerificationRequestedAt: null, + }); + await addLog( + `步骤 ${visibleStep}:当前认证页已进入添加邮箱页,跳过登录短信验证码,交给后续“绑定邮箱”步骤处理。`, + 'warn' + ); + if (typeof completeNodeFromBackground === 'function') { + await completeNodeFromBackground(options.nodeId || 'fetch-login-code', { + loginVerificationRequestedAt: null, + skipLoginVerificationStep: true, + addEmailPage: true, + }); + } + } + function isStep8AddPhoneStateError(error) { const message = String(error?.message || error || ''); return /add-phone|手机号页面|手机号验证页|phone[\s-_]verification|phone\s+number/i.test(message); } - function isStep8EmailInUseError(error) { - const message = String(error?.message || error || ''); - return /STEP8_EMAIL_IN_USE::|email_in_use|email\s+(?:address\s+)?already\s+exists|already\s+associated\s+with\s+this\s+email/i.test(message); - } - - function isStep8MaxCheckAttemptsError(error) { - const message = String(error?.message || error || ''); - return /AUTH_MAX_CHECK_ATTEMPTS::|max_check_attempts/i.test(message); - } - - async function openStep8AddEmailPage(state, visibleStep, reasonLabel = '') { - const tabId = typeof getTabId === 'function' ? await getTabId('signup-page') : 0; - const url = STEP8_ADD_EMAIL_URL; - if (tabId && chrome?.tabs?.update) { - await chrome.tabs.update(tabId, { url, active: true }); - } else if (typeof reuseOrCreateTab === 'function') { - await reuseOrCreateTab('signup-page', url); - } else { - throw new Error(`Step ${visibleStep}: cannot reopen add-email page for Step 8 recovery.`); - } - if (typeof sleepWithStop === 'function') { - await sleepWithStop(1000); - } - await addLog( - `步骤 ${visibleStep}:重新打开添加邮箱页面${reasonLabel ? `(${reasonLabel})` : ''}。`, - 'warn' - ); - return { - ...(state || {}), - oauthUrl: state?.oauthUrl || url, - }; - } - - async function resetStep8AfterEmailInUse(state, visibleStep) { - const currentEmail = String(state?.email || '').trim(); - const registrationEmailUpdates = typeof buildRegistrationEmailStateUpdates === 'function' - ? buildRegistrationEmailStateUpdates(state, { - currentEmail: null, - preservePrevious: true, - source: 'step8_recovery', - }) - : { email: null }; - await setState({ - ...registrationEmailUpdates, - step8VerificationTargetEmail: '', - loginVerificationRequestedAt: null, - }); - if (currentEmail) { - await addLog(`步骤 ${visibleStep}:检测到邮箱 ${currentEmail} 已被占用,已清理运行态并准备重新获取新邮箱。`, 'warn'); - } else { - await addLog(`步骤 ${visibleStep}:检测到邮箱已被占用,已清理运行态并准备重新获取新邮箱。`, 'warn'); - } - } - - async function resetStep8AfterMaxCheckAttempts(visibleStep) { - await setState({ - step8VerificationTargetEmail: '', - loginVerificationRequestedAt: null, - }); - await addLog(`步骤 ${visibleStep}:检测到 max_check_attempts,将重新开始当前添加邮箱步骤,不继续点击重试。`, 'warn'); - } - async function recoverStep8PollingFailure(currentState, visibleStep) { const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep); try { @@ -409,79 +403,142 @@ return result || {}; } - async function runStep8Attempt(state, runtime = {}) { - const visibleStep = getVisibleStep(state, 8); - activeFetchLoginCodeStep = visibleStep; + async function ensureAuthTabForPostLoginStep(state, visibleStep) { const authTabId = await getTabId('signup-page'); - if (authTabId) { await chrome.tabs.update(authTabId, { active: true }); - } else { - if (!state.oauthUrl) { - throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`); - } - await reuseOrCreateTab('signup-page', state.oauthUrl); + return authTabId; + } + if (!state?.oauthUrl) { + throw new Error(`步骤 ${visibleStep}:缺少登录用 OAuth 链接,请先完成刷新 OAuth 并登录。`); + } + return reuseOrCreateTab('signup-page', state.oauthUrl); + } + + async function completePostLoginPhoneVerificationSkippedOnOauth(visibleStep, options = {}) { + await addLog(`步骤 ${visibleStep}:当前认证页已进入 OAuth 授权页,跳过手机号验证步骤。`, 'warn', { + step: visibleStep, + stepKey: 'post-login-phone-verification', + }); + if (typeof completeNodeFromBackground === 'function') { + await completeNodeFromBackground(options.nodeId || 'post-login-phone-verification', { + directOAuthConsentPage: true, + phoneVerification: false, + }); + } + } + + async function executePostLoginPhoneVerification(state) { + const visibleStep = getVisibleStep(state, 9); + activeFetchLoginCodeStep = visibleStep; + activeFetchLoginCodeStepKey = 'post-login-phone-verification'; + const authTabId = await ensureAuthTabForPostLoginStep(state, visibleStep); + const pageState = await getLoginAuthStateFromContent(visibleStep, { + timeoutMs: await getStep8ReadyTimeoutMs('确认手机号验证页或 OAuth 授权页已就绪', state?.oauthUrl || '', visibleStep), + logMessage: `步骤 ${visibleStep}:正在确认是否需要手机号验证...`, + }); + + if (pageState?.state === 'oauth_consent_page') { + await completePostLoginPhoneVerificationSkippedOnOauth(visibleStep, { nodeId: state?.nodeId }); + return; + } + if (pageState?.state !== 'add_phone_page' && pageState?.state !== 'phone_verification_page') { + throw new Error(`步骤 ${visibleStep}:手机号验证步骤只处理添加手机号页或手机验证码页,当前状态:${pageState?.state || 'unknown'}。URL: ${pageState?.url || ''}`.trim()); + } + if (!state?.phoneVerificationEnabled) { + throw new Error(`步骤 ${visibleStep}:检测到需要手机号验证,但手机接码未开启。URL: ${pageState?.url || ''}`.trim()); + } + if (typeof phoneVerificationHelpers?.completePhoneVerificationFlow !== 'function') { + throw new Error(`步骤 ${visibleStep}:手机号验证流程不可用,接码模块尚未初始化。`); } - const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0; - let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt); + const result = await phoneVerificationHelpers.completePhoneVerificationFlow(authTabId, pageState, { + step: visibleStep, + visibleStep, + }); + if (typeof completeNodeFromBackground === 'function') { + await completeNodeFromBackground(state?.nodeId || 'post-login-phone-verification', { + phoneVerification: true, + postLoginPhoneVerification: true, + code: result?.code || '', + }); + } + return result || {}; + } + + async function executeBindEmail(state) { + const visibleStep = getVisibleStep(state, 9); + activeFetchLoginCodeStep = visibleStep; + activeFetchLoginCodeStepKey = 'bind-email'; + await ensureAuthTabForPostLoginStep(state, visibleStep); + const pageState = await getLoginAuthStateFromContent(visibleStep, { + timeoutMs: await getStep8ReadyTimeoutMs('确认添加邮箱页或 OAuth 授权页已就绪', state?.oauthUrl || '', visibleStep), + logMessage: `步骤 ${visibleStep}:正在确认是否需要绑定邮箱...`, + }); + + if (pageState?.state === 'oauth_consent_page') { + await addLog(`步骤 ${visibleStep}:当前认证页已进入 OAuth 授权页,跳过绑定邮箱步骤。`, 'warn', { + step: visibleStep, + stepKey: 'bind-email', + }); + if (typeof completeNodeFromBackground === 'function') { + await completeNodeFromBackground(state?.nodeId || 'bind-email', { + directOAuthConsentPage: true, + bindEmailSubmitted: false, + }); + } + return; + } + + if (pageState?.state !== 'add_email_page') { + throw new Error(`步骤 ${visibleStep}:绑定邮箱步骤只处理添加邮箱页,当前状态:${pageState?.state || 'unknown'}。URL: ${pageState?.url || ''}`.trim()); + } + + const addEmailPreparation = await submitAddEmailIfNeeded(state, visibleStep, pageState); + const preparedState = addEmailPreparation?.state || state; + const nextPageState = addEmailPreparation?.pageState || pageState; + if (nextPageState?.state !== 'verification_page') { + throw new Error(`步骤 ${visibleStep}:绑定邮箱提交后必须进入邮箱验证码页,当前状态:${nextPageState?.state || 'unknown'}。URL: ${nextPageState?.url || ''}`.trim()); + } + + if (typeof completeNodeFromBackground === 'function') { + await completeNodeFromBackground(state?.nodeId || 'bind-email', { + bindEmailSubmitted: true, + email: preparedState?.email || '', + step8VerificationTargetEmail: preparedState?.step8VerificationTargetEmail || nextPageState?.displayedEmail || '', + }); + } + } + + async function pollEmailVerificationCode(preparedState, pageState, visibleStep, runtime = {}) { + let latestResendAt = Math.max( + 0, + Number(runtime?.stickyLastResendAt) || 0, + Number(preparedState?.loginVerificationRequestedAt) || 0 + ); const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function' ? runtime.onResendRequestedAt : null; - - throwIfStopped(); - let pageState = await ensureStep8VerificationPageReady({ - visibleStep, - authLoginStep: getAuthLoginStepForVisibleStep(visibleStep), - allowPhoneVerificationPage: true, - allowAddEmailPage: true, - timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep), - }); - if (pageState?.state === 'oauth_consent_page') { - await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { nodeId: state?.nodeId }); - return; - } - if (pageState?.state === 'phone_verification_page') { - return executeLoginPhoneCodeStep(state, authTabId, visibleStep); - } - - let preparedState = state; - const addEmailPreparation = await submitAddEmailIfNeeded(preparedState, visibleStep, pageState); - preparedState = addEmailPreparation?.state || preparedState; - pageState = addEmailPreparation?.pageState || pageState; - if (pageState?.state === 'oauth_consent_page') { - await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { nodeId: preparedState?.nodeId || state?.nodeId }); - return; - } - if (pageState?.state === 'phone_verification_page') { - return executeLoginPhoneCodeStep(preparedState, authTabId, visibleStep); - } - - const preparedStateLastResendAt = Number(preparedState?.loginVerificationRequestedAt) || 0; - if (preparedStateLastResendAt > 0) { - latestResendAt = Math.max(latestResendAt, preparedStateLastResendAt); - } - const mail = getMailConfig(preparedState); if (mail.error) throw new Error(mail.error); const stepStartedAt = Date.now(); const verificationFilterAfterTimestamp = mail.provider === '2925' ? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS) : stepStartedAt; - const verificationSessionKey = `8:${stepStartedAt}`; + const verificationSessionKey = `${visibleStep}:${stepStartedAt}`; const shouldCompareVerificationEmail = mail.provider !== '2925'; const displayedVerificationEmail = shouldCompareVerificationEmail ? normalizeStep8VerificationTargetEmail(pageState?.displayedEmail) : ''; const fixedTargetEmail = shouldCompareVerificationEmail - ? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(preparedState?.email)) + ? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(preparedState?.step8VerificationTargetEmail || preparedState?.email)) : ''; await setState({ step8VerificationTargetEmail: displayedVerificationEmail || '', }); - await addLog(`步骤 ${visibleStep}:登录验证码页面已就绪,开始获取验证码。`, 'info'); + await addLog(`步骤 ${visibleStep}:邮箱验证码页面已就绪,开始获取验证码。`, 'info'); if (shouldCompareVerificationEmail && displayedVerificationEmail) { await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info'); } @@ -491,7 +548,7 @@ completionStep: visibleStep, promptStep: visibleStep, }); - return; + return { lastResendAt: latestResendAt }; } if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { @@ -564,6 +621,104 @@ }; } + async function completeFetchBindEmailCodeSkippedOnOauth(visibleStep, options = {}) { + await addLog(`步骤 ${visibleStep}:当前认证页已进入 OAuth 授权页,跳过绑定邮箱验证码步骤。`, 'warn', { + step: visibleStep, + stepKey: 'fetch-bind-email-code', + }); + if (typeof completeNodeFromBackground === 'function') { + await completeNodeFromBackground(options.nodeId || 'fetch-bind-email-code', { + directOAuthConsentPage: true, + bindEmailCodeSkipped: true, + }); + } + } + + async function executeFetchBindEmailCode(state) { + const visibleStep = getVisibleStep(state, 10); + activeFetchLoginCodeStep = visibleStep; + activeFetchLoginCodeStepKey = 'fetch-bind-email-code'; + await ensureAuthTabForPostLoginStep(state, visibleStep); + const pageState = await getLoginAuthStateFromContent(visibleStep, { + timeoutMs: await getStep8ReadyTimeoutMs('确认绑定邮箱验证码页已就绪', state?.oauthUrl || '', visibleStep), + logMessage: `步骤 ${visibleStep}:正在确认绑定邮箱验证码页...`, + }); + + if (pageState?.state === 'oauth_consent_page') { + if (state?.bindEmailSubmitted) { + throw new Error(`步骤 ${visibleStep}:绑定邮箱提交后不应直接进入 OAuth 授权页,必须先完成邮箱验证码。URL: ${pageState?.url || ''}`.trim()); + } + await completeFetchBindEmailCodeSkippedOnOauth(visibleStep, { nodeId: state?.nodeId }); + return; + } + if (pageState?.state !== 'verification_page') { + throw new Error(`步骤 ${visibleStep}:获取绑定邮箱验证码步骤只处理邮箱验证码页,当前状态:${pageState?.state || 'unknown'}。URL: ${pageState?.url || ''}`.trim()); + } + if (!state?.bindEmailSubmitted) { + throw new Error(`步骤 ${visibleStep}:尚未完成绑定邮箱提交,不能直接获取绑定邮箱验证码。`); + } + + return pollEmailVerificationCode(state, pageState, visibleStep, { + stickyLastResendAt: Number(state?.loginVerificationRequestedAt) || 0, + }); + } + + async function runStep8Attempt(state, runtime = {}) { + const visibleStep = getVisibleStep(state, 8); + activeFetchLoginCodeStep = visibleStep; + activeFetchLoginCodeStepKey = 'fetch-login-code'; + const authTabId = await getTabId('signup-page'); + + if (authTabId) { + await chrome.tabs.update(authTabId, { active: true }); + } else { + if (!state.oauthUrl) { + throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`); + } + await reuseOrCreateTab('signup-page', state.oauthUrl); + } + + throwIfStopped(); + let pageState = await ensureStep8VerificationPageReady({ + visibleStep, + authLoginStep: getAuthLoginStepForVisibleStep(visibleStep), + allowPhoneVerificationPage: true, + allowAddEmailPage: true, + timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep), + }); + if (pageState?.state === 'oauth_consent_page') { + await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { nodeId: state?.nodeId }); + return; + } + const phoneLoginCodeMode = isPhoneLoginCodeMode(state); + if (phoneLoginCodeMode) { + if (pageState?.state === 'phone_verification_page') { + return executeLoginPhoneCodeStep(state, authTabId, visibleStep); + } + if (pageState?.state === 'add_email_page') { + await completeStep8WhenDeferredToBindEmail(visibleStep, { nodeId: state?.nodeId }); + return; + } + if (pageState?.state === 'verification_page') { + throw new Error(`步骤 ${visibleStep}:手机号注册模式只允许处理手机登录验证码,当前进入了普通邮箱登录验证码页,不会回落到邮箱 provider。URL: ${pageState?.url || ''}`.trim()); + } + if (pageState?.state === 'add_phone_page') { + throw new Error(`步骤 ${visibleStep}:手机号注册模式不应进入添加手机号页。URL: ${pageState?.url || ''}`.trim()); + } + throw new Error(`步骤 ${visibleStep}:手机号注册模式登录验证码步骤进入了不允许的页面:${pageState?.state || 'unknown'}。URL: ${pageState?.url || ''}`.trim()); + } + + if (pageState?.state === 'add_phone_page' || pageState?.state === 'phone_verification_page') { + await completeStep8WhenDeferredToPostLoginPhone(visibleStep, pageState, { nodeId: state?.nodeId }); + return; + } + if (pageState?.state === 'add_email_page') { + throw new Error(`步骤 ${visibleStep}:邮箱注册模式不应进入添加邮箱页。URL: ${pageState?.url || ''}`.trim()); + } + + return pollEmailVerificationCode(state, pageState, visibleStep, runtime); + } + function isStep8RestartStep7Error(error) { const message = String(error?.message || error || ''); return /STEP8_RESTART_STEP7::/i.test(message); @@ -576,7 +731,6 @@ let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0; let retryWithoutStep7Streak = 0; const maxRetryWithoutStep7Streak = 3; - let currentNodeRecoveryAttempt = 0; while (true) { try { @@ -600,27 +754,6 @@ let currentError = err; let retryWithoutStep7 = false; - if (isStep8EmailInUseError(currentError) || isStep8MaxCheckAttemptsError(currentError)) { - currentNodeRecoveryAttempt += 1; - if (currentNodeRecoveryAttempt > STEP8_CURRENT_STEP_RECOVERY_MAX_ATTEMPTS) { - throw currentError; - } - if (isStep8EmailInUseError(currentError)) { - await resetStep8AfterEmailInUse(currentState, visibleStep); - await openStep8AddEmailPage(currentState, visibleStep, 'email_in_use'); - } else { - await resetStep8AfterMaxCheckAttempts(visibleStep); - await openStep8AddEmailPage(currentState, visibleStep, 'max_check_attempts'); - } - const latestState = typeof getState === 'function' ? await getState() : currentState; - currentState = { - ...(currentState || {}), - ...(latestState || {}), - oauthUrl: currentState?.oauthUrl || latestState?.oauthUrl || STEP8_ADD_EMAIL_URL, - }; - continue; - } - const isMailPollingError = isVerificationMailPollingError(err); if (isMailPollingError && !isStep8RestartStep7Error(err)) { const recovery = await recoverStep8PollingFailure(currentState, visibleStep); @@ -717,7 +850,12 @@ throw new Error(`步骤 ${visibleStep}:登录验证码流程未成功完成。`); } - return { executeStep8 }; + return { + executeStep8, + executePostLoginPhoneVerification, + executeBindEmail, + executeFetchBindEmailCode, + }; } return { createStep8Executor }; diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index e3b1b5b..6255b8d 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -1251,7 +1251,7 @@ } const data = await response.json(); if (data?.status !== 'ok') { - throw new Error(data?.message || data?.status || 'unknown response'); + throw new Error(data?.message || data?.status || '未知响应'); } return buildDirectAddressSeed(countryCode, data.address || {}, fallbackSeed); } diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index 285303c..7849966 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -19,7 +19,6 @@ isStep6RecoverableResult, isStep6SuccessResult, getTabId, - phoneVerificationHelpers = null, refreshOAuthUrlBeforeStep6, reuseOrCreateTab, sendToContentScriptResilient, @@ -103,40 +102,101 @@ return match ? match[0] : 'https://auth.openai.com/add-phone'; } - async function completeStep7AddPhoneHandoff(state = {}, err, completionStep) { - if (!state?.phoneVerificationEnabled) { + function getStep7ResultState(result = {}) { + return String(result?.state || '').trim(); + } + + function isStep7OauthConsentResult(result = {}) { + return Boolean(result?.directOAuthConsentPage) + || getStep7ResultState(result) === 'oauth_consent_page'; + } + + function isStep7AddEmailResult(result = {}) { + return Boolean(result?.addEmailPage) || getStep7ResultState(result) === 'add_email_page'; + } + + function isStep7AddPhoneResult(result = {}) { + return Boolean(result?.addPhonePage) || getStep7ResultState(result) === 'add_phone_page'; + } + + function isStep7PhoneVerificationResult(result = {}) { + return Boolean(result?.phoneVerificationPage) || getStep7ResultState(result) === 'phone_verification_page'; + } + + function isStep7PlainVerificationResult(result = {}) { + return getStep7ResultState(result) === 'verification_page' && !isStep7PhoneVerificationResult(result); + } + + function buildStep7CompletionPayload(result = {}, currentState = {}, currentIdentifierType = '', currentPhoneNumber = '') { + const phoneSignupMode = currentIdentifierType === 'phone'; + const payload = { + loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, + }; + + if (currentIdentifierType === 'phone') { + payload.accountIdentifierType = 'phone'; + payload.accountIdentifier = currentPhoneNumber; + payload.signupPhoneNumber = currentPhoneNumber; + payload.signupPhoneCompletedActivation = currentState?.signupPhoneCompletedActivation || null; + payload.signupPhoneActivation = currentState?.signupPhoneActivation || null; + } + + if (isStep7OauthConsentResult(result)) { + payload.skipLoginVerificationStep = true; + payload.directOAuthConsentPage = true; + return payload; + } + + if (phoneSignupMode) { + if (isStep7AddPhoneResult(result)) { + throw new Error(`步骤 ${completionStepForState(currentState)}:手机号注册模式 OAuth 登录不应进入添加手机号页。URL: ${result?.url || ''}`.trim()); + } + if (isStep7AddEmailResult(result)) { + payload.skipLoginVerificationStep = true; + payload.addEmailPage = true; + return payload; + } + if (isStep7PhoneVerificationResult(result)) { + return payload; + } + if (isStep7PlainVerificationResult(result)) { + throw new Error(`步骤 ${completionStepForState(currentState)}:手机号注册模式 OAuth 登录进入了普通邮箱登录验证码页,当前流程不会回落到邮箱验证码。URL: ${result?.url || ''}`.trim()); + } + throw new Error(`步骤 ${completionStepForState(currentState)}:手机号注册模式 OAuth 登录进入了不允许的页面:${getLoginAuthStateLabel(result.state)}。URL: ${result?.url || ''}`.trim()); + } + + if (isStep7AddEmailResult(result)) { + throw new Error(`步骤 ${completionStepForState(currentState)}:邮箱注册模式 OAuth 登录不应进入添加邮箱页。URL: ${result?.url || ''}`.trim()); + } + if (isStep7AddPhoneResult(result) || isStep7PhoneVerificationResult(result)) { + payload.skipLoginVerificationStep = true; + payload.addPhonePage = isStep7AddPhoneResult(result); + payload.phoneVerificationPage = isStep7PhoneVerificationResult(result); + return payload; + } + if (isStep7PlainVerificationResult(result)) { + return payload; + } + + throw new Error(`步骤 ${completionStepForState(currentState)}:邮箱注册模式 OAuth 登录进入了不允许的页面:${getLoginAuthStateLabel(result.state)}。URL: ${result?.url || ''}`.trim()); + } + + function completionStepForState(state = {}) { + const visibleStep = Math.floor(Number(state?.visibleStep) || 0); + return visibleStep > 0 ? visibleStep : 7; + } + + async function completeStep7PostLoginPhoneHandoff(state = {}, err, completionStep) { + if (normalizeStep7SignupMethod(state?.resolvedSignupMethod || state?.signupMethod) === 'phone') { throw new Error( - `步骤 ${completionStep}:登录提交后页面进入手机号页面,必须先启用接码/phone verification 后才能继续。URL: ${extractAddPhoneUrl(err)}` + `步骤 ${completionStep}:手机号注册模式 OAuth 登录进入了添加手机号页,当前流程不允许在手机号注册模式补手机号。URL: ${extractAddPhoneUrl(err)}` ); } - if (typeof phoneVerificationHelpers?.completePhoneVerificationFlow !== 'function') { - throw new Error(`步骤 ${completionStep}:手机号验证流程不可用,接码模块尚未初始化。`); - } - if (typeof getTabId !== 'function') { - throw new Error(`步骤 ${completionStep}:无法定位认证页面标签页,不能继续手机号验证。`); - } - - const signupTabId = await getTabId('signup-page'); - if (!Number.isInteger(signupTabId)) { - throw new Error(`步骤 ${completionStep}:认证页面标签页已关闭,无法继续手机号验证。`); - } - - const pageState = { - addPhonePage: true, - phoneVerificationPage: false, - state: 'add_phone_page', - url: extractAddPhoneUrl(err), - }; - await phoneVerificationHelpers.completePhoneVerificationFlow(signupTabId, pageState, { - step: completionStep, - visibleStep: completionStep, - }); await completeNodeFromBackground(state?.nodeId || 'oauth-login', { loginVerificationRequestedAt: null, skipLoginVerificationStep: true, - directOAuthConsentPage: true, - phoneVerification: true, - loginPhoneVerification: true, + addPhonePage: true, + directOAuthConsentPage: false, }); } @@ -261,22 +321,12 @@ } if (isStep6SuccessResult(result)) { - const completionPayload = { - loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, - }; - if (currentIdentifierType === 'phone') { - completionPayload.accountIdentifierType = 'phone'; - completionPayload.accountIdentifier = currentPhoneNumber; - completionPayload.signupPhoneNumber = currentPhoneNumber; - completionPayload.signupPhoneCompletedActivation = currentState?.signupPhoneCompletedActivation || null; - completionPayload.signupPhoneActivation = currentState?.signupPhoneActivation || null; - } - if (Object.prototype.hasOwnProperty.call(result || {}, 'skipLoginVerificationStep')) { - completionPayload.skipLoginVerificationStep = Boolean(result.skipLoginVerificationStep); - } - if (Object.prototype.hasOwnProperty.call(result || {}, 'directOAuthConsentPage')) { - completionPayload.directOAuthConsentPage = Boolean(result.directOAuthConsentPage); - } + const completionPayload = buildStep7CompletionPayload( + result, + { ...(currentState || {}), visibleStep: completionStep }, + currentIdentifierType, + currentPhoneNumber + ); await completeNodeFromBackground(state?.nodeId || 'oauth-login', completionPayload); return; @@ -295,7 +345,7 @@ const latestAddPhoneState = typeof getState === 'function' ? await getState().catch(() => state) : state; - await completeStep7AddPhoneHandoff( + await completeStep7PostLoginPhoneHandoff( { ...(state || {}), ...(latestAddPhoneState || {}) }, err, completionStep diff --git a/background/steps/submit-signup-email.js b/background/steps/submit-signup-email.js index c289b4b..0e4a690 100644 --- a/background/steps/submit-signup-email.js +++ b/background/steps/submit-signup-email.js @@ -37,7 +37,7 @@ function isRetryableStep2TransportErrorMessage(errorLike) { const message = getErrorMessage(errorLike); - return /Content script on signup-page did not respond in \d+s|Receiving end does not exist|message channel closed|A listener indicated an asynchronous response|port closed before a response was received|did not respond in \d+s/i.test(message); + return /Content script on signup-page did not respond in \d+s|内容脚本\s+\d+(?:\.\d+)?\s*秒内未响应|Receiving end does not exist|message channel closed|A listener indicated an asynchronous response|port closed before a response was received|did not respond in \d+s/i.test(message); } function isLikelyLoggedInChatgptHomeUrl(rawUrl) { diff --git a/background/tab-runtime.js b/background/tab-runtime.js index 3275778..765f0e7 100644 --- a/background/tab-runtime.js +++ b/background/tab-runtime.js @@ -649,7 +649,7 @@ settled = true; const seconds = Math.ceil(responseTimeoutMs / 1000); console.warn(LOG_PREFIX, `[sendTabMessageWithTimeout] timeout ${debugLabel} after ${Date.now() - startedAt}ms`); - reject(new Error(`Content script on ${source} did not respond in ${seconds}s. Try refreshing the tab and retry.`)); + reject(new Error(`${getSourceLabel(source)} 内容脚本 ${seconds} 秒内未响应,请刷新页面后重试。`)); }, responseTimeoutMs); chrome.tabs.sendMessage(tabId, message) @@ -677,7 +677,7 @@ const commandKey = getSourceCommandKey(source); const timer = setTimeout(() => { pendingCommands.delete(commandKey); - reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`)); + reject(new Error(`${getSourceLabel(source)} 内容脚本 ${timeout / 1000} 秒内未响应,请刷新页面后重试。`)); }, timeout); pendingCommands.set(commandKey, { message, diff --git a/background/verification-flow.js b/background/verification-flow.js index e890686..a7e3a39 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -76,7 +76,7 @@ const isRetryableVerificationTransportError = typeof deps.isRetryableContentScriptTransportError === 'function' ? deps.isRetryableContentScriptTransportError - : ((error) => /back\/forward cache|message channel is closed|Receiving end does not exist|port closed before a response was received|A listener indicated an asynchronous response|did not respond in \d+s/i.test( + : ((error) => /back\/forward cache|message channel is closed|Receiving end does not exist|port closed before a response was received|A listener indicated an asynchronous response|内容脚本\s+\d+(?:\.\d+)?\s*秒内未响应|did not respond in \d+s/i.test( String(typeof error === 'string' ? error : error?.message || '') )); diff --git a/content/signup-page.js b/content/signup-page.js index 42b4459..1251af0 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -89,8 +89,10 @@ function resolveCommandNodeId(message = {}) { const visibleStep = Number(message.payload?.visibleStep || message.step) || 0; if (visibleStep === 4) return 'fetch-signup-code'; if (visibleStep === 8 || visibleStep === 11) return 'fetch-login-code'; - if (visibleStep === 9 || visibleStep === 12) return 'confirm-oauth'; - if (visibleStep === 7 || visibleStep === 10) return 'oauth-login'; + if (visibleStep === 9 || visibleStep === 12) return 'post-login-phone-verification'; + if (visibleStep === 10 || visibleStep === 13) return 'confirm-oauth'; + if (visibleStep === 14 || visibleStep === 15) return 'platform-verify'; + if (visibleStep === 7) return 'oauth-login'; if (visibleStep === 5) return 'fill-profile'; if (visibleStep === 3) return 'fill-password'; if (visibleStep === 2) return 'submit-signup-email'; diff --git a/content/utils.js b/content/utils.js index 9d47001..77455c5 100644 --- a/content/utils.js +++ b/content/utils.js @@ -283,11 +283,13 @@ const DEFAULT_OPENAI_NODE_BY_STEP = Object.freeze({ 6: 'wait-registration-success', 7: 'oauth-login', 8: 'fetch-login-code', - 9: 'confirm-oauth', - 10: 'platform-verify', + 9: 'post-login-phone-verification', + 10: 'confirm-oauth', 11: 'fetch-login-code', - 12: 'confirm-oauth', - 13: 'platform-verify', + 12: 'post-login-phone-verification', + 13: 'confirm-oauth', + 14: 'platform-verify', + 15: 'platform-verify', }); function resolveReportNodeId(stepOrNodeId, data = {}) { diff --git a/data/step-definitions.js b/data/step-definitions.js index 525b487..facb7c1 100644 --- a/data/step-definitions.js +++ b/data/step-definitions.js @@ -9,20 +9,16 @@ const SIGNUP_METHOD_EMAIL = 'email'; const SIGNUP_METHOD_PHONE = 'phone'; - const NORMAL_STEP_DEFINITIONS = [ + const NORMAL_PREFIX_STEP_DEFINITIONS = [ { id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' }, { id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-signup-email' }, { id: 3, order: 30, key: 'fill-password', title: '填写密码并继续', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-password' }, { id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-signup-code' }, { id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-profile' }, { id: 6, order: 60, key: 'wait-registration-success', title: '等待注册成功', sourceId: 'chatgpt', driverId: null, command: 'wait-registration-success' }, - { id: 7, order: 70, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' }, - { id: 8, order: 80, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' }, - { id: 9, order: 90, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' }, - { id: 10, order: 100, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' }, ]; - const PLUS_PAYPAL_STEP_DEFINITIONS = [ + const PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS = [ { id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' }, { id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-signup-email' }, { id: 3, order: 30, key: 'fill-password', title: '填写密码并继续', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-password' }, @@ -32,13 +28,9 @@ { id: 7, order: 70, key: 'plus-checkout-billing', title: '填写账单并提交订单', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-billing' }, { id: 8, order: 80, key: 'paypal-approve', title: 'PayPal 登录与授权', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-approve' }, { id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-return' }, - { id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' }, - { id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' }, - { id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' }, - { id: 13, order: 130, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' }, ]; - const PLUS_GOPAY_STEP_DEFINITIONS = [ + const PLUS_GOPAY_PREFIX_STEP_DEFINITIONS = [ { id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' }, { id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-signup-email' }, { id: 3, order: 30, key: 'fill-password', title: '填写密码并继续', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-password' }, @@ -46,13 +38,9 @@ { id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-profile' }, { id: 6, order: 60, key: 'plus-checkout-create', title: '打开 GoPay 订阅页', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-create' }, { id: 7, order: 70, key: 'gopay-subscription-confirm', title: '等待 GoPay 订阅确认', sourceId: 'gopay-flow', driverId: 'content/gopay-flow', command: 'gopay-subscription-confirm' }, - { id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' }, - { id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' }, - { id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' }, - { id: 13, order: 130, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' }, ]; - const PLUS_GPC_STEP_DEFINITIONS = [ + const PLUS_GPC_PREFIX_STEP_DEFINITIONS = [ { id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' }, { id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-signup-email' }, { id: 3, order: 30, key: 'fill-password', title: '填写密码并继续', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-password' }, @@ -60,12 +48,50 @@ { id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-profile' }, { id: 6, order: 60, key: 'plus-checkout-create', title: '创建 GPC 订单', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-create' }, { id: 7, order: 70, key: 'plus-checkout-billing', title: '等待 GPC 任务完成', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-billing' }, - { id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' }, - { id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' }, - { id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' }, - { id: 13, order: 130, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' }, ]; + function createOpenAiAuthTail(startId, startOrder, signupMethod = SIGNUP_METHOD_EMAIL) { + const id = Number(startId) || 7; + const order = Number(startOrder) || id * 10; + const commonStart = [ + { id, order, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' }, + { id: id + 1, order: order + 10, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' }, + ]; + + if (signupMethod === SIGNUP_METHOD_PHONE) { + return [ + ...commonStart, + { id: id + 2, order: order + 20, key: 'bind-email', title: '绑定邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'bind-email' }, + { id: id + 3, order: order + 30, key: 'fetch-bind-email-code', title: '获取绑定邮箱验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fetch-bind-email-code', mailRuleId: 'openai-login-code' }, + { id: id + 4, order: order + 40, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' }, + { id: id + 5, order: order + 50, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' }, + ]; + } + + return [ + ...commonStart, + { id: id + 2, order: order + 20, key: 'post-login-phone-verification', title: '手机号验证', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'post-login-phone-verification' }, + { id: id + 3, order: order + 30, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' }, + { id: id + 4, order: order + 40, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' }, + ]; + } + + function createOpenAiSteps(prefixSteps, startId, startOrder, signupMethod = SIGNUP_METHOD_EMAIL) { + return [ + ...prefixSteps, + ...createOpenAiAuthTail(startId, startOrder, signupMethod), + ]; + } + + const NORMAL_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_EMAIL); + const NORMAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE); + const PLUS_PAYPAL_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL); + const PLUS_PAYPAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE); + const PLUS_GOPAY_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL); + const PLUS_GOPAY_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE); + const PLUS_GPC_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GPC_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL); + const PLUS_GPC_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GPC_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE); + const PHONE_SIGNUP_TITLE_OVERRIDES = Object.freeze({ 'submit-signup-email': '注册并输入手机号', 'fetch-signup-code': '获取手机验证码', @@ -103,14 +129,18 @@ } function getOpenAiModeStepDefinitions(options = {}) { + const signupMethod = getResolvedSignupMethod(options); if (!isPlusModeEnabled(options)) { - return NORMAL_STEP_DEFINITIONS; + return signupMethod === SIGNUP_METHOD_PHONE ? NORMAL_PHONE_STEP_DEFINITIONS : NORMAL_STEP_DEFINITIONS; } const paymentMethod = normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod); if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { - return PLUS_GPC_STEP_DEFINITIONS; + return signupMethod === SIGNUP_METHOD_PHONE ? PLUS_GPC_PHONE_STEP_DEFINITIONS : PLUS_GPC_STEP_DEFINITIONS; } - return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS; + if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY) { + return signupMethod === SIGNUP_METHOD_PHONE ? PLUS_GOPAY_PHONE_STEP_DEFINITIONS : PLUS_GOPAY_STEP_DEFINITIONS; + } + return signupMethod === SIGNUP_METHOD_PHONE ? PLUS_PAYPAL_PHONE_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS; } function getOpenAiPlusPaymentStepTitle(options = {}) { @@ -141,9 +171,13 @@ const keyed = new Map(); for (const step of [ ...NORMAL_STEP_DEFINITIONS, + ...NORMAL_PHONE_STEP_DEFINITIONS, ...PLUS_PAYPAL_STEP_DEFINITIONS, + ...PLUS_PAYPAL_PHONE_STEP_DEFINITIONS, ...PLUS_GOPAY_STEP_DEFINITIONS, + ...PLUS_GOPAY_PHONE_STEP_DEFINITIONS, ...PLUS_GPC_STEP_DEFINITIONS, + ...PLUS_GPC_PHONE_STEP_DEFINITIONS, ]) { keyed.set(`${step.id}:${step.key}`, step); } @@ -317,10 +351,14 @@ DEFAULT_ACTIVE_FLOW_ID, STEP_DEFINITIONS: NORMAL_STEP_DEFINITIONS, NORMAL_STEP_DEFINITIONS, + NORMAL_PHONE_STEP_DEFINITIONS, PLUS_STEP_DEFINITIONS: PLUS_PAYPAL_STEP_DEFINITIONS, PLUS_PAYPAL_STEP_DEFINITIONS, + PLUS_PAYPAL_PHONE_STEP_DEFINITIONS, PLUS_GOPAY_STEP_DEFINITIONS, + PLUS_GOPAY_PHONE_STEP_DEFINITIONS, PLUS_GPC_STEP_DEFINITIONS, + PLUS_GPC_PHONE_STEP_DEFINITIONS, SIGNUP_METHOD_EMAIL, SIGNUP_METHOD_PHONE, getAllSteps, diff --git a/phone-sms/providers/five-sim.js b/phone-sms/providers/five-sim.js index dab3b6b..313557f 100644 --- a/phone-sms/providers/five-sim.js +++ b/phone-sms/providers/five-sim.js @@ -578,7 +578,7 @@ const maxPrice = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice); const operator = normalizeFiveSimOperator(state.fiveSimOperator); if (maxPrice && operator !== DEFAULT_OPERATOR) { - throw new Error('5sim maxPrice only works when operator is "any"; clear the price limit or switch operator to any before buying a number.'); + throw new Error('5sim 价格上限仅支持运营商为 "any" 时使用;请清空价格上限,或先把运营商切换为 any。'); } } diff --git a/phone-sms/providers/hero-sms.js b/phone-sms/providers/hero-sms.js index bcd5acc..da5ec30 100644 --- a/phone-sms/providers/hero-sms.js +++ b/phone-sms/providers/hero-sms.js @@ -109,7 +109,7 @@ query = { api_key: config.apiKey, ...query }; } if (!config.fetchImpl) { - throw new Error('HeroSMS fetch implementation is unavailable.'); + throw new Error('HeroSMS 网络请求实现不可用。'); } const controller = typeof AbortController === 'function' ? new AbortController() : null; const timeoutId = controller @@ -123,7 +123,7 @@ const text = await response.text(); const payload = parsePayload(text); if (!response.ok) { - const error = new Error(`${actionLabel} failed: ${describePayload(payload) || response.status}`); + const error = new Error(`${actionLabel}失败:${describePayload(payload) || response.status}`); error.payload = payload; error.status = response.status; throw error; @@ -131,7 +131,7 @@ return payload; } catch (error) { if (error?.name === 'AbortError') { - throw new Error(`${actionLabel} timed out.`); + throw new Error(`${actionLabel}超时。`); } throw error; } finally { @@ -162,7 +162,7 @@ async function fetchBalance(state = {}, deps = {}) { const config = resolveConfig(state, deps); if (!config.apiKey) { - throw new Error('HeroSMS API key is missing. Save it in the side panel before querying balance.'); + throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); } const payload = await fetchPayload(config, { action: 'getBalance' }, 'HeroSMS getBalance'); const balance = Number(String(describePayload(payload)).replace(/^ACCESS_BALANCE:/i, '').trim()); diff --git a/phone-sms/providers/registry.js b/phone-sms/providers/registry.js index 068c2d5..070da15 100644 --- a/phone-sms/providers/registry.js +++ b/phone-sms/providers/registry.js @@ -112,7 +112,7 @@ const definition = getProviderDefinition(providerId); const module = getProviderModule(providerId); if (!module || typeof module.createProvider !== 'function') { - throw new Error(`Phone SMS provider module is not loaded: ${definition.id}`); + throw new Error(`接码平台模块未加载:${definition.id}`); } return module.createProvider(deps); } diff --git a/shared/source-registry.js b/shared/source-registry.js index 4e67d63..1083e50 100644 --- a/shared/source-registry.js +++ b/shared/source-registry.js @@ -170,6 +170,9 @@ 'fill-profile', 'oauth-login', 'submit-verification-code', + 'post-login-phone-verification', + 'bind-email', + 'fetch-bind-email-code', 'confirm-oauth', 'detect-auth-state', ], diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index fbf56fb..8eb86a3 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -6192,14 +6192,14 @@ async function loadHeroSmsCountries() { .filter((entry) => entry.id) .sort((left, right) => String(left.label || '').localeCompare(String(right.label || ''))); if (!optionItems.length) { - throw new Error('empty country list'); + throw new Error('国家列表为空'); } heroSmsCountrySearchTextById.clear(); optionItems.forEach((entry) => heroSmsCountrySearchTextById.set(String(entry.id), entry.searchText)); applyOptions(optionItems, selectHeroSmsCountry); applyOptions(optionItems, selectHeroSmsCountryFallback); } catch (error) { - console.warn('Failed to load 5sim countries:', error); + console.warn('加载 5sim 国家列表失败:', error); const fallbackItems = FIVE_SIM_SUPPORTED_COUNTRY_ITEMS.map((item) => ({ id: item.id, label: formatFiveSimCountryDisplayLabel(item.id, item.eng), @@ -6224,7 +6224,7 @@ async function loadHeroSmsCountries() { const payload = await response.json(); const countries = Array.isArray(payload?.value) ? payload.value : (Array.isArray(payload) ? payload : []); if (!countries.length) { - throw new Error('empty country list'); + throw new Error('国家列表为空'); } const optionItems = countries .filter((item) => Number(item?.id) > 0 && (String(item?.eng || '').trim() || String(item?.chn || '').trim())) @@ -6240,7 +6240,7 @@ async function loadHeroSmsCountries() { }); if (!optionItems.length) { - throw new Error('empty country list'); + throw new Error('国家列表为空'); } heroSmsCountrySearchTextById.clear(); @@ -6251,7 +6251,7 @@ async function loadHeroSmsCountries() { applyOptions(optionItems, selectHeroSmsCountry); applyOptions(optionItems, selectHeroSmsCountryFallback); } catch (error) { - console.warn('Failed to load HeroSMS countries:', error); + console.warn('加载 HeroSMS 国家列表失败:', error); const fallbackItems = HERO_SMS_FALLBACK_COUNTRY_ITEMS .map((item) => { const id = normalizeHeroSmsCountryId(item.id); @@ -6620,7 +6620,7 @@ async function loadFiveSimCountries() { const items = parseFiveSimCountriesPayload(payload); applyOptions(items.length ? items : fallbackItems); } catch (error) { - console.warn('Failed to load 5sim countries:', error); + console.warn('加载 5sim 国家列表失败:', error); applyOptions(fallbackItems); } @@ -9378,7 +9378,7 @@ function applySettingsState(state) { if (previousPhoneSmsProvider !== restoredPhoneSmsProvider) { heroSmsCountrySelectionOrder = []; loadHeroSmsCountries().catch((error) => { - console.warn('Failed to reload SMS countries after provider restore:', error); + console.warn('恢复接码平台后重新加载国家列表失败:', error); }); } if (inputHeroSmsApiKey) { @@ -15491,13 +15491,13 @@ Promise.allSettled([ const fiveSimResult = results[1]; const nexSmsResult = results[2]; if (heroResult?.status === 'rejected') { - console.error('Failed to load HeroSMS countries:', heroResult.reason); + console.error('加载 HeroSMS 国家列表失败:', heroResult.reason); } if (fiveSimResult?.status === 'rejected') { - console.error('Failed to load 5sim countries:', fiveSimResult.reason); + console.error('加载 5sim 国家列表失败:', fiveSimResult.reason); } if (nexSmsResult?.status === 'rejected') { - console.error('Failed to load NexSMS countries:', nexSmsResult.reason); + console.error('加载 NexSMS 国家列表失败:', nexSmsResult.reason); } return restoreState().then(() => { syncPasswordToggleLabel(); diff --git a/tests/background-step6-retry-limit.test.js b/tests/background-step6-retry-limit.test.js index a4b4e97..a834799 100644 --- a/tests/background-step6-retry-limit.test.js +++ b/tests/background-step6-retry-limit.test.js @@ -137,7 +137,7 @@ test('step 7 retries up to configured limit and then fails', async () => { assert.equal(events.completed, 0); }); -test('step 7 exits internal retry loop immediately when add-phone is detected', async () => { +test('step 7 hands add-phone to the dedicated post-login phone node without internal retry', async () => { const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); const globalScope = {}; const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); @@ -145,7 +145,7 @@ test('step 7 exits internal retry loop immediately when add-phone is detected', const events = { refreshCalls: 0, sendCalls: 0, - completed: 0, + completions: [], logs: [], }; @@ -153,8 +153,8 @@ test('step 7 exits internal retry loop immediately when add-phone is detected', addLog: async (message, level = 'info') => { events.logs.push({ message, level }); }, - completeNodeFromBackground: async () => { - events.completed += 1; + completeNodeFromBackground: async (step, payload) => { + events.completions.push({ step, payload }); }, getErrorMessage: (error) => error?.message || String(error || ''), getLoginAuthStateLabel: (state) => state || 'unknown', @@ -174,21 +174,28 @@ test('step 7 exits internal retry loop immediately when add-phone is detected', throwIfStopped: () => {}, }); - await assert.rejects( - () => executor.executeStep7({ email: 'user@example.com', password: 'secret' }), - /add-phone/ - ); + await executor.executeStep7({ email: 'user@example.com', password: 'secret' }); assert.equal(events.refreshCalls, 1, 'add-phone should stop further OAuth refresh attempts'); assert.equal(events.sendCalls, 1, 'add-phone should stop after the first failed login attempt'); - assert.equal(events.completed, 0); + assert.deepStrictEqual(events.completions, [ + { + step: 'oauth-login', + payload: { + loginVerificationRequestedAt: null, + skipLoginVerificationStep: true, + addPhonePage: true, + directOAuthConsentPage: false, + }, + }, + ]); assert.ok( !events.logs.some(({ message }) => /准备重试/.test(message)), 'add-phone failure should not be logged as an internal retryable attempt' ); }); -test('step 7 hands direct add-phone to shared phone verification when enabled', async () => { +test('step 7 no longer runs shared phone verification inside oauth-login', async () => { const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); const globalScope = {}; const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); @@ -239,36 +246,21 @@ test('step 7 hands direct add-phone to shared phone verification when enabled', }); assert.equal(events.refreshCalls, 1); - assert.deepStrictEqual(events.phoneCalls, [ - { - tabId: 91, - pageState: { - addPhonePage: true, - phoneVerificationPage: false, - state: 'add_phone_page', - url: 'https://auth.openai.com/add-phone', - }, - options: { - step: 7, - visibleStep: 7, - }, - }, - ]); + assert.deepStrictEqual(events.phoneCalls, []); assert.deepStrictEqual(events.completions, [ { step: 'oauth-login', payload: { loginVerificationRequestedAt: null, skipLoginVerificationStep: true, - directOAuthConsentPage: true, - phoneVerification: true, - loginPhoneVerification: true, + addPhonePage: true, + directOAuthConsentPage: false, }, }, ]); }); -test('step 7 direct add-phone stays fatal when phone verification is disabled', async () => { +test('step 7 add-phone handoff does not depend on phone verification being enabled', async () => { const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); const globalScope = {}; const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); @@ -304,15 +296,12 @@ test('step 7 direct add-phone stays fatal when phone verification is disabled', throwIfStopped: () => {}, }); - await assert.rejects( - () => executor.executeStep7({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: false }), - /手机号页面.*接码|phone verification/i - ); + await executor.executeStep7({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: false }); assert.equal(events.phoneCalls, 0); - assert.equal(events.completions, 0); + assert.equal(events.completions, 1); }); -test('step 7 propagates fatal errors from shared add-phone verification', async () => { +test('step 7 ignores obsolete shared add-phone verifier during handoff', async () => { const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); const globalScope = {}; const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); @@ -348,12 +337,9 @@ test('step 7 propagates fatal errors from shared add-phone verification', async throwIfStopped: () => {}, }); - await assert.rejects( - () => executor.executeStep7({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: true }), - /没有可用手机号/ - ); - assert.equal(events.phoneCalls, 1); - assert.equal(events.completions, 0); + await executor.executeStep7({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: true }); + assert.equal(events.phoneCalls, 0); + assert.equal(events.completions, 1); }); test('step 7 starts a new oauth timeout window for each refreshed oauth url', async () => { @@ -382,6 +368,7 @@ test('step 7 starts a new oauth timeout window for each refreshed oauth url', as reuseOrCreateTab: async () => {}, sendToContentScriptResilient: async (_source, _message, options) => ({ step6Outcome: 'success', + state: 'verification_page', usedTimeoutMs: options.timeoutMs, }), startOAuthFlowTimeoutWindow: async (payload) => { diff --git a/tests/background-step7-recovery.test.js b/tests/background-step7-recovery.test.js index 6830101..ca51754 100644 --- a/tests/background-step7-recovery.test.js +++ b/tests/background-step7-recovery.test.js @@ -96,7 +96,7 @@ test('step 8 submits login verification directly without replaying step 7', asyn assert.equal(calls.resolveOptions.completionStep, 8); }); -test('step 8 keeps phone-registered accounts on email-code flow when page is email verification', async () => { +test('step 8 rejects ordinary email verification page in phone login mode', async () => { const calls = { getMailConfigCalls: 0, helperCalls: [], @@ -159,18 +159,21 @@ test('step 8 keeps phone-registered accounts on email-code flow when page is ema throwIfStopped: () => {}, }); - await executor.executeStep8({ - visibleStep: 8, - accountIdentifierType: 'phone', - signupPhoneCompletedActivation: { - activationId: 'signup-done', - phoneNumber: '66959916439', - }, - oauthUrl: 'https://oauth.example/latest', - }); + await assert.rejects( + () => executor.executeStep8({ + visibleStep: 8, + accountIdentifierType: 'phone', + signupPhoneCompletedActivation: { + activationId: 'signup-done', + phoneNumber: '66959916439', + }, + oauthUrl: 'https://oauth.example/latest', + }), + /手机号注册模式只允许处理手机登录验证码/ + ); - assert.equal(calls.getMailConfigCalls, 1); - assert.equal(calls.resolveCalls, 1); + assert.equal(calls.getMailConfigCalls, 0); + assert.equal(calls.resolveCalls, 0); assert.deepStrictEqual(calls.helperCalls, []); assert.deepStrictEqual(calls.completions, []); }); @@ -260,9 +263,154 @@ test('step 8 routes only a real phone verification page through sms helper', asy ]); }); -test('step 8 submits add-email before polling the email verification code', async () => { +test('post-login phone verification completes only on phone pages', async () => { + const calls = { + helperCalls: [], + completions: [], + }; + + const executor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + completeNodeFromBackground: async (step, payload) => { + calls.completions.push({ step, payload }); + }, + ensureStep8VerificationPageReady: async () => { + throw new Error('post-login phone step should inspect auth state directly'); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({}), + getTabId: async () => 1, + phoneVerificationHelpers: { + completePhoneVerificationFlow: async (tabId, pageState, options) => { + calls.helperCalls.push({ tabId, pageState, options }); + return { code: '112233' }; + }, + }, + reuseOrCreateTab: async () => 1, + sendToContentScriptResilient: async () => ({ + state: 'add_phone_page', + url: 'https://auth.openai.com/add-phone', + }), + setState: async () => {}, + throwIfStopped: () => {}, + }); + + await executor.executePostLoginPhoneVerification({ + visibleStep: 9, + nodeId: 'post-login-phone-verification', + phoneVerificationEnabled: true, + oauthUrl: 'https://oauth.example/latest', + }); + + assert.deepStrictEqual(calls.helperCalls, [ + { + tabId: 1, + pageState: { + state: 'add_phone_page', + url: 'https://auth.openai.com/add-phone', + }, + options: { + step: 9, + visibleStep: 9, + }, + }, + ]); + assert.deepStrictEqual(calls.completions, [ + { + step: 'post-login-phone-verification', + payload: { + phoneVerification: true, + postLoginPhoneVerification: true, + code: '112233', + }, + }, + ]); +}); + +test('post-login phone verification skips on OAuth consent and errors when disabled', async () => { + const completions = []; + const executor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + completeNodeFromBackground: async (step, payload) => { + completions.push({ step, payload }); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getTabId: async () => 1, + phoneVerificationHelpers: { + completePhoneVerificationFlow: async () => { + throw new Error('OAuth consent should not call phone helper'); + }, + }, + reuseOrCreateTab: async () => 1, + sendToContentScriptResilient: async () => ({ state: 'oauth_consent_page' }), + setState: async () => {}, + throwIfStopped: () => {}, + }); + + await executor.executePostLoginPhoneVerification({ + visibleStep: 9, + nodeId: 'post-login-phone-verification', + phoneVerificationEnabled: true, + oauthUrl: 'https://oauth.example/latest', + }); + + assert.deepStrictEqual(completions, [ + { + step: 'post-login-phone-verification', + payload: { + directOAuthConsentPage: true, + phoneVerification: false, + }, + }, + ]); + + const disabledExecutor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getTabId: async () => 1, + phoneVerificationHelpers: { + completePhoneVerificationFlow: async () => { + throw new Error('disabled phone verification should not call helper'); + }, + }, + reuseOrCreateTab: async () => 1, + sendToContentScriptResilient: async () => ({ + state: 'phone_verification_page', + url: 'https://auth.openai.com/phone-verification', + }), + setState: async () => {}, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => disabledExecutor.executePostLoginPhoneVerification({ + visibleStep: 9, + phoneVerificationEnabled: false, + oauthUrl: 'https://oauth.example/latest', + }), + /手机接码未开启/ + ); +}); + +test('step 8 defers add-email page to the dedicated bind-email node in phone mode', async () => { const calls = { contentMessages: [], + completions: [], resolvedStates: [], setStates: [], mailStates: [], @@ -277,6 +425,9 @@ test('step 8 submits add-email before polling the email verification code', asyn }, }, CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + completeNodeFromBackground: async (step, payload) => { + calls.completions.push({ step, payload }); + }, confirmCustomVerificationStepBypass: async () => {}, ensureStep8VerificationPageReady: async () => ({ state: 'add_email_page', url: 'https://auth.openai.com/add-email' }), getOAuthFlowRemainingMs: async () => 5000, @@ -339,29 +490,320 @@ test('step 8 submits add-email before polling the email verification code', asyn oauthUrl: 'https://oauth.example/latest', }); - assert.equal(calls.contentMessages.length, 1); - assert.equal(calls.resolvedStates.length, 1); - assert.equal(calls.resolveOptions.preserveAccountIdentity, true); - assert.equal(calls.persistCalls.length, 1); - assert.equal(calls.persistCalls[0].email, 'new.user@example.com'); - assert.equal(calls.persistCalls[0].options.preserveAccountIdentity, true); - assert.equal(calls.persistCalls[0].options.source, 'step8_add_email'); - assert.equal(calls.mailStates[0].email, 'new.user@example.com'); - assert.equal(calls.resolvedVerification.state.email, 'new.user@example.com'); - assert.equal(calls.resolvedVerification.options.targetEmail, 'new.user@example.com'); + assert.equal(calls.contentMessages.length, 0); + assert.equal(calls.resolvedStates.length, 0); + assert.equal(calls.persistCalls.length, 0); + assert.equal(calls.mailStates.length, 0); assert.deepStrictEqual(calls.setStates, [ { - step8VerificationTargetEmail: 'new.user@example.com', + step8VerificationTargetEmail: '', + loginVerificationRequestedAt: null, + }, + ]); + assert.deepStrictEqual(calls.completions, [ + { + step: 'fetch-login-code', + payload: { + loginVerificationRequestedAt: null, + skipLoginVerificationStep: true, + addEmailPage: true, + }, }, ]); }); -test('step 8 reruns step 7 with preserved phone login identity after add-email verification failure', async () => { +test('bind-email submits add-email and requires an email verification page', async () => { + const calls = { + contentMessages: [], + completions: [], + persistCalls: [], + setStates: [], + }; + let runtimeState = { + email: '', + password: 'secret', + oauthUrl: 'https://oauth.example/latest', + }; + + const executor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + completeNodeFromBackground: async (step, payload) => { + calls.completions.push({ step, payload }); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...runtimeState }), + getTabId: async () => 1, + persistRegistrationEmailState: async (state, email, options) => { + calls.persistCalls.push({ state, email, options }); + runtimeState = { + ...runtimeState, + email, + }; + }, + resolveSignupEmailForFlow: async (_state, options = {}) => { + assert.equal(options.preserveAccountIdentity, true); + return 'bind.user@example.com'; + }, + reuseOrCreateTab: async () => 1, + sendToContentScriptResilient: async (_source, message) => { + if (message.type === 'GET_LOGIN_AUTH_STATE') { + return { state: 'add_email_page', url: 'https://auth.openai.com/add-email' }; + } + calls.contentMessages.push(message); + assert.equal(message.type, 'SUBMIT_ADD_EMAIL'); + assert.equal(message.payload.email, 'bind.user@example.com'); + return { + submitted: true, + displayedEmail: 'bind.user@example.com', + url: 'https://auth.openai.com/email-verification', + }; + }, + setState: async (payload) => { + calls.setStates.push(payload); + runtimeState = { + ...runtimeState, + ...payload, + }; + }, + throwIfStopped: () => {}, + }); + + await executor.executeBindEmail({ + visibleStep: 9, + nodeId: 'bind-email', + oauthUrl: 'https://oauth.example/latest', + }); + + assert.equal(calls.contentMessages.length, 1); + assert.equal(calls.persistCalls.length, 1); + assert.equal(calls.persistCalls[0].options.source, 'bind_email'); + assert.deepStrictEqual(calls.completions, [ + { + step: 'bind-email', + payload: { + bindEmailSubmitted: true, + email: 'bind.user@example.com', + step8VerificationTargetEmail: 'bind.user@example.com', + }, + }, + ]); +}); + +test('bind-email skips on OAuth consent and rejects direct OAuth after submit', async () => { + const completions = []; + const skipExecutor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + completeNodeFromBackground: async (step, payload) => { + completions.push({ step, payload }); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getTabId: async () => 1, + reuseOrCreateTab: async () => 1, + sendToContentScriptResilient: async () => ({ state: 'oauth_consent_page' }), + setState: async () => {}, + throwIfStopped: () => {}, + }); + + await skipExecutor.executeBindEmail({ + visibleStep: 9, + nodeId: 'bind-email', + oauthUrl: 'https://oauth.example/latest', + }); + assert.deepStrictEqual(completions, [ + { + step: 'bind-email', + payload: { + directOAuthConsentPage: true, + bindEmailSubmitted: false, + }, + }, + ]); + + const directOauthExecutor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ email: '', oauthUrl: 'https://oauth.example/latest' }), + getTabId: async () => 1, + resolveSignupEmailForFlow: async () => 'bind.user@example.com', + reuseOrCreateTab: async () => 1, + sendToContentScriptResilient: async (_source, message) => { + if (message.type === 'GET_LOGIN_AUTH_STATE') { + return { state: 'add_email_page', url: 'https://auth.openai.com/add-email' }; + } + return { + submitted: true, + directOAuthConsentPage: true, + url: 'https://auth.openai.com/authorize', + }; + }, + setState: async () => {}, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => directOauthExecutor.executeBindEmail({ + visibleStep: 9, + oauthUrl: 'https://oauth.example/latest', + }), + /绑定邮箱提交后必须进入邮箱验证码页/ + ); +}); + +test('fetch-bind-email-code polls only after bind-email submitted', async () => { + const calls = { + resolveOptions: null, + setStates: [], + }; + const realDateNow = Date.now; + Date.now = () => 222000; + + const executor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + confirmCustomVerificationStepBypass: async () => {}, + getOAuthFlowRemainingMs: async () => 9000, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getMailConfig: () => ({ + provider: 'qq', + label: 'QQ 邮箱', + source: 'mail-qq', + url: 'https://mail.qq.com', + navigateOnReuse: false, + }), + getTabId: async () => 1, + HOTMAIL_PROVIDER: 'hotmail-api', + isTabAlive: async () => true, + LUCKMAIL_PROVIDER: 'luckmail-api', + resolveVerificationStep: async (_step, _state, _mail, options) => { + calls.resolveOptions = options; + }, + reuseOrCreateTab: async () => 1, + sendToContentScriptResilient: async () => ({ + state: 'verification_page', + displayedEmail: 'bind.user@example.com', + url: 'https://auth.openai.com/email-verification', + }), + setState: async (payload) => { + calls.setStates.push(payload); + }, + shouldUseCustomRegistrationEmail: () => false, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, + throwIfStopped: () => {}, + }); + + try { + await executor.executeFetchBindEmailCode({ + visibleStep: 10, + nodeId: 'fetch-bind-email-code', + bindEmailSubmitted: true, + email: 'bind.user@example.com', + oauthUrl: 'https://oauth.example/latest', + }); + } finally { + Date.now = realDateNow; + } + + assert.equal(calls.resolveOptions.completionStep, 10); + assert.equal(calls.resolveOptions.sessionKey, '10:222000'); + assert.equal(calls.resolveOptions.targetEmail, 'bind.user@example.com'); + assert.deepStrictEqual(calls.setStates, [ + { + step8VerificationTargetEmail: 'bind.user@example.com', + }, + ]); +}); + +test('fetch-bind-email-code rejects unexpected pages after bind-email submitted', async () => { + const executor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getTabId: async () => 1, + reuseOrCreateTab: async () => 1, + sendToContentScriptResilient: async () => ({ + state: 'oauth_consent_page', + url: 'https://auth.openai.com/authorize', + }), + setState: async () => {}, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => executor.executeFetchBindEmailCode({ + visibleStep: 10, + bindEmailSubmitted: true, + oauthUrl: 'https://oauth.example/latest', + }), + /绑定邮箱提交后不应直接进入 OAuth 授权页/ + ); + + const notSubmittedCompletions = []; + const skipExecutor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + completeNodeFromBackground: async (step, payload) => { + notSubmittedCompletions.push({ step, payload }); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getTabId: async () => 1, + reuseOrCreateTab: async () => 1, + sendToContentScriptResilient: async () => ({ state: 'oauth_consent_page' }), + setState: async () => {}, + throwIfStopped: () => {}, + }); + + await skipExecutor.executeFetchBindEmailCode({ + visibleStep: 10, + nodeId: 'fetch-bind-email-code', + bindEmailSubmitted: false, + oauthUrl: 'https://oauth.example/latest', + }); + assert.deepStrictEqual(notSubmittedCompletions, [ + { + step: 'fetch-bind-email-code', + payload: { + directOAuthConsentPage: true, + bindEmailCodeSkipped: true, + }, + }, + ]); +}); + +test('step 8 does not submit or recover add-email inside fetch-login-code', async () => { const calls = { ensureCalls: 0, resolveCalls: 0, rerunStates: [], contentMessages: [], + completions: [], }; let runtimeState = { visibleStep: 8, @@ -388,6 +830,9 @@ test('step 8 reruns step 7 with preserved phone login identity after add-email v }, }, CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + completeNodeFromBackground: async (step, payload) => { + calls.completions.push({ step, payload }); + }, confirmCustomVerificationStepBypass: async () => {}, ensureStep8VerificationPageReady: async () => { calls.ensureCalls += 1; @@ -463,16 +908,23 @@ test('step 8 reruns step 7 with preserved phone login identity after add-email v await executor.executeStep8({ ...runtimeState }); - assert.equal(calls.contentMessages.length, 1); - assert.equal(calls.resolveCalls, 2); - assert.equal(calls.rerunStates.length, 1); - assert.equal(calls.rerunStates[0].email, 'new.user@example.com'); - assert.equal(calls.rerunStates[0].accountIdentifierType, 'phone'); - assert.equal(calls.rerunStates[0].accountIdentifier, '+447780579093'); - assert.equal(calls.rerunStates[0].signupPhoneNumber, '+447780579093'); + assert.equal(calls.ensureCalls, 1); + assert.equal(calls.contentMessages.length, 0); + assert.equal(calls.resolveCalls, 0); + assert.equal(calls.rerunStates.length, 0); + assert.deepStrictEqual(calls.completions, [ + { + step: 'fetch-login-code', + payload: { + loginVerificationRequestedAt: null, + skipLoginVerificationStep: true, + addEmailPage: true, + }, + }, + ]); }); -test('step 8 add-email rereads persisted phone identity before rerunning step 7', async () => { +test('step 8 rejects add-email page in email login mode', async () => { const calls = { resolveCalls: 0, rerunStates: [], @@ -563,16 +1015,14 @@ test('step 8 add-email rereads persisted phone identity before rerunning step 7' await assert.rejects( () => executor.executeStep8({ ...runtimeState }), - /STEP8_RESTART_STEP7::/ + /邮箱注册模式不应进入添加邮箱页/ ); - assert.equal(calls.resolveCalls, 2); - assert.equal(calls.rerunStates.length, 1); - assert.equal(calls.rerunStates[0].accountIdentifierType, 'phone'); - assert.equal(calls.rerunStates[0].signupPhoneNumber, '+447780579093'); + assert.equal(calls.resolveCalls, 0); + assert.equal(calls.rerunStates.length, 0); }); -test('step 8 email_in_use recovery preserves the previous registration baseline', async () => { +test('step 8 does not run add-email email_in_use recovery in email login mode', async () => { const calls = { contentCalls: 0, setStates: [], @@ -588,15 +1038,6 @@ test('step 8 email_in_use recovery preserves the previous registration baseline' }, }, }, - buildRegistrationEmailStateUpdates: () => ({ - email: null, - registrationEmailState: { - current: '', - previous: 'old.user@example.com', - source: 'step8_recovery', - updatedAt: 123, - }, - }), CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', confirmCustomVerificationStepBypass: async () => {}, ensureStep8VerificationPageReady: async () => ({ @@ -653,30 +1094,24 @@ test('step 8 email_in_use recovery preserves the previous registration baseline' throwIfStopped: () => {}, }); - await executor.executeStep8({ - email: 'old.user@example.com', - registrationEmailState: { - current: 'old.user@example.com', - previous: 'old.user@example.com', - source: 'generated:duck', - updatedAt: 1, - }, - oauthUrl: 'https://auth.openai.com/add-email', - password: 'secret', - visibleStep: 8, - }); + await assert.rejects( + () => executor.executeStep8({ + email: 'old.user@example.com', + registrationEmailState: { + current: 'old.user@example.com', + previous: 'old.user@example.com', + source: 'generated:duck', + updatedAt: 1, + }, + oauthUrl: 'https://auth.openai.com/add-email', + password: 'secret', + visibleStep: 8, + }), + /邮箱注册模式不应进入添加邮箱页/ + ); - assert.deepStrictEqual(calls.setStates[0], { - email: null, - registrationEmailState: { - current: '', - previous: 'old.user@example.com', - source: 'step8_recovery', - updatedAt: 123, - }, - step8VerificationTargetEmail: '', - loginVerificationRequestedAt: null, - }); + assert.equal(calls.contentCalls, 0); + assert.deepStrictEqual(calls.setStates, []); }); test('Plus login-code step reuses step 8 verification logic but completes visible step 11', async () => { diff --git a/tests/five-sim-provider.test.js b/tests/five-sim-provider.test.js index 4ee374f..87771a1 100644 --- a/tests/five-sim-provider.test.js +++ b/tests/five-sim-provider.test.js @@ -192,15 +192,15 @@ test('5sim provider rejects maxPrice with custom operator before buying', async }); await assert.rejects( - () => provider.requestActivation({ - fiveSimApiKey: 'demo-key', - fiveSimCountryId: 'vietnam', - fiveSimCountryLabel: '瓒婂崡 (Vietnam)', - fiveSimMaxPrice: '12', - fiveSimOperator: 'virtual21', - }), - /maxPrice only works when operator is "any"/ - ); + () => provider.requestActivation({ + fiveSimApiKey: 'demo-key', + fiveSimCountryId: 'vietnam', + fiveSimCountryLabel: '瓒婂崡 (Vietnam)', + fiveSimMaxPrice: '12', + fiveSimOperator: 'virtual21', + }), + /价格上限仅支持运营商为 "any"/ + ); assert.deepStrictEqual(requests, []); }); diff --git a/tests/phone-sms-provider-registry.test.js b/tests/phone-sms-provider-registry.test.js index fdfce9e..077cd0b 100644 --- a/tests/phone-sms-provider-registry.test.js +++ b/tests/phone-sms-provider-registry.test.js @@ -40,5 +40,5 @@ test('phone sms provider registry normalizes ids, order and labels consistently' registry.createProvider('5sim', { foo: 1 }), { provider: '5sim', deps: { foo: 1 } } ); - assert.throws(() => registry.createProvider('nexsms'), /Phone SMS provider module is not loaded: nexsms/); + assert.throws(() => registry.createProvider('nexsms'), /接码平台模块未加载:nexsms/); }); diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index d083bfb..9e4e35a 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -1634,7 +1634,7 @@ test('phone verification helper fails fast when HeroSMS country list is empty', heroSmsCountryId: 0, heroSmsCountryFallback: [], }), - /HeroSMS countries are empty/i + /HeroSMS 未选择国家/ ); }); @@ -1968,7 +1968,7 @@ test('phone verification helper rejects HeroSMS WRONG_MAX_PRICE below configured await assert.rejects( helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsMinPrice: '0.07' }), - /below configured minPrice=0\.07/i + /低于当前配置的最低购买价 0\.07/ ); const actions = requests.map((requestUrl) => `${requestUrl.searchParams.get('action')}:${requestUrl.searchParams.get('maxPrice') || ''}`); @@ -2002,7 +2002,7 @@ test('phone verification helper rejects reversed price range before fetching pri heroSmsMinPrice: '0.2', heroSmsMaxPrice: '0.1', }), - /price range is invalid/i + /价格区间无效/ ); assert.equal(fetchCalled, false); }); @@ -2039,7 +2039,7 @@ test('phone verification helper stops when WRONG_MAX_PRICE exceeds configured ma await assert.rejects( helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.05' }), - /exceeds configured maxPrice=0\.05/i + /超过当前配置的价格上限 0\.05/ ); const actions = requests.map((requestUrl) => `${requestUrl.searchParams.get('action')}:${requestUrl.searchParams.get('maxPrice') || ''}`); @@ -2403,16 +2403,16 @@ test('phone verification helper rejects 5sim maxPrice with custom operator befor throwIfStopped: () => {}, }); - await assert.rejects( - () => helpers.requestPhoneActivation({ - phoneSmsProvider: '5sim', - fiveSimApiKey: 'five-token', - fiveSimCountryOrder: ['vietnam'], - fiveSimOperator: 'virtual21', - fiveSimMaxPrice: '0.1', - heroSmsActivationRetryRounds: 1, - }), - /maxPrice only works when operator is "any"/ + await assert.rejects( + () => helpers.requestPhoneActivation({ + phoneSmsProvider: '5sim', + fiveSimApiKey: 'five-token', + fiveSimCountryOrder: ['vietnam'], + fiveSimOperator: 'virtual21', + fiveSimMaxPrice: '0.1', + heroSmsActivationRetryRounds: 1, + }), + /价格上限仅支持运营商为 "any"/ ); assert.deepStrictEqual(requests, []); }); @@ -8105,14 +8105,14 @@ test('phone verification helper logs no-supply diagnostics with consecutive stre phoneVerificationPage: false, url: 'https://auth.openai.com/add-phone', }), - /all provider candidates failed to acquire number/i + /所有接码平台候选均未获取到手机号/ ); await runOnce(); await runOnce(); const diagnosticsLogs = logs - .filter((entry) => String(entry.message || '').includes('diagnostics: 无号连续失败')); + .filter((entry) => String(entry.message || '').includes('步骤 9 诊断:无号连续失败')); assert.equal(diagnosticsLogs.length >= 2, true); assert.equal(diagnosticsLogs.every((entry) => entry.options?.step === 9), true); @@ -8120,15 +8120,15 @@ test('phone verification helper logs no-supply diagnostics with consecutive stre assert.equal(diagnosticsLogs.some((entry) => entry.message.includes('无号连续失败 1 次')), true); assert.equal(diagnosticsLogs.some((entry) => entry.message.includes('无号连续失败 2 次')), true); assert.equal( - diagnosticsLogs.some((entry) => entry.message.includes('priceRange=0.04~0.06')), + diagnosticsLogs.some((entry) => entry.message.includes('价格区间=0.04~0.06')), true ); assert.equal( - diagnosticsLogs.some((entry) => entry.message.includes('minPrice=0.04')), + diagnosticsLogs.some((entry) => entry.message.includes('最低价=0.04')), true ); assert.equal( - diagnosticsLogs.some((entry) => entry.message.includes('maxPrice=0.06')), + diagnosticsLogs.some((entry) => entry.message.includes('最高价=0.06')), true ); assert.equal( @@ -8139,6 +8139,63 @@ test('phone verification helper logs no-supply diagnostics with consecutive stre assert.equal(requests.some((entry) => entry.searchParams.get('action') === 'getNumber'), true); }); +test('phone verification helper localizes HeroSMS BAD_KEY acquisition failure', async () => { + let currentState = { + heroSmsApiKey: 'bad-key', + heroSmsCountryId: 52, + heroSmsCountryLabel: 'Thailand', + heroSmsCountryFallback: [], + currentPhoneActivation: null, + reusablePhoneActivation: null, + phoneVerificationReplacementLimit: 1, + }; + + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const action = new URL(url).searchParams.get('action'); + if (action === 'getPrices') { + return { + ok: true, + text: async () => buildHeroSmsPricesPayload({ country: '52', cost: 0.05, count: 20 }), + }; + } + if (action === 'getNumber' || action === 'getNumberV2') { + return { + ok: true, + text: async () => 'BAD_KEY', + }; + } + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...currentState }), + sendToContentScriptResilient: async (_source, message) => { + throw new Error(`Unexpected content-script message: ${message.type}`); + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + await assert.rejects( + helpers.completePhoneVerificationFlow(1, { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }), + (error) => { + assert.match(error.message, /步骤 9:所有接码平台候选均未获取到手机号/); + assert.match(error.message, /HeroSMS:获取手机号失败:API Key 无效(BAD_KEY)/); + assert.doesNotMatch(error.message, /all provider candidates failed|failed to acquire number|HeroSMS getNumber failed/i); + return true; + } + ); +}); + test('phone verification helper routes 5sim buy, check, and finish by current activation provider', async () => { const requests = []; let currentState = { diff --git a/tests/source-registry-module.test.js b/tests/source-registry-module.test.js index 66c7ea2..7ebe678 100644 --- a/tests/source-registry-module.test.js +++ b/tests/source-registry-module.test.js @@ -54,6 +54,9 @@ test('shared source registry exposes canonical source, alias, detection, and rea assert.equal(registry.shouldReportReadyForFrame('unknown-source', false), false); assert.equal(registry.getCleanupOwnerSource('oauth-localhost-callback'), 'openai-auth'); assert.equal(registry.driverAcceptsCommand('openai-auth', 'submit-signup-email'), true); + assert.equal(registry.driverAcceptsCommand('openai-auth', 'post-login-phone-verification'), true); + assert.equal(registry.driverAcceptsCommand('openai-auth', 'bind-email'), true); + assert.equal(registry.driverAcceptsCommand('openai-auth', 'fetch-bind-email-code'), true); assert.equal(registry.driverAcceptsCommand('content/platform-panel', 'platform-verify'), true); assert.equal(registry.driverAcceptsCommand('openai-auth', 'platform-verify'), false); }); diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js index af176b2..cf0dc0d 100644 --- a/tests/step-definitions-module.test.js +++ b/tests/step-definitions-module.test.js @@ -15,7 +15,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }); assert.equal(Array.isArray(steps), true); - assert.equal(steps.length, 10); + assert.equal(steps.length, 11); assert.equal(steps.every((step) => step.flowId === 'openai'), true); assert.deepStrictEqual( steps.map((step) => step.order), @@ -32,6 +32,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'wait-registration-success', 'oauth-login', 'fetch-login-code', + 'post-login-phone-verification', 'confirm-oauth', 'platform-verify', ] @@ -40,6 +41,23 @@ test('step definitions module exposes ordered normal and Plus step metadata', () assert.equal(steps[5].title, '等待注册成功'); assert.equal(phoneSteps[1].title, '注册并输入手机号'); assert.equal(phoneSteps[3].title, '获取手机验证码'); + assert.deepStrictEqual( + phoneSteps.map((step) => step.key), + [ + 'open-chatgpt', + 'submit-signup-email', + 'fill-password', + 'fetch-signup-code', + 'fill-profile', + 'wait-registration-success', + 'oauth-login', + 'fetch-login-code', + 'bind-email', + 'fetch-bind-email-code', + 'confirm-oauth', + 'platform-verify', + ] + ); assert.deepStrictEqual( plusSteps.map((step) => step.key), @@ -55,6 +73,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'plus-checkout-return', 'oauth-login', 'fetch-login-code', + 'post-login-phone-verification', 'confirm-oauth', 'platform-verify', ] @@ -64,11 +83,33 @@ test('step definitions module exposes ordered normal and Plus step metadata', () assert.equal(plusSteps.find((step) => step.key === 'paypal-approve')?.title, 'PayPal 登录与授权'); assert.equal(plusPhoneSteps[1].title, '注册并输入手机号'); assert.equal(plusPhoneSteps[3].title, '获取手机验证码'); + assert.deepStrictEqual( + plusPhoneSteps.map((step) => step.key), + [ + 'open-chatgpt', + 'submit-signup-email', + 'fill-password', + 'fetch-signup-code', + 'fill-profile', + 'plus-checkout-create', + 'plus-checkout-billing', + 'paypal-approve', + 'plus-checkout-return', + 'oauth-login', + 'fetch-login-code', + 'bind-email', + 'fetch-bind-email-code', + 'confirm-oauth', + 'platform-verify', + ] + ); assert.equal(goPaySteps.some((step) => step.key === 'paypal-approve'), false); assert.equal(api.getStepById(8, { plusModeEnabled: true, plusPaymentMethod: 'gopay' }), null); assert.equal(api.getPlusPaymentStepTitle({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), ''); - 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); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]); + assert.equal(api.getLastStepId({ plusModeEnabled: true }), 14); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); + assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone' }), 15); assert.equal(api.hasFlow('openai'), true); assert.equal(api.hasFlow('site-a'), false); assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai']); @@ -89,12 +130,13 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'gopay-subscription-confirm', 'oauth-login', 'fetch-login-code', + 'post-login-phone-verification', 'confirm-oauth', 'platform-verify', ] ); - assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13]); - assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), 13); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14]); + assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), 14); assert.equal(goPaySteps[5].title, '打开 GoPay 订阅页'); assert.equal(goPaySteps[6].title, '等待 GoPay 订阅确认'); @@ -110,12 +152,13 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'plus-checkout-billing', 'oauth-login', 'fetch-login-code', + 'post-login-phone-verification', 'confirm-oauth', 'platform-verify', ] ); - assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13]); - assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 13); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14]); + assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 14); assert.equal(gpcSteps[5].title, '创建 GPC 订单'); assert.equal(gpcSteps[6].title, '等待 GPC 任务完成'); }); diff --git a/tests/step8-retry-page-recovery.test.js b/tests/step8-retry-page-recovery.test.js index 5794f31..0e65aa2 100644 --- a/tests/step8-retry-page-recovery.test.js +++ b/tests/step8-retry-page-recovery.test.js @@ -241,7 +241,7 @@ return { assert.match(String(result?.url || ''), /chatgpt\.com/); }); -test('step 8 ready check completes phone verification flow before waiting for OAuth consent', async () => { +test('step 8 ready check rejects add-phone instead of completing phone verification', async () => { const api = new Function(` let pollCount = 0; const phoneVerificationCalls = []; @@ -292,28 +292,13 @@ return { }; `)(); - const { result, phoneVerificationCalls } = await api.run(); - - assert.deepStrictEqual(phoneVerificationCalls, [ - { - tabId: 88, - pageState: { - url: 'https://auth.openai.com/add-phone', - addPhonePage: true, - phoneVerificationPage: false, - consentReady: false, - }, - }, - ]); - assert.deepStrictEqual(result, { - url: 'https://auth.openai.com/authorize', - addPhonePage: false, - phoneVerificationPage: false, - consentReady: true, - }); + await assert.rejects( + () => api.run(), + /自动确认 OAuth 只处理 OAuth 授权页/ + ); }); -test('step 8 ready check blocks phone verification flow when sms toggle is disabled', async () => { +test('step 8 ready check rejects phone pages before OAuth confirmation', async () => { const api = new Function(` const phoneVerificationCalls = []; @@ -358,6 +343,6 @@ return { const { error, phoneVerificationCalls } = await api.run(); - assert.match(String(error?.message || ''), /未开启接码功能/); + assert.match(String(error?.message || ''), /自动确认 OAuth 只处理 OAuth 授权页/); assert.deepStrictEqual(phoneVerificationCalls, []); });