From 612fccd4c60176fb8499ca6972da92c1ae42c031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B4=E5=9C=A3=E4=BD=91?= Date: Wed, 6 May 2026 17:36:29 +0800 Subject: [PATCH 1/5] fix(gpc): allow local helper for whatsapp otp --- background/steps/fill-plus-checkout.js | 3 +- sidepanel/sidepanel.js | 9 +-- ...us-checkout-billing-tab-resolution.test.js | 59 +++++++++++++++++++ tests/sidepanel-plus-payment-method.test.js | 16 ++--- 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index 89133ec..78168f2 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -423,8 +423,7 @@ } await addLog(`步骤 7:GPC 模式开始 OTP 验证(reference_id: ${referenceId})...`, 'info'); let otp = ''; - const useLocalSmsHelper = Boolean(state?.gopayHelperLocalSmsHelperEnabled) - && normalizeGpcOtpChannel(state?.gopayHelperOtpChannel) === 'sms'; + const useLocalSmsHelper = Boolean(state?.gopayHelperLocalSmsHelperEnabled); if (useLocalSmsHelper) { try { await addLog('步骤 7:正在从本地 SMS Helper 等待 GPC OTP...', 'info'); diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 62525a5..dbbc2b9 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -3147,7 +3147,7 @@ function collectSettingsPayload() { ? selectGpcHelperOtpChannel.value : (latestState?.gopayHelperOtpChannel || 'whatsapp') ); - const selectedGpcLocalSmsHelperEnabled = selectedGpcOtpChannel === 'sms' && Boolean( + const selectedGpcLocalSmsHelperEnabled = Boolean( typeof inputGpcHelperLocalSmsEnabled !== 'undefined' && inputGpcHelperLocalSmsEnabled ? inputGpcHelperLocalSmsEnabled.checked : latestState?.gopayHelperLocalSmsHelperEnabled @@ -7014,8 +7014,8 @@ function updatePlusModeUI() { ? normalizePlusPaymentMethod(selectPlusPaymentMethod.value) : method; const gpcRowsVisible = enabled && selectedMethod === gpcValue; - const localSmsControlsVisible = gpcRowsVisible && gpcOtpChannel === 'sms'; - const effectiveLocalSmsEnabled = gpcOtpChannel === 'sms' && localSmsEnabled; + const localSmsControlsVisible = gpcRowsVisible; + const effectiveLocalSmsEnabled = localSmsEnabled; if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) { selectPlusPaymentMethod.value = method; if (selectPlusPaymentMethod.style) { @@ -11229,9 +11229,6 @@ selectPlusPaymentMethod?.addEventListener('change', () => { scheduleSettingsAutoSave(); }); input?.addEventListener('change', () => { - if (input === inputGpcHelperLocalSmsEnabled && input.checked && selectGpcHelperOtpChannel) { - selectGpcHelperOtpChannel.value = 'sms'; - } if (input === selectGpcHelperOtpChannel || input === inputGpcHelperLocalSmsEnabled) { updatePlusModeUI(); } diff --git a/tests/plus-checkout-billing-tab-resolution.test.js b/tests/plus-checkout-billing-tab-resolution.test.js index 43005ea..4fabdec 100644 --- a/tests/plus-checkout-billing-tab-resolution.test.js +++ b/tests/plus-checkout-billing-tab-resolution.test.js @@ -932,6 +932,65 @@ test('GPC billing reads OTP from local SMS helper when enabled', async () => { assert.equal(events.completed[0].step, 7); }); +test('GPC billing can read WhatsApp OTP from local helper when enabled', async () => { + const fetchCalls = []; + const { events, executor } = createExecutorHarness({ + frames: [], + stateByFrame: {}, + fetchImpl: async (url, options = {}) => { + fetchCalls.push({ url, options }); + if (url.startsWith('http://127.0.0.1:18767/otp')) { + return { + ok: true, + status: 200, + json: async () => ({ ok: true, otp: '765432', message_id: 'wa-1' }), + }; + } + if (url.endsWith('/api/gopay/otp')) { + return { + ok: true, + status: 200, + json: async () => ({ reference_id: 'ref_wa', challenge_id: 'challenge_wa' }), + }; + } + if (url.endsWith('/api/gopay/pin')) { + return { + ok: true, + status: 200, + json: async () => ({ stage: 'gopay_complete' }), + }; + } + throw new Error(`unexpected url: ${url}`); + }, + }); + + await executor.executePlusCheckoutBilling({ + plusPaymentMethod: 'gpc-helper', + plusCheckoutSource: 'gpc-helper', + gopayHelperReferenceId: 'ref_wa', + gopayHelperApiUrl: 'https://gopay.hwork.pro/', + gopayHelperPin: '654321', + gopayHelperCardKey: 'card_wa', + gopayHelperOtpChannel: 'whatsapp', + gopayHelperLocalSmsHelperEnabled: true, + gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767', + gopayHelperPhoneNumber: '+8613800138000', + }); + + assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false); + assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '765432'), true); + const helperUrl = new URL(fetchCalls[0].url); + assert.equal(helperUrl.origin + helperUrl.pathname, 'http://127.0.0.1:18767/otp'); + assert.equal(helperUrl.searchParams.get('reference_id'), 'ref_wa'); + assert.equal(helperUrl.searchParams.get('phone_number'), '+8613800138000'); + assert.deepEqual(JSON.parse(fetchCalls[1].options.body), { + reference_id: 'ref_wa', + otp: '765432', + card_key: 'card_wa', + }); + assert.equal(events.completed[0].step, 7); +}); + test('GPC billing retries OTP with compatibility field after HTTP 400', async () => { const fetchCalls = []; let currentState = { diff --git a/tests/sidepanel-plus-payment-method.test.js b/tests/sidepanel-plus-payment-method.test.js index b05f552..dc0f5fa 100644 --- a/tests/sidepanel-plus-payment-method.test.js +++ b/tests/sidepanel-plus-payment-method.test.js @@ -257,24 +257,20 @@ return { assert.equal(api.rows.rowGpcHelperCardKey.style.display, ''); assert.equal(api.rows.rowGpcHelperPhone.style.display, ''); assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, ''); - assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none'); + assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, ''); assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none'); assert.match(api.plusPaymentMethodCaption.textContent, /GPC/); - api.selectGpcHelperOtpChannel.value = 'sms'; - api.updatePlusModeUI(); - assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, ''); - assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none'); - api.inputGpcHelperLocalSmsEnabled.checked = true; api.updatePlusModeUI(); + assert.equal(api.selectGpcHelperOtpChannel.value, 'whatsapp'); assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, ''); - api.selectGpcHelperOtpChannel.value = 'whatsapp'; + api.selectGpcHelperOtpChannel.value = 'sms'; api.updatePlusModeUI(); - assert.equal(api.inputGpcHelperLocalSmsEnabled.checked, false); - assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none'); - assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none'); + assert.equal(api.inputGpcHelperLocalSmsEnabled.checked, true); + assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, ''); + assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, ''); api.selectPlusPaymentMethod.value = 'gopay'; api.updatePlusModeUI(); From 4f8a149a59b1af6f1dc850622d7e4e050fb7c09b Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Wed, 6 May 2026 17:52:51 +0800 Subject: [PATCH 2/5] docs: align GPC local helper OTP wording --- sidepanel/sidepanel.html | 6 +++--- 项目完整链路说明.md | 4 ++-- 项目文件结构说明.md | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 1584dd9..6c96092 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -323,17 +323,17 @@ diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 34844fd..ea9422e 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -46,7 +46,7 @@ - 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro` 或 `v` 版本误显示为比 `Ultra` 更新 - 展示一个单独的“接码”开关、注册方式 `signupMethod` 与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim / NexSMS。普通模式下开启接码后可把注册方式切到手机号注册,并在 OAuth 登录链路命中手机号登录验证码页时继续复用同一手机号续跑短信验证 - 侧栏在接码卡内提供一个独立运行态“注册手机号”输入框,位于接码订单运行状态下方;第 2 步自动拿到号码后立即回填,用户手动接管手机号注册时也可以直接改写这一个运行态槽位。这个输入框表达账号身份,不等同于接码订单;后续自动拉短信仍依赖 `signupPhoneActivation / signupPhoneCompletedActivation` -- 展示 `Plus 模式` 开关与 Plus 支付方式配置;支付方式支持 PayPal / GoPay / GPC,PayPal 展示账号池下拉与添加按钮,GoPay 展示手机号和 PIN,GPC 展示卡密、专用手机号、OTP 渠道、本地 macOS SMS helper 开关与 URL、PIN;步骤列表切换为 Plus 模式 13 步定义,普通模式的注册成功等待步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步 +- 展示 `Plus 模式` 开关与 Plus 支付方式配置;支付方式支持 PayPal / GoPay / GPC,PayPal 展示账号池下拉与添加按钮,GoPay 展示手机号和 PIN,GPC 展示卡密、专用手机号、OTP 渠道、本地 OTP helper 开关与 URL、PIN;步骤列表切换为 Plus 模式 13 步定义,普通模式的注册成功等待步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步 - 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作 ### 2.2 Background Service Worker @@ -548,7 +548,7 @@ Plus 模式可见步骤: 1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。 2. 第 6 步 `创建 Plus Checkout`:打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR` 与 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`;GoPay 使用 `ID / IDR` 与 `https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`;GPC helper 模式改为把 accessToken、卡密、手机号、`otp_channel` 等提交给 helper API 创建 GPC 订单。 -3. 第 7 步 `填写账单并提交订阅`:按 `plusPaymentMethod` 选择 PayPal、GoPay 或 GPC。PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是先提交 OTP,再提交 PIN。若侧栏启用本地 macOS SMS helper,后台会先轮询 `gopayHelperLocalSmsHelperUrl` 的 `/otp` 接口读取 SMS OTP,读取失败再回退到手动 OTP 输入弹窗。 +3. 第 7 步 `填写账单并提交订阅`:按 `plusPaymentMethod` 选择 PayPal、GoPay 或 GPC。PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是先提交 OTP,再提交 PIN。若侧栏启用本地 OTP helper,后台会先轮询 `gopayHelperLocalSmsHelperUrl` 的 `/otp` 接口读取当前 OTP 通道的验证码,读取失败再回退到手动 OTP 输入弹窗;仓库内置的 `scripts/gpc_sms_helper_macos.py` 仍是 macOS Messages 短信读取实现,其他通道需要提供兼容的本地 `/otp` 接口。 4. 第 8 步按当前支付方式显示为 `PayPal 登录与授权` 或 `GoPay 手机验证与授权`,底层 step key 仍为 `paypal-approve`。PayPal 模式后台优先读取侧边栏当前选中的 PayPal 账号;GoPay 模式读取侧边栏的国家区号、手机号、可选验证码和 PIN,先在 GoPay 页面填写手机号;验证码优先用侧边栏已填值,否则弹出插件输入框让用户手动填写,提交后继续填写 PIN。 5. 第 9 步 `订阅回跳确认`:等待 PayPal / GoPay 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。 6. 第 10 步:复用原 Step 7 OAuth 登录执行器,但状态和日志按 Plus 可见第 10 步记录。 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 4164348..e47682e 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -150,7 +150,7 @@ receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱` 时 额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收 码或从 QQ / 网易 / Gmail 转发目标邮箱收码;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密 - 钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关与 PayPal 账号下拉框,右侧使用公共表单弹窗添加账号;GPC Plus 配置额外提供 OTP 渠道选择、本地 macOS SMS helper 开关和 helper URL;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;设置卡片还新增“授权总超时”开关,用于控制 Step 7 后链 5 分钟总预算。 + 钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关与 PayPal 账号下拉框,右侧使用公共表单弹窗添加账号;GPC Plus 配置额外提供 OTP 渠道选择、本地 OTP helper 开关和 helper URL;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;设置卡片还新增“授权总超时”开关,用于控制 Step 7 后链 5 分钟总预算。 - `sidepanel/paypal-manager.js`:侧边栏 PayPal 账号管理器,负责 Plus 模式下的账号下拉框渲染、添加账号弹窗、保存账号与切换当前账号。 - `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装 配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 账号记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启 @@ -215,7 +215,7 @@ - `tests/paypal-approve-detection.test.js`:测试 Plus 第 8 步后台执行器对 PayPal 标签页发现、分离式账号/密码页识别、联合登录页识别,以及登录后直接离开 PayPal 页的分支判断。 - `tests/paypal-flow-content.test.js`:测试 PayPal 内容脚本对可见邮箱/密码输入框的识别,并覆盖邮箱页即使已预填相同账号也会先清空再重填后继续下一步。 - `tests/gpc-sms-helper-script.test.js`:测试 GPC macOS 本地短信 helper 在非 macOS 环境下会提示平台与 iPhone 短信转发要求。 -- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe;当前也覆盖 GPC helper 的本地 SMS OTP 自动读取分支。 +- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe;当前也覆盖 GPC helper 的本地 OTP 自动读取分支。 - `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。 - `tests/cloudflare-temp-email-utils.test.js`:测试 Cloudflare Temp Email 工具层的 URL、域名、邮件解析逻辑。 - `tests/content-utils.test.js`:测试内容脚本公共工具层对 `mail.126.com` 等网页邮箱来源的识别逻辑。 From b6777edbc8d6017edc9c6da1b9428955dd109532 Mon Sep 17 00:00:00 2001 From: initiatione <269353933@qq.com> Date: Wed, 6 May 2026 18:16:59 +0800 Subject: [PATCH 3/5] Add HeroSMS free phone reuse --- background.js | 70 ++ background/message-router.js | 19 + background/phone-verification-flow.js | 776 ++++++++++++++++- content/phone-auth.js | 74 ++ content/signup-page.js | 4 + sidepanel/sidepanel.css | 14 + sidepanel/sidepanel.html | 30 + sidepanel/sidepanel.js | 192 +++- .../background-message-router-module.test.js | 74 ++ tests/phone-auth-country-match.test.js | 49 ++ tests/phone-verification-flow.test.js | 819 ++++++++++++++++++ tests/run-count-unlimited.test.js | 2 + ...epanel-phone-verification-settings.test.js | 62 ++ 13 files changed, 2170 insertions(+), 15 deletions(-) diff --git a/background.js b/background.js index 74003d3..bf00a00 100644 --- a/background.js +++ b/background.js @@ -632,6 +632,8 @@ const PERSISTED_SETTING_DEFAULTS = { autoRunDelayMinutes: 30, autoStepDelaySeconds: null, phoneVerificationEnabled: false, + freePhoneReuseEnabled: true, + freePhoneReuseAutoEnabled: true, signupMethod: DEFAULT_SIGNUP_METHOD, phoneSmsProvider: DEFAULT_PHONE_SMS_PROVIDER, phoneSmsProviderOrder: [], @@ -798,6 +800,7 @@ const DEFAULT_STATE = { currentPhoneVerificationCountdownWindowIndex: 0, currentPhoneVerificationCountdownWindowTotal: 0, reusablePhoneActivation: null, + freeReusablePhoneActivation: null, phoneReusableActivationPool: [], signupPhoneNumber: '', signupPhoneActivation: null, @@ -2286,6 +2289,8 @@ function normalizePersistentSettingValue(key, value) { case 'gopayHelperLocalSmsHelperEnabled': case 'autoRunDelayEnabled': case 'phoneVerificationEnabled': + case 'freePhoneReuseEnabled': + case 'freePhoneReuseAutoEnabled': case 'plusModeEnabled': return Boolean(value); case 'phoneSmsProvider': @@ -3093,6 +3098,7 @@ async function resetState() { 'tabRegistry', 'sourceLastUrls', 'reusablePhoneActivation', + 'freeReusablePhoneActivation', 'phoneReusableActivationPool', 'luckmailApiKey', 'luckmailBaseUrl', @@ -3132,6 +3138,19 @@ async function resetState() { .map((entry) => normalizePhonePreferredActivation(entry)) .filter(Boolean) : []; + const freeReusablePhoneActivation = ( + prev.freeReusablePhoneActivation + && typeof prev.freeReusablePhoneActivation === 'object' + && !Array.isArray(prev.freeReusablePhoneActivation) + && String( + prev.freeReusablePhoneActivation.phoneNumber + ?? prev.freeReusablePhoneActivation.number + ?? prev.freeReusablePhoneActivation.phone + ?? '' + ).trim() + ) + ? prev.freeReusablePhoneActivation + : null; await chrome.storage.session.clear(); await chrome.storage.session.set({ ...DEFAULT_STATE, @@ -3154,6 +3173,8 @@ async function resetState() { currentLuckmailMailCursor: null, // Keep reusable phone activation across round resets so the same number can be reactivated up to maxUses. reusablePhoneActivation, + // Keep free reuse phone activation until the user clears or the flow retires it. + freeReusablePhoneActivation, phoneReusableActivationPool, preferredIcloudHost: prev.preferredIcloudHost || '', }); @@ -6613,6 +6634,53 @@ async function finalizePhoneActivationAfterSuccessfulFlow(state) { return phoneVerificationHelpers.finalizePendingPhoneActivationConfirmation(state); } +async function clearFreeReusablePhoneActivation() { + await setState({ freeReusablePhoneActivation: null }); + broadcastDataUpdate({ freeReusablePhoneActivation: null }); + await addLog('已清除白嫖复用手机号记录。', 'ok'); + return { ok: true, freeReusablePhoneActivation: null }; +} + +async function setFreeReusablePhoneActivation(record = {}) { + const phoneNumber = String(record.phoneNumber || record.number || record.phone || '').trim(); + if (!phoneNumber) { + throw new Error('请先填写白嫖复用手机号。'); + } + const state = await getState(); + const activationId = String(record.activationId || record.id || record.activation || '').trim(); + const countryId = Math.max(1, Math.floor(Number(record.countryId) || Number(state.heroSmsCountryId) || HERO_SMS_COUNTRY_ID)); + const stateCountryLabel = Math.floor(Number(state.heroSmsCountryId) || 0) === countryId + ? String(state.heroSmsCountryLabel || '').trim() + : ''; + const countryLabel = String( + record.countryLabel + || stateCountryLabel + || (countryId === HERO_SMS_COUNTRY_ID ? HERO_SMS_COUNTRY_LABEL : `Country #${countryId}`) + ).trim(); + const activation = { + ...(activationId ? { activationId } : {}), + phoneNumber, + provider: PHONE_SMS_PROVIDER_HERO, + serviceCode: HERO_SMS_SERVICE_CODE, + countryId, + ...(countryLabel ? { countryLabel } : {}), + successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)), + maxUses: Math.max(1, Math.floor(Number(record.maxUses) || 3)), + source: 'free-manual-reuse', + recordedAt: Date.now(), + manualOnly: !activationId, + }; + await setState({ freeReusablePhoneActivation: activation }); + broadcastDataUpdate({ freeReusablePhoneActivation: activation }); + await addLog( + activationId + ? `已手动记录白嫖复用手机号 ${phoneNumber}(#${activationId})。` + : `已手动记录白嫖复用手机号 ${phoneNumber}。未填写 HeroSMS 激活 ID,仅支持手动填号复用。`, + 'ok' + ); + return { ok: true, freeReusablePhoneActivation: activation }; +} + // ============================================================ // Tab Registry // ============================================================ @@ -10594,6 +10662,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args), deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args), clearAutoRunTimerAlarm, + clearFreeReusablePhoneActivation, clearLuckmailRuntimeState, clearStopRequest, closeLocalhostCallbackTabs, @@ -10682,6 +10751,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter setContributionMode, setEmailState, setEmailStateSilently, + setFreeReusablePhoneActivation, setSignupPhoneState, setSignupPhoneStateSilently, setIcloudAliasPreservedState, diff --git a/background/message-router.js b/background/message-router.js index a0f0a1c..3db7566 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -16,6 +16,7 @@ clearAccountRunHistory, deleteAccountRunHistoryRecords, clearAutoRunTimerAlarm, + clearFreeReusablePhoneActivation, clearLuckmailRuntimeState, clearStopRequest, closeLocalhostCallbackTabs, @@ -103,6 +104,7 @@ setContributionMode, setEmailState, setEmailStateSilently, + setFreeReusablePhoneActivation, setSignupPhoneState, setSignupPhoneStateSilently, setIcloudAliasPreservedState, @@ -679,6 +681,20 @@ return { ok: true }; } + case 'CLEAR_FREE_REUSABLE_PHONE': { + if (typeof clearFreeReusablePhoneActivation !== 'function') { + throw new Error('白嫖复用手机号清除能力未接入。'); + } + return await clearFreeReusablePhoneActivation(); + } + + case 'SET_FREE_REUSABLE_PHONE': { + if (typeof setFreeReusablePhoneActivation !== 'function') { + throw new Error('白嫖复用手机号记录能力未接入。'); + } + return await setFreeReusablePhoneActivation(message.payload || {}); + } + case 'SET_CONTRIBUTION_MODE': { const enabled = Boolean(message.payload?.enabled); const state = await ensureManualInteractionAllowed(enabled ? '进入贡献模式' : '退出贡献模式'); @@ -986,6 +1002,9 @@ if (Boolean(currentState?.contributionMode) && typeof setContributionMode === 'function') { await setContributionMode(true); } + if (Object.keys(stateUpdates).length > 0 && typeof broadcastDataUpdate === 'function') { + broadcastDataUpdate(stateUpdates); + } if (modeChanged) { const selectedPlusPaymentMethod = getPlusPaymentMethodLabel( stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal' diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index f497774..429e990 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -10,6 +10,7 @@ generateRandomName, getOAuthFlowStepTimeoutMs, getState, + requestStop = null, sendToContentScript, sendToContentScriptResilient, setState, @@ -40,6 +41,7 @@ const PHONE_VERIFICATION_CODE_STATE_KEY = 'currentPhoneVerificationCode'; const REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'reusablePhoneActivation'; const REUSABLE_PHONE_ACTIVATION_POOL_STATE_KEY = 'phoneReusableActivationPool'; + const FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'freeReusablePhoneActivation'; const PREFERRED_PHONE_ACTIVATION_STATE_KEY = 'phonePreferredActivation'; const PHONE_RUNTIME_COUNTDOWN_ENDS_AT_KEY = 'currentPhoneVerificationCountdownEndsAt'; const PHONE_RUNTIME_COUNTDOWN_WINDOW_INDEX_KEY = 'currentPhoneVerificationCountdownWindowIndex'; @@ -89,7 +91,13 @@ const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::'; const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::'; const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::'; + const PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX = 'PHONE_RESEND_BANNED_NUMBER::'; const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::'; + const PHONE_MANUAL_FREE_REUSE_ERROR_PREFIX = 'PHONE_MANUAL_FREE_REUSE::'; + const PHONE_AUTO_FREE_REUSE_PREPARE_ERROR_PREFIX = 'PHONE_AUTO_FREE_REUSE_PREPARE::'; + const FREE_PHONE_REUSE_PREPARE_TIMEOUT_MS = 20000; + const FREE_PHONE_REUSE_PREPARE_INTERVAL_MS = 2000; + const FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS = 10; const PHONE_SMS_FAILURE_SKIP_THRESHOLD = 2; const MAX_ACTIVATION_PRICE_HINTS = 256; const activationPriceHintsByKey = new Map(); @@ -292,6 +300,24 @@ return Math.max(0, Math.floor(Number(value) || 0)); } + function normalizePhoneDigits(value) { + return String(value || '').replace(/\D+/g, ''); + } + + function phoneNumbersMatch(left, right) { + const leftDigits = normalizePhoneDigits(left); + const rightDigits = normalizePhoneDigits(right); + return Boolean( + leftDigits + && rightDigits + && ( + leftDigits === rightDigits + || leftDigits.endsWith(rightDigits) + || rightDigits.endsWith(leftDigits) + ) + ); + } + function normalizeTimestampMs(value) { const numeric = Number(value); if (Number.isFinite(numeric) && numeric > 0) { @@ -435,6 +461,15 @@ return Boolean(value); } + function normalizeFreePhoneReuseEnabled(value) { + return Boolean(value); + } + + function normalizeFreePhoneReuseAutoEnabled(state = {}) { + return normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled) + && Boolean(state?.freePhoneReuseAutoEnabled); + } + function normalizeHeroSmsAcquirePriority(value = '') { const normalized = String(value || '').trim().toLowerCase(); if (normalized === HERO_SMS_ACQUIRE_PRIORITY_PRICE) { @@ -983,6 +1018,66 @@ maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)), ...(expiresAt > 0 ? { expiresAt } : {}), ...(statusAction ? { statusAction } : {}), + ...(record.source ? { source: String(record.source || '').trim() } : {}), + ...(record.phoneCodeReceived ? { phoneCodeReceived: true } : {}), + ...(record.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(record.phoneCodeReceivedAt) || 0) } : {}), + }; + } + + function normalizeManualFreeReusablePhoneActivation(record) { + if (!record || typeof record !== 'object' || Array.isArray(record)) { + return null; + } + const phoneNumber = String( + record.phoneNumber ?? record.number ?? record.phone ?? '' + ).trim(); + if (!phoneNumber) { + return null; + } + const activationId = String( + record.activationId ?? record.id ?? record.activation ?? '' + ).trim(); + const countryLabel = String(record.countryLabel || '').trim(); + const statusAction = String(record.statusAction || '').trim(); + return { + ...(activationId ? { activationId } : {}), + phoneNumber, + provider: PHONE_SMS_PROVIDER_HERO, + serviceCode: String(record.serviceCode || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE, + countryId: normalizeCountryId(record.countryId, HERO_SMS_COUNTRY_ID), + ...(countryLabel ? { countryLabel } : {}), + successfulUses: normalizeUseCount(record.successfulUses), + maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)), + ...(statusAction ? { statusAction } : {}), + source: 'free-manual-reuse', + recordedAt: Math.max(0, Number(record.recordedAt) || Date.now()), + manualOnly: !activationId, + }; + } + + function normalizeFreeReusablePhoneActivation(record) { + const normalized = normalizeActivation(record) || normalizeManualFreeReusablePhoneActivation(record); + if (!normalized) { + return null; + } + const recordedAt = Math.max(0, Number(record?.recordedAt) || 0); + return { + ...normalized, + provider: PHONE_SMS_PROVIDER_HERO, + source: 'free-manual-reuse', + ...(recordedAt ? { recordedAt } : {}), + }; + } + + function markActivationPhoneCodeReceived(activation) { + const normalizedActivation = normalizeActivation(activation); + if (!normalizedActivation) { + return null; + } + return { + ...normalizedActivation, + phoneCodeReceived: true, + phoneCodeReceivedAt: normalizedActivation.phoneCodeReceivedAt || Date.now(), }; } @@ -1067,6 +1162,18 @@ } } + async function persistFreeReusableActivation(activation) { + await setPhoneRuntimeState({ + [FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]: normalizeFreeReusablePhoneActivation(activation), + }); + } + + async function clearFreeReusableActivation() { + await setPhoneRuntimeState({ + [FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]: null, + }); + } + function normalizeActivationFallback(record) { if (!record || typeof record !== 'object' || Array.isArray(record)) { return null; @@ -1183,6 +1290,34 @@ return /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试/i.test(message); } + function isPhoneResendBannedNumberError(error) { + const message = String(error?.message || error || '').trim(); + if (!message) { + return false; + } + if (message.startsWith(PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX)) { + return true; + } + return /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|can(?:not|'t)\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number/i.test(message); + } + + function shouldTreatResendThrottledAsBanned(state = {}) { + return Boolean(state?.phoneResendThrottledAsBannedEnabled); + } + + function buildHighRiskResendThrottledError(message = '') { + return new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${message || 'OpenAI resend is throttled and configured as high-probability banned phone.'}`); + } + + function buildPhoneMaxUsageExceededError(message = '') { + return new Error(`PHONE_MAX_USAGE_EXCEEDED::${message || 'OpenAI reported phone_max_usage_exceeded for this phone number.'}`); + } + + function isPhoneMaxUsageExceededFlowError(error) { + const message = String(error?.message || error || '').trim(); + return message.startsWith('PHONE_MAX_USAGE_EXCEEDED::') || isPhoneNumberUsedError(message); + } + function isPhoneRoute405RecoveryError(error) { const message = String(error?.message || error || '').trim(); if (!message) { @@ -1505,6 +1640,19 @@ }; } + 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.'); + } + return { + provider: PHONE_SMS_PROVIDER_HERO, + apiKey, + baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL), + countryCandidates: resolveCountryCandidates(state), + }; + } + function parseActivationPayload(payload, fallback = null) { const normalizedFallback = normalizeActivation(fallback) || normalizeActivationFallback(fallback); const directActivation = normalizeActivation(payload); @@ -3220,16 +3368,28 @@ if (!normalizedActivation) { return ''; } + const normalizedStatus = Math.floor(Number(status) || 0); + if ( + (normalizedStatus === 6 || normalizedStatus === 8) + && shouldSkipTerminalStatusForFreeReuse(state, normalizedActivation) + ) { + const identifier = normalizedActivation.phoneNumber || normalizedActivation.activationId || 'current activation'; + await addLog( + `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的 setStatus(${normalizedStatus})。`, + 'info' + ); + return `free reuse setStatus(${normalizedStatus}) skipped`; + } const config = resolvePhoneConfig(state); if (config.provider === PHONE_SMS_PROVIDER_5SIM) { - const endpoint = status === 6 + const endpoint = normalizedStatus === 6 ? `/user/finish/${normalizedActivation.activationId}` : `/user/cancel/${normalizedActivation.activationId}`; const payload = await fetchFiveSimPayload(config, endpoint, actionLabel || '5sim set status'); return describeFiveSimPayload(payload); } if (config.provider === PHONE_SMS_PROVIDER_NEXSMS) { - if (status === 6) { + if (normalizedStatus === 6) { return 'NexSMS complete skipped'; } const payload = await fetchNexSmsPayload( @@ -3251,12 +3411,21 @@ const payload = await fetchHeroSmsPayload(config, { action: 'setStatus', id: normalizedActivation.activationId, - status, + status: normalizedStatus, }, actionLabel); return describeHeroSmsPayload(payload); } async function completePhoneActivation(state = {}, activation) { + if (shouldSkipTerminalStatusForFreeReuse(state, activation)) { + const normalizedActivation = normalizeActivation(activation); + const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation'; + await addLog( + `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码完成状态。`, + 'info' + ); + return; + } if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) { const provider = getFiveSimProviderForState(state); if (provider) { @@ -3269,6 +3438,15 @@ async function cancelPhoneActivation(state = {}, activation) { try { + const normalizedActivation = normalizeActivation(activation); + if (shouldSkipTerminalStatusForFreeReuse(state, activation)) { + const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation'; + await addLog( + `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码取消状态。`, + 'info' + ); + return; + } if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) { const provider = getFiveSimProviderForState(state); if (provider) { @@ -3282,8 +3460,62 @@ } } + async function retireFreeReusableActivation(reason = '') { + const suffix = reason ? ` ${reason}` : ''; + await addLog(`步骤 9:已清除白嫖复用手机号记录。${suffix}`, 'warn'); + await clearFreeReusableActivation(); + } + + async function discardPhoneActivationFromReuse(reason = '', activation = null, state = {}) { + const rejectedPhoneNumber = String(activation?.phoneNumber || '').trim(); + if (!rejectedPhoneNumber) { + return; + } + const updates = {}; + const currentActivation = normalizeActivation(state[PHONE_ACTIVATION_STATE_KEY]); + if (phoneNumbersMatch(currentActivation?.phoneNumber, rejectedPhoneNumber)) { + updates[PHONE_ACTIVATION_STATE_KEY] = null; + updates[PHONE_VERIFICATION_CODE_STATE_KEY] = ''; + } + const reusableActivation = normalizeActivation(state[REUSABLE_PHONE_ACTIVATION_STATE_KEY]); + if (phoneNumbersMatch(reusableActivation?.phoneNumber, rejectedPhoneNumber)) { + updates[REUSABLE_PHONE_ACTIVATION_STATE_KEY] = null; + } + const reusablePool = readReusableActivationPoolFromState(state); + const nextReusablePool = reusablePool.filter((entry) => ( + !phoneNumbersMatch(entry?.phoneNumber, rejectedPhoneNumber) + )); + if (nextReusablePool.length !== reusablePool.length) { + updates[REUSABLE_PHONE_ACTIVATION_POOL_STATE_KEY] = nextReusablePool; + } + const freeReusableActivation = normalizeFreeReusablePhoneActivation(state[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]); + if (phoneNumbersMatch(freeReusableActivation?.phoneNumber, rejectedPhoneNumber)) { + updates[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] = null; + } + if (Object.keys(updates).length) { + await setPhoneRuntimeState(updates); + await addLog( + `步骤 9:已从复用记录中移除手机号 ${rejectedPhoneNumber}。${reason || '目标站拒绝该号码。'}`, + 'warn' + ); + } + } + + function isFreeAutoReuseActivation(activation) { + return normalizeActivation(activation)?.source === 'free-auto-reuse'; + } + async function banPhoneActivation(state = {}, activation) { try { + if (shouldSkipTerminalStatusForFreeReuse(state, activation)) { + const normalizedActivation = normalizeActivation(activation); + const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation'; + await addLog( + `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码封禁状态。`, + 'info' + ); + return; + } if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) { const provider = getFiveSimProviderForState(state); if (provider) { @@ -3313,6 +3545,134 @@ } } + function isHeroSmsWaitingStatusText(text) { + return /^STATUS_(WAIT_CODE|WAIT_RETRY|WAIT_RESEND)(?::.+)?$/i.test(String(text || '').trim()); + } + + function isHeroSmsReadyForFreshSmsText(text) { + return /^STATUS_WAIT_CODE(?::.+)?$/i.test(String(text || '').trim()); + } + + function isHeroSmsCancelledStatusText(text) { + return /^STATUS_CANCEL$/i.test(String(text || '').trim()); + } + + async function prepareFreeReusablePhoneActivation(state = {}, activation) { + const normalizedActivation = normalizeFreeReusablePhoneActivation(activation); + if (!normalizedActivation) { + return { + ok: false, + reason: 'missing_free_reusable_activation', + message: 'Free reusable phone activation is missing.', + }; + } + 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.', + }; + } + + const statusAction = resolveActivationStatusAction(normalizedActivation); + const config = resolveHeroSmsPhoneConfig(state); + const start = Date.now(); + let lastStatus = ''; + let prepareRound = 0; + + while ( + Date.now() - start < FREE_PHONE_REUSE_PREPARE_TIMEOUT_MS + && prepareRound < FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS + ) { + throwIfStopped(); + prepareRound += 1; + + try { + await setPhoneActivationStatus( + { ...state, phoneSmsProvider: PHONE_SMS_PROVIDER_HERO }, + normalizedActivation, + 3, + 'HeroSMS setStatus(3) for automatic free reuse' + ); + } catch (error) { + return { + ok: false, + reason: 'set_status_failed', + message: error.message || 'HeroSMS setStatus(3) failed.', + lastStatus, + prepareRound, + }; + } + + await addLog( + `步骤 9:自动白嫖复用已刷新 ${normalizedActivation.phoneNumber},${Math.ceil(FREE_PHONE_REUSE_PREPARE_INTERVAL_MS / 1000)} 秒后检查等待状态(${prepareRound}/${FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS})。`, + 'info' + ); + await sleepWithStop(FREE_PHONE_REUSE_PREPARE_INTERVAL_MS); + + try { + const payload = await fetchHeroSmsPayload(config, { + action: statusAction, + id: normalizedActivation.activationId, + }, `HeroSMS ${statusAction} for automatic free reuse`); + const statusText = describeHeroSmsPayload(payload); + lastStatus = statusText; + await addLog( + `步骤 9:自动白嫖复用号码 ${normalizedActivation.phoneNumber} 状态:${statusText || 'empty response'}(${prepareRound}/${FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS})。`, + 'info' + ); + + const v2Waiting = statusAction === 'getStatusV2' + && payload + && typeof payload === 'object' + && !Array.isArray(payload) + && !payload.sms?.code + && !payload.call?.code; + if (isHeroSmsReadyForFreshSmsText(statusText) || isHeroSmsWaitingStatusText(statusText) || v2Waiting) { + return { + ok: true, + activation: { + ...normalizedActivation, + source: 'free-auto-reuse', + }, + }; + } + if (/^STATUS_OK:/i.test(statusText)) { + await addLog( + `步骤 9:自动白嫖复用仍看到旧验证码,将再次刷新等待短信状态。`, + 'warn' + ); + continue; + } + if (isHeroSmsCancelledStatusText(statusText)) { + return { + ok: false, + reason: 'activation_cancelled', + message: 'HeroSMS activation was cancelled before automatic free reuse.', + lastStatus, + prepareRound, + }; + } + } catch (error) { + return { + ok: false, + reason: 'get_status_failed', + message: error.message || 'HeroSMS getStatus failed.', + lastStatus, + prepareRound, + }; + } + } + + return { + ok: false, + reason: 'prepare_timeout', + message: `Timed out waiting for saved phone to enter SMS waiting state. Last status: ${lastStatus || 'unknown'}.`, + lastStatus, + prepareRound, + }; + } + async function pollPhoneActivationCode(state = {}, activation, options = {}) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { @@ -3777,6 +4137,71 @@ return result || {}; } + async function checkPhoneResendPageError(tabId, state = {}) { + if (!usePageProbeForPhoneResend(state)) { + return { + hasError: false, + reason: '', + message: '', + }; + } + const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; + try { + const result = await sendToContentScriptResilient('signup-page', { + type: 'CHECK_PHONE_RESEND_ERROR', + source: 'background', + payload: { visibleStep }, + }, { + timeoutMs: 3000, + responseTimeoutMs: 3000, + retryDelayMs: 500, + logStep: visibleStep, + logStepKey: 'phone-verification', + }); + + if (result?.error) { + throw new Error(result.error); + } + return result || {}; + } catch (error) { + if (isStopRequestedError(error)) { + throw error; + } + if (isPhoneResendBannedNumberError(error)) { + return { + hasError: true, + reason: 'resend_phone_banned', + message: error.message, + }; + } + if (isPhoneResendThrottledError(error)) { + return { + hasError: true, + reason: 'resend_throttled', + message: error.message, + }; + } + if (isPhoneMaxUsageExceededFlowError(error)) { + return { + hasError: true, + reason: 'phone_max_usage_exceeded', + message: error.message, + }; + } + await addLog(`步骤 9:检查手机重发错误时遇到暂时性问题,已忽略。${error.message}`, 'warn'); + return { + hasError: false, + reason: '', + message: '', + }; + } + } + + function usePageProbeForPhoneResend(state = {}) { + const provider = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER); + return provider === PHONE_SMS_PROVIDER_HERO || provider === PHONE_SMS_PROVIDER_NEXSMS || provider === PHONE_SMS_PROVIDER_5SIM; + } + async function persistCurrentActivation(activation) { const normalizedActivation = normalizeActivation(activation); const updates = { @@ -3843,6 +4268,67 @@ await persistReusableActivation(null); } + async function handoffFreeReusablePhone(tabId, state = {}) { + if (!normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled)) { + return null; + } + const freeReusableActivation = normalizeFreeReusablePhoneActivation( + state[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] + ); + if (!freeReusableActivation) { + return null; + } + + if (freeReusableActivation.successfulUses >= freeReusableActivation.maxUses) { + await retireFreeReusableActivation( + `保存的手机号 ${freeReusableActivation.phoneNumber} 已达到 ${freeReusableActivation.successfulUses}/${freeReusableActivation.maxUses} 次。` + ); + return null; + } + + if (normalizeFreePhoneReuseAutoEnabled(state)) { + await addLog( + `步骤 9:准备自动白嫖复用已保存手机号 ${freeReusableActivation.phoneNumber}(${freeReusableActivation.successfulUses + 1}/${freeReusableActivation.maxUses})。`, + 'info' + ); + const prepared = await prepareFreeReusablePhoneActivation(state, freeReusableActivation); + if (!prepared.ok) { + const reason = prepared.message || prepared.reason || 'unknown error'; + const stopMessage = `自动白嫖复用准备失败:${freeReusableActivation.phoneNumber} 未确认进入等待短信状态,本次不购买新 HeroSMS 号码。原因:${reason}`; + await addLog( + `步骤 9:自动白嫖复用准备失败,停止本次接码且不购买新 HeroSMS 号码。${reason}`, + 'error' + ); + if (prepared.reason === 'activation_cancelled') { + await retireFreeReusableActivation( + `自动白嫖复用号码 ${freeReusableActivation.phoneNumber} 已被 HeroSMS 取消。` + ); + } + if (typeof requestStop === 'function') { + await requestStop({ logMessage: stopMessage }); + } + throw new Error(`${PHONE_AUTO_FREE_REUSE_PREPARE_ERROR_PREFIX}${stopMessage}`); + } + await persistCurrentActivation(prepared.activation); + return prepared.activation; + } + + const fillResult = await submitPhoneNumber(tabId, freeReusableActivation.phoneNumber, freeReusableActivation); + await clearCurrentActivation(); + const message = `开始手动复用手机 ${freeReusableActivation.phoneNumber},请到 SMS 上刷新。`; + await addLog(`步骤 9:${message}`, 'warn'); + if (typeof requestStop === 'function') { + await requestStop({ logMessage: message }); + } + const handoffError = new Error(`${PHONE_MANUAL_FREE_REUSE_ERROR_PREFIX}${message}`); + handoffError.result = { + manualFreePhoneReuse: true, + phoneNumber: freeReusableActivation.phoneNumber, + fillResult, + }; + throw handoffError; + } + async function setPhoneRuntimeCountdown(activation, waitSeconds, windowIndex, windowTotal) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { @@ -4144,6 +4630,8 @@ ...normalizedActivation, successfulUses, }; + delete nextReusableActivation.phoneCodeReceived; + delete nextReusableActivation.phoneCodeReceivedAt; await upsertReusableActivationPool(nextReusableActivation, { state }); if (!normalizeHeroSmsReuseEnabled(state?.heroSmsReuseEnabled)) { await clearReusableActivation(); @@ -4158,6 +4646,119 @@ await persistReusableActivation(nextReusableActivation); } + function shouldPreserveActivationForFreeReuse(state, activation) { + if (!normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled)) { + return false; + } + const normalizedActivation = normalizeActivation(activation); + return Boolean( + normalizedActivation + && normalizedActivation.provider === PHONE_SMS_PROVIDER_HERO + && normalizedActivation.source === 'hero-sms-new' + && normalizedActivation.phoneCodeReceived + ); + } + + function shouldSkipTerminalStatusForFreeReuse(state, activation) { + const normalizedActivation = normalizeActivation(activation); + if (!normalizedActivation || normalizedActivation.provider !== PHONE_SMS_PROVIDER_HERO) { + return false; + } + if (isFreeAutoReuseActivation(normalizedActivation)) { + return true; + } + if (normalizedActivation.source === 'free-manual-reuse') { + return true; + } + const savedFreeActivation = normalizeFreeReusablePhoneActivation( + state?.[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] + ); + if ( + savedFreeActivation + && ( + isSameActivation(savedFreeActivation, normalizedActivation) + || phoneNumbersMatch(savedFreeActivation.phoneNumber, normalizedActivation.phoneNumber) + ) + ) { + return true; + } + return shouldPreserveActivationForFreeReuse(state, normalizedActivation); + } + + async function markFreeReusableActivationAfterCode(state, activation) { + const latestState = { + ...(state || {}), + ...(typeof getState === 'function' ? await getState() : {}), + }; + if (!normalizeFreePhoneReuseEnabled(latestState?.freePhoneReuseEnabled)) { + return; + } + if (normalizeFreeReusablePhoneActivation(latestState[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY])) { + return; + } + const normalizedActivation = normalizeActivation(activation); + if ( + !normalizedActivation + || normalizedActivation.provider !== PHONE_SMS_PROVIDER_HERO + || !normalizedActivation.phoneCodeReceived + || isFreeAutoReuseActivation(normalizedActivation) + ) { + return; + } + const countryConfig = resolveCountryConfigFromActivation(normalizedActivation, latestState); + await persistFreeReusableActivation({ + ...normalizedActivation, + source: 'free-manual-reuse', + countryId: countryConfig.id, + ...(countryConfig.label ? { countryLabel: countryConfig.label } : {}), + recordedAt: Date.now(), + }); + await addLog( + `步骤 9:收到有效短信后已保存白嫖复用手机号 ${normalizedActivation.phoneNumber}。`, + 'info' + ); + } + + async function markFreeReusableActivationAfterAutoSuccess(state, activation) { + const normalizedActivation = normalizeFreeReusablePhoneActivation(activation); + if (!normalizedActivation || !isFreeAutoReuseActivation(activation)) { + return; + } + + const latestState = { + ...(state || {}), + ...(typeof getState === 'function' ? await getState() : {}), + }; + const savedActivation = normalizeFreeReusablePhoneActivation( + latestState[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] + ); + if (!savedActivation || savedActivation.activationId !== normalizedActivation.activationId) { + return; + } + + const successfulUses = savedActivation.successfulUses + 1; + const maxUses = Math.max(1, Math.floor(Number(savedActivation.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)); + if (successfulUses >= maxUses) { + await clearFreeReusableActivation(); + await addLog( + `步骤 9:自动白嫖复用手机号 ${savedActivation.phoneNumber} 已达到 ${successfulUses}/${maxUses} 次,已清除本地记录。`, + 'info' + ); + return; + } + + await persistFreeReusableActivation({ + ...savedActivation, + source: 'free-manual-reuse', + successfulUses, + maxUses, + }); + await addLog( + `步骤 9:自动白嫖复用手机号 ${savedActivation.phoneNumber} 成功(${successfulUses}/${maxUses}),保留供后续注册使用。`, + 'info' + ); + } + async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { @@ -4191,6 +4792,24 @@ intervalMs: pollIntervalSeconds * 1000, maxRounds: pollMaxRounds, onStatus: async ({ elapsedMs, pollCount, statusText }) => { + 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.'}`); + } + if (pageError?.reason === 'phone_max_usage_exceeded') { + throw buildPhoneMaxUsageExceededError(pageError.message); + } + if (pageError?.reason === 'resend_throttled') { + if (shouldTreatResendThrottledAsBanned(state)) { + throw buildHighRiskResendThrottledError(pageError.message); + } + await addLog( + `步骤 9:检测到号码 ${normalizedActivation.phoneNumber} 重发限流,但未启用“按疑似封禁处理”,继续等待短信。${pageError.message || ''}`.trim(), + 'warn' + ); + } + } const shouldLog = ( pollCount === 1 || statusText !== lastLoggedStatus @@ -4214,6 +4833,50 @@ }; } catch (error) { if (!isPhoneCodeTimeoutError(error)) { + if (isPhoneResendBannedNumberError(error)) { + await addLog( + `步骤 9:OpenAI 无法向号码 ${normalizedActivation.phoneNumber} 发送短信,立即更换号码。${error.message}`, + 'warn' + ); + await clearPhoneRuntimeCountdown(); + return { + code: '', + replaceNumber: true, + reason: 'resend_phone_banned', + }; + } + if (isPhoneMaxUsageExceededFlowError(error)) { + await addLog( + `步骤 9:OpenAI 提示号码 ${normalizedActivation.phoneNumber} 达到使用上限,立即更换号码。${error.message}`, + 'warn' + ); + await clearPhoneRuntimeCountdown(); + return { + code: '', + replaceNumber: true, + reason: 'phone_max_usage_exceeded', + }; + } + if (isPhoneResendThrottledError(error)) { + if (shouldTreatResendThrottledAsBanned(state)) { + await addLog( + `步骤 9:号码 ${normalizedActivation.phoneNumber} 重发限流且配置为高风险封禁,立即更换号码。${error.message}`, + 'warn' + ); + await clearPhoneRuntimeCountdown(); + return { + code: '', + replaceNumber: true, + reason: 'resend_throttled_high_risk_banned', + }; + } + await addLog( + `步骤 9:号码 ${normalizedActivation.phoneNumber} 重发限流,但未启用高风险换号,继续原等待逻辑。${error.message}`, + 'warn' + ); + await sleepWithStop(pollIntervalSeconds * 1000); + continue; + } if (isPhoneActivationOrderMissingError(error, normalizedActivation.provider)) { await addLog( `Step 9: ${providerLabel} activation for ${normalizedActivation.phoneNumber} became invalid (${error.message || error}), replacing number immediately.`, @@ -4257,6 +4920,18 @@ if (isStopRequestedError(resendError)) { throw resendError; } + if (isPhoneResendBannedNumberError(resendError)) { + await addLog( + `步骤 9:OpenAI 无法向号码 ${normalizedActivation.phoneNumber} 发送短信,立即更换号码。${resendError.message}`, + 'warn' + ); + await clearPhoneRuntimeCountdown(); + return { + code: '', + replaceNumber: true, + reason: 'resend_phone_banned', + }; + } if (isPhoneResendThrottledError(resendError)) { await addLog( `步骤 9:号码 ${normalizedActivation.phoneNumber} 重发短信被限流,立即更换号码。${resendError.message}`, @@ -4266,7 +4941,9 @@ return { code: '', replaceNumber: true, - reason: 'resend_throttled', + reason: shouldTreatResendThrottledAsBanned(state) + ? 'resend_throttled_high_risk_banned' + : 'resend_throttled', }; } await addLog(`步骤 9:点击手机验证码页面重发按钮失败。${resendError.message}`, 'warn'); @@ -5101,13 +5778,18 @@ ); } if (!activation) { - activation = await acquirePhoneActivation(state, { - blockedCountryIds: getBlockedCountryIds(), - countryPriceFloorByCountryId: getCountryPriceFloorById(), - skipPreferredActivation: preferredActivationExhausted, - }); - shouldCancelActivation = true; - await persistCurrentActivation(activation); + activation = await handoffFreeReusablePhone(tabId, state); + if (activation) { + shouldCancelActivation = false; + } else { + activation = await acquirePhoneActivation(state, { + blockedCountryIds: getBlockedCountryIds(), + countryPriceFloorByCountryId: getCountryPriceFloorById(), + skipPreferredActivation: preferredActivationExhausted, + }); + shouldCancelActivation = true; + await persistCurrentActivation(activation); + } addPhoneReentryWithSameActivation = 0; } else if (preferReuseExistingActivationOnAddPhone) { addPhoneReentryWithSameActivation += 1; @@ -5120,6 +5802,11 @@ `步骤 9:当前号码 ${activation.phoneNumber} 反复返回添加手机号页,正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, 'warn' ); + if (isFreeAutoReuseActivation(activation)) { + await retireFreeReusableActivation( + `自动白嫖复用号码 ${activation.phoneNumber} 反复返回添加手机号页。` + ); + } if (shouldCancelActivation && activation) { await cancelPhoneActivation(state, activation); } @@ -5170,6 +5857,16 @@ `步骤 9:添加手机号页面提示 ${activation.phoneNumber} 已被使用(${addPhoneRejectText}),正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, 'warn' ); + await discardPhoneActivationFromReuse( + `目标站拒绝该号码(${addPhoneRejectText})。`, + activation, + await getState() + ); + if (isFreeAutoReuseActivation(activation)) { + await retireFreeReusableActivation( + `自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。` + ); + } if (shouldCancelActivation && activation) { await banPhoneActivation(state, activation); } @@ -5258,6 +5955,12 @@ await setPhoneRuntimeState({ [PHONE_VERIFICATION_CODE_STATE_KEY]: String(codeResult.code || '').trim(), }); + activation = markActivationPhoneCodeReceived(activation) || activation; + await persistCurrentActivation(activation); + await setPhoneRuntimeState({ + [PHONE_VERIFICATION_CODE_STATE_KEY]: String(codeResult.code || '').trim(), + }); + await markFreeReusableActivationAfterCode(state, activation); await addLog(`步骤 9:已收到手机验证码 ${codeResult.code}。`, 'info'); const submitResult = await submitPhoneVerificationCode(tabId, codeResult.code); @@ -5281,6 +5984,16 @@ if (isPhoneNumberUsedError(invalidErrorText)) { shouldReplaceNumber = true; replaceReason = 'phone_number_used'; + await discardPhoneActivationFromReuse( + `目标站拒绝该号码(${invalidErrorText})。`, + activation, + await getState() + ); + if (isFreeAutoReuseActivation(activation)) { + await retireFreeReusableActivation( + `自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。` + ); + } if (shouldCancelActivation && activation) { await banPhoneActivation(state, activation); shouldCancelActivation = false; @@ -5331,8 +6044,19 @@ continue; } - await completePhoneActivation(state, activation); - await markActivationReusableAfterSuccess(state, activation); + const latestSuccessState = await getState(); + if (shouldSkipTerminalStatusForFreeReuse(latestSuccessState, activation)) { + await addLog( + `步骤 9:已跳过 HeroSMS 完成状态,保留 ${activation.phoneNumber} 供白嫖复用。`, + 'info' + ); + } else { + await completePhoneActivation(latestSuccessState, activation); + } + await markFreeReusableActivationAfterAutoSuccess(state, activation); + if (!isFreeAutoReuseActivation(activation)) { + await markActivationReusableAfterSuccess(state, activation); + } clearCountrySmsFailure(activation.countryId, activation.provider); shouldCancelActivation = false; await clearCurrentActivation(); @@ -5372,6 +6096,18 @@ if (shouldCancelActivation && activation) { await cancelPhoneActivation(state, activation); } + if (isFreeAutoReuseActivation(activation)) { + await retireFreeReusableActivation( + `自动白嫖复用号码 ${activation.phoneNumber} 在失败后被更换。` + ); + } + if (isPhoneNumberUsedError(replaceReason)) { + await discardPhoneActivationFromReuse( + `目标站拒绝该号码(${replaceReason})。`, + activation, + await getState() + ); + } await clearCurrentActivation(); activation = null; shouldCancelActivation = false; @@ -5430,8 +6166,20 @@ }; } } catch (error) { + const errorMessage = String(error?.message || error || ''); + if ( + errorMessage.startsWith(PHONE_MANUAL_FREE_REUSE_ERROR_PREFIX) + || errorMessage.startsWith(PHONE_AUTO_FREE_REUSE_PREPARE_ERROR_PREFIX) + ) { + throw error; + } + if (isFreeAutoReuseActivation(activation)) { + await retireFreeReusableActivation( + `自动白嫖复用号码 ${activation.phoneNumber} 执行失败:${errorMessage || 'unknown error'}。` + ); + } if (shouldCancelActivation && activation) { - await cancelPhoneActivation(state, activation); + await cancelPhoneActivation(await getState(), activation); } await clearCurrentActivation(); throw sanitizePhoneRestartStep7Error(sanitizePhoneCodeTimeoutError(error)); diff --git a/content/phone-auth.js b/content/phone-auth.js index 84804aa..be79514 100644 --- a/content/phone-auth.js +++ b/content/phone-auth.js @@ -19,11 +19,14 @@ waitForElement, } = deps; const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::'; + const PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX = 'PHONE_RESEND_BANNED_NUMBER::'; + const PHONE_MAX_USAGE_EXCEEDED_PATTERN = /phone_max_usage_exceeded/i; const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::'; const PHONE_ROUTE_405_RECOVERY_COOLDOWN_MS = 6000; const PHONE_RESEND_ROUTE_405_MAX_RECOVERIES = 2; const PHONE_RESEND_ROUTE_405_MAX_RECOVERY_TOTAL_MS = 12000; const PHONE_RESEND_THROTTLED_PATTERN = /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试|重试次数过多/i; + const PHONE_RESEND_BANNED_NUMBER_PATTERN = /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|can(?:not|'t)\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number/i; const PHONE_ROUTE_405_PATTERN = /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+["']?\/phone-verification/i; const PHONE_ROUTE_405_MAX_RECOVERY_CLICKS = 3; const rootScope = typeof self !== 'undefined' ? self : globalThis; @@ -493,6 +496,63 @@ return ''; } + function getPhoneResendBannedNumberText() { + const inlineMatch = getPhoneVerificationInlineMessages() + .find((text) => PHONE_RESEND_BANNED_NUMBER_PATTERN.test(text)); + if (inlineMatch) { + return inlineMatch; + } + const pageSnapshot = String(getPageTextSnapshot?.() || '').replace(/\s+/g, ' ').trim(); + if (pageSnapshot && PHONE_RESEND_BANNED_NUMBER_PATTERN.test(pageSnapshot)) { + const concise = pageSnapshot.match( + /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|can(?:not|'t)\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number[^.。!?]*[.。!?]?|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number[^.。!?]*[.。!?]?/i + ); + return String(concise?.[0] || pageSnapshot).trim(); + } + return ''; + } + + function checkPhoneResendError() { + const maxUsageText = getAddPhoneErrorText(); + if (maxUsageText && PHONE_MAX_USAGE_EXCEEDED_PATTERN.test(maxUsageText)) { + return { + hasError: true, + reason: 'phone_max_usage_exceeded', + message: maxUsageText, + url: location.href, + }; + } + + const bannedNumberText = getPhoneResendBannedNumberText(); + if (bannedNumberText) { + return { + hasError: true, + reason: 'resend_phone_banned', + prefix: PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX, + message: bannedNumberText, + url: location.href, + }; + } + + const throttledText = getPhoneResendThrottleText(); + if (throttledText) { + return { + hasError: true, + reason: 'resend_throttled', + prefix: PHONE_RESEND_THROTTLED_ERROR_PREFIX, + message: throttledText, + url: location.href, + }; + } + + return { + hasError: false, + reason: '', + message: '', + url: location.href, + }; + } + function getAuthRetryButton(options = {}) { const { allowDisabled = false } = options; const direct = document.querySelector('button[data-dd-action-name="Try again"]'); @@ -779,6 +839,10 @@ await recoverRoute405WithinResend(); continue; } + const bannedNumberText = getPhoneResendBannedNumberText(); + if (bannedNumberText) { + throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${bannedNumberText}`); + } const throttledText = getPhoneResendThrottleText(); if (throttledText) { throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${throttledText}`); @@ -792,6 +856,10 @@ await recoverRoute405WithinResend(); continue; } + const afterClickBannedNumberText = getPhoneResendBannedNumberText(); + if (afterClickBannedNumberText) { + throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${afterClickBannedNumberText}`); + } const afterClickThrottleText = getPhoneResendThrottleText(); if (afterClickThrottleText) { throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${afterClickThrottleText}`); @@ -804,6 +872,11 @@ await sleep(250); } + const timeoutBannedNumberText = getPhoneResendBannedNumberText(); + if (timeoutBannedNumberText) { + throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${timeoutBannedNumberText}`); + } + const timeoutThrottleText = getPhoneResendThrottleText(); if (timeoutThrottleText) { throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${timeoutThrottleText}`); @@ -839,6 +912,7 @@ return { getPhoneVerificationDisplayedPhone, + checkPhoneResendError, isPhoneVerificationPageReady, resendPhoneVerificationCode, returnToAddPhone, diff --git a/content/signup-page.js b/content/signup-page.js index 6ab247a..f3a3615 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -25,6 +25,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1' || message.type === 'SUBMIT_PHONE_NUMBER' || message.type === 'SUBMIT_PHONE_VERIFICATION_CODE' || message.type === 'RESEND_PHONE_VERIFICATION_CODE' + || message.type === 'CHECK_PHONE_RESEND_ERROR' || message.type === 'RETURN_TO_ADD_PHONE' || message.type === 'ENSURE_SIGNUP_ENTRY_READY' || message.type === 'ENSURE_SIGNUP_PHONE_ENTRY_READY' @@ -97,6 +98,8 @@ async function handleCommand(message) { return await submitPhoneVerificationCodeWithProfileFallback(message.payload); case 'RESEND_PHONE_VERIFICATION_CODE': return await phoneAuthHelpers.resendPhoneVerificationCode(); + case 'CHECK_PHONE_RESEND_ERROR': + return phoneAuthHelpers.checkPhoneResendError(); case 'RETURN_TO_ADD_PHONE': return await phoneAuthHelpers.returnToAddPhone(); case 'ENSURE_SIGNUP_ENTRY_READY': @@ -2915,6 +2918,7 @@ const phoneAuthHelpers = self.MultiPagePhoneAuth?.createPhoneAuthHelpers?.({ resendPhoneVerificationCode: async () => { throw new Error('Phone auth helpers are unavailable.'); }, + checkPhoneResendError: () => ({ hasError: false, reason: '', message: '', url: location.href }), returnToAddPhone: async () => { throw new Error('Phone auth helpers are unavailable.'); }, diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 6004558..2c4fc3f 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -2367,6 +2367,20 @@ header { min-height: 33px; } +.hero-sms-free-phone-cell { + flex-wrap: wrap; +} + +.hero-sms-manual-phone-input { + flex: 1 1 150px; + min-width: 120px; +} + +.hero-sms-free-phone-country { + flex: 0 0 auto; + white-space: nowrap; +} + .signup-phone-runtime-inline, .signup-phone-runtime-input { width: 100%; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 6c96092..9fb7d82 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -1467,6 +1467,28 @@ + + @@ -1487,6 +1509,14 @@ 验证码 未获取 +