From d28a9fcb3531b606aa53196ca50bc20028d5b7d3 Mon Sep 17 00:00:00 2001 From: initiatione <269353933@qq.com> Date: Thu, 7 May 2026 18:53:03 +0800 Subject: [PATCH] Fix manual free phone reuse handoff --- background.js | 116 ++++++++- background/phone-verification-flow.js | 53 +++- .../background-message-router-module.test.js | 41 ++++ tests/phone-verification-flow.test.js | 232 ++++++++++++++++++ 4 files changed, 433 insertions(+), 9 deletions(-) diff --git a/background.js b/background.js index 4e67c5c..12f253a 100644 --- a/background.js +++ b/background.js @@ -378,8 +378,18 @@ const FIVE_SIM_COUNTRY_ID = 'vietnam'; const FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)'; const FIVE_SIM_SUPPORTED_COUNTRY_IDS = ['indonesia', 'thailand', 'vietnam']; const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(FIVE_SIM_SUPPORTED_COUNTRY_IDS); -const HERO_SMS_SUPPORTED_COUNTRY_IDS = [6, 52, 10]; +const HERO_SMS_SUPPORTED_COUNTRY_IDS = [6, 52, 187, 16, 151, 43, 73, 10]; const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(HERO_SMS_SUPPORTED_COUNTRY_IDS.map(String)); +const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([ + { prefix: '84', id: 10, label: 'Vietnam' }, + { prefix: '66', id: 52, label: 'Thailand' }, + { prefix: '62', id: 6, label: 'Indonesia' }, + { prefix: '44', id: 16, label: 'United Kingdom' }, + { prefix: '81', id: 151, label: 'Japan' }, + { prefix: '49', id: 43, label: 'Germany' }, + { prefix: '33', id: 73, label: 'France' }, + { prefix: '1', id: 187, label: 'USA' }, +]); const FIVE_SIM_OPERATOR = DEFAULT_FIVE_SIM_OPERATOR; const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; @@ -6700,19 +6710,117 @@ async function clearFreeReusablePhoneActivation() { return { ok: true, freeReusablePhoneActivation: null }; } +function inferHeroSmsCountryFromPhoneNumber(phoneNumber = '') { + const digits = String(phoneNumber || '').replace(/\D+/g, ''); + if (!digits) { + return null; + } + const match = HERO_SMS_COUNTRY_BY_PHONE_PREFIX.find((entry) => digits.startsWith(entry.prefix)); + if (!match) { + return null; + } + return { + id: Math.max(1, Math.floor(Number(match.id) || 0)), + label: String(match.label || '').trim() || `Country #${match.id}`, + }; +} + +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); +} + +function normalizeLocalHeroSmsActivation(record) { + if (!record || typeof record !== 'object' || Array.isArray(record)) { + return null; + } + const activationId = String(record.activationId ?? record.id ?? record.activation ?? '').trim(); + const phoneNumber = String(record.phoneNumber ?? record.number ?? record.phone ?? '').trim(); + if (!activationId || !phoneNumber) { + return null; + } + const rawProvider = String(record.provider ?? record.smsProvider ?? '').trim(); + const provider = rawProvider ? normalizePhoneSmsProvider(rawProvider) : PHONE_SMS_PROVIDER_HERO; + if (provider !== PHONE_SMS_PROVIDER_HERO) { + return null; + } + const countryId = Math.max( + 0, + Math.floor(Number(record.countryId ?? record.country ?? record.countryCode) || 0) + ); + const countryLabel = String(record.countryLabel || record.label || '').trim(); + const serviceCode = String(record.serviceCode || record.service || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE; + return { + ...record, + provider: PHONE_SMS_PROVIDER_HERO, + activationId, + phoneNumber, + serviceCode, + ...(countryId > 0 ? { countryId } : {}), + ...(countryLabel ? { countryLabel } : {}), + }; +} + +function findLocalHeroSmsActivationForPhone(state = {}, phoneNumber = '') { + const candidates = [ + state.currentPhoneActivation, + state.reusablePhoneActivation, + state.pendingPhoneActivationConfirmation, + state.signupPhoneActivation, + state.signupPhoneCompletedActivation, + state.phonePreferredActivation, + state.freeReusablePhoneActivation, + ]; + if (Array.isArray(state.phoneReusableActivationPool)) { + candidates.push(...state.phoneReusableActivationPool); + } + for (const candidate of candidates) { + const normalized = normalizeLocalHeroSmsActivation(candidate); + if (normalized && phoneNumbersMatch(normalized.phoneNumber, phoneNumber)) { + return normalized; + } + } + return 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 localActivation = findLocalHeroSmsActivationForPhone(state, phoneNumber); + const activationId = String( + record.activationId + || record.id + || record.activation + || localActivation?.activationId + || '' + ).trim(); + const inferredCountry = inferHeroSmsCountryFromPhoneNumber(phoneNumber); + const hasExplicitCountry = Number.isFinite(Number(record.countryId)) && Number(record.countryId) > 0; + const countryId = Math.max( + 1, + Math.floor( + Number(record.countryId) + || Number(localActivation?.countryId) + || Number(inferredCountry?.id) + || 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 + || (Number(localActivation?.countryId) === countryId ? localActivation?.countryLabel : '') + || (!hasExplicitCountry && inferredCountry?.id === countryId ? inferredCountry.label : '') || stateCountryLabel || (countryId === HERO_SMS_COUNTRY_ID ? HERO_SMS_COUNTRY_LABEL : `Country #${countryId}`) ).trim(); @@ -6720,7 +6828,7 @@ async function setFreeReusablePhoneActivation(record = {}) { ...(activationId ? { activationId } : {}), phoneNumber, provider: PHONE_SMS_PROVIDER_HERO, - serviceCode: HERO_SMS_SERVICE_CODE, + serviceCode: String(record.serviceCode || localActivation?.serviceCode || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE, countryId, ...(countryLabel ? { countryLabel } : {}), successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)), diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index ee951e0..e597c1d 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -100,6 +100,16 @@ const FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS = 10; const PHONE_SMS_FAILURE_SKIP_THRESHOLD = 2; const MAX_ACTIVATION_PRICE_HINTS = 256; + const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([ + { prefix: '84', id: 10, label: 'Vietnam' }, + { prefix: '66', id: 52, label: 'Thailand' }, + { prefix: '62', id: 6, label: 'Indonesia' }, + { prefix: '44', id: 16, label: 'United Kingdom' }, + { prefix: '81', id: 151, label: 'Japan' }, + { prefix: '49', id: 43, label: 'Germany' }, + { prefix: '33', id: 73, label: 'France' }, + { prefix: '1', id: 187, label: 'USA' }, + ]); const activationPriceHintsByKey = new Map(); let activePhoneVerificationLogStep = null; let activePhoneVerificationLogStepKey = null; @@ -422,6 +432,21 @@ return String(value || '').trim() || fallback; } + function inferHeroSmsCountryFromPhoneNumber(phoneNumber = '') { + const digits = String(phoneNumber || '').replace(/\D+/g, ''); + if (!digits) { + return null; + } + const match = HERO_SMS_COUNTRY_BY_PHONE_PREFIX.find((entry) => digits.startsWith(entry.prefix)); + if (!match) { + return null; + } + return { + id: normalizeCountryId(match.id, 0), + label: normalizeCountryLabel(match.label, `Country #${match.id}`), + }; + } + function normalizePhoneCodeWaitSeconds(value) { const parsed = Math.floor(Number(value)); if (!Number.isFinite(parsed) || parsed <= 0) { @@ -1037,14 +1062,19 @@ const activationId = String( record.activationId ?? record.id ?? record.activation ?? '' ).trim(); - const countryLabel = String(record.countryLabel || '').trim(); + const inferredCountry = inferHeroSmsCountryFromPhoneNumber(phoneNumber); + const countryId = normalizeCountryId(record.countryId, inferredCountry?.id || HERO_SMS_COUNTRY_ID); + const countryLabel = String( + record.countryLabel + || (inferredCountry && inferredCountry.id === countryId ? inferredCountry.label : '') + ).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), + countryId, ...(countryLabel ? { countryLabel } : {}), successfulUses: normalizeUseCount(record.successfulUses), maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)), @@ -3935,7 +3965,16 @@ }; } } else { - const countryId = normalizeCountryId(activation.countryId, 0); + const inferredCountry = inferHeroSmsCountryFromPhoneNumber(activation.phoneNumber); + const rawCountryId = normalizeCountryId(activation.countryId, 0); + const hasExplicitCountry = Object.prototype.hasOwnProperty.call(activation, 'countryId') + && Number.isFinite(rawCountryId) + && rawCountryId > 0 + && !(activation.manualOnly && rawCountryId === HERO_SMS_COUNTRY_ID && inferredCountry?.id && inferredCountry.id !== rawCountryId); + const countryId = hasExplicitCountry ? rawCountryId : normalizeCountryId(inferredCountry?.id, rawCountryId); + const countryLabel = hasExplicitCountry + ? activation.countryLabel + : (inferredCountry?.label || activation.countryLabel); if (countryId > 0) { const matched = candidates.find((entry) => entry.id === countryId); if (matched) { @@ -3943,7 +3982,7 @@ } return { id: countryId, - label: normalizeCountryLabel(activation.countryLabel, `Country #${countryId}`), + label: normalizeCountryLabel(countryLabel, `Country #${countryId}`), }; } } @@ -4286,7 +4325,11 @@ return null; } - if (normalizeFreePhoneReuseAutoEnabled(state)) { + const canPrepareAutomaticFreeReuse = normalizeFreePhoneReuseAutoEnabled(state) + && !freeReusableActivation.manualOnly + && Boolean(String(freeReusableActivation.activationId || '').trim()); + + if (canPrepareAutomaticFreeReuse) { await addLog( `步骤 9:准备自动白嫖复用已保存手机号 ${freeReusableActivation.phoneNumber}(${freeReusableActivation.successfulUses + 1}/${freeReusableActivation.maxUses})。`, 'info' diff --git a/tests/background-message-router-module.test.js b/tests/background-message-router-module.test.js index d436b3a..b931f8a 100644 --- a/tests/background-message-router-module.test.js +++ b/tests/background-message-router-module.test.js @@ -28,6 +28,47 @@ test('background free reusable phone setter does not depend on module-scoped pho assert.match(setterBlock, /maxUses:\s*Math\.max\(1,\s*Math\.floor\(Number\(record\.maxUses\)\s*\|\|\s*3\)\)/); }); +test('background free reusable phone setter can recover local HeroSMS activation id by phone number', () => { + const source = fs.readFileSync('background.js', 'utf8'); + const setterStart = source.indexOf('async function setFreeReusablePhoneActivation'); + const setterEnd = source.indexOf('// ============================================================\n// Tab Registry', setterStart); + const setterBlock = source.slice(setterStart, setterEnd); + + assert.match(source, /function findLocalHeroSmsActivationForPhone\(/); + assert.match(source, /state\.currentPhoneActivation/); + assert.match(source, /state\.reusablePhoneActivation/); + assert.match(source, /state\.signupPhoneActivation/); + assert.match(source, /state\.signupPhoneCompletedActivation/); + assert.match(source, /state\.phonePreferredActivation/); + assert.match(source, /state\.phoneReusableActivationPool/); + assert.match(setterBlock, /findLocalHeroSmsActivationForPhone\(state,\s*phoneNumber\)/); + assert.match(setterBlock, /activationId = String\(\s*record\.activationId[\s\S]*localActivation\?\.activationId/); + assert.match(setterBlock, /manualOnly:\s*!activationId/); +}); + +test('background HeroSMS phone prefix inference covers built-in major countries', () => { + const source = fs.readFileSync('background.js', 'utf8'); + const supportedStart = source.indexOf('const HERO_SMS_SUPPORTED_COUNTRY_IDS = ['); + const prefixStart = source.indexOf('const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze(['); + const prefixEnd = source.indexOf(']);', prefixStart); + const supportedBlock = source.slice(supportedStart, source.indexOf('];', supportedStart)); + const prefixBlock = source.slice(prefixStart, prefixEnd); + + assert.match(supportedBlock, /\[6,\s*52,\s*187,\s*16,\s*151,\s*43,\s*73,\s*10/); + [ + ['84', 10, 'Vietnam'], + ['66', 52, 'Thailand'], + ['62', 6, 'Indonesia'], + ['44', 16, 'United Kingdom'], + ['81', 151, 'Japan'], + ['49', 43, 'Germany'], + ['33', 73, 'France'], + ['1', 187, 'USA'], + ].forEach(([prefix, id, label]) => { + assert.match(prefixBlock, new RegExp(`prefix:\\s*'${prefix}'[\\s\\S]*id:\\s*${id}[\\s\\S]*label:\\s*'${label}'`)); + }); +}); + test('message router module exposes a factory', () => { const source = fs.readFileSync('background/message-router.js', 'utf8'); const globalScope = {}; diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index 2faa972..89d853b 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -3569,6 +3569,238 @@ test('phone verification helper gives automatic free reuse priority over paid re assert.equal(currentState.reusablePhoneActivation.activationId, 'paid-reuse'); }); +test('phone verification helper hands off manual-only free reuse even when automatic free reuse is enabled', async () => { + const requests = []; + const messages = []; + const stops = []; + let currentState = { + heroSmsApiKey: 'demo-key', + heroSmsReuseEnabled: true, + freePhoneReuseEnabled: true, + freePhoneReuseAutoEnabled: true, + verificationResendCount: 0, + currentPhoneActivation: null, + reusablePhoneActivation: { + activationId: 'paid-reuse', + phoneNumber: '66950003333', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + countryLabel: 'Thailand', + successfulUses: 0, + maxUses: 3, + }, + freeReusablePhoneActivation: { + phoneNumber: '84943328460', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + countryLabel: 'Thailand', + successfulUses: 1, + maxUses: 3, + source: 'free-manual-reuse', + manualOnly: true, + }, + }; + + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + requests.push(parsedUrl); + throw new Error(`HeroSMS should not be called for manual-only free reuse: ${parsedUrl.searchParams.get('action')}`); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...currentState }), + requestStop: async (payload) => { + stops.push(payload); + }, + sendToContentScriptResilient: async (_source, message) => { + messages.push(message); + if (message.type === 'SUBMIT_PHONE_NUMBER') { + assert.equal(message.payload.phoneNumber, '84943328460'); + return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' }; + } + 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, /^PHONE_MANUAL_FREE_REUSE::/); + assert.equal(error.result?.manualFreePhoneReuse, true); + assert.equal(error.result?.phoneNumber, '84943328460'); + assert.deepStrictEqual(error.result?.fillResult, { + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + }); + return true; + } + ); + + assert.deepStrictEqual(messages.map((message) => message.type), ['SUBMIT_PHONE_NUMBER']); + assert.equal(stops.length, 1); + assert.match(stops[0].logMessage, /开始手动复用手机 84943328460/); + assert.deepStrictEqual(requests, []); + assert.equal(currentState.currentPhoneActivation, null); + assert.equal(currentState.reusablePhoneActivation.activationId, 'paid-reuse'); + assert.equal(currentState.freeReusablePhoneActivation.phoneNumber, '84943328460'); +}); + +test('phone verification helper infers manual free reuse country from phone prefix', async () => { + const messages = []; + const stops = []; + let currentState = { + heroSmsApiKey: 'demo-key', + heroSmsCountryId: 52, + heroSmsCountryLabel: 'Thailand', + freePhoneReuseEnabled: true, + freePhoneReuseAutoEnabled: true, + verificationResendCount: 0, + currentPhoneActivation: null, + freeReusablePhoneActivation: { + phoneNumber: '84943328460', + provider: 'hero-sms', + serviceCode: 'dr', + successfulUses: 1, + maxUses: 3, + source: 'free-manual-reuse', + manualOnly: true, + }, + }; + + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + throw new Error(`HeroSMS should not be called for manual-only free reuse: ${parsedUrl.searchParams.get('action')}`); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...currentState }), + requestStop: async (payload) => { + stops.push(payload); + }, + sendToContentScriptResilient: async (_source, message) => { + messages.push(message); + if (message.type === 'SUBMIT_PHONE_NUMBER') { + return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' }; + } + 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, /^PHONE_MANUAL_FREE_REUSE::/); + return true; + } + ); + + assert.equal(stops.length, 1); + assert.deepStrictEqual(messages.map((message) => message.type), ['SUBMIT_PHONE_NUMBER']); + assert.equal(messages[0].payload.phoneNumber, '84943328460'); + assert.equal(messages[0].payload.countryId, 10); + assert.equal(messages[0].payload.countryLabel, 'Vietnam'); +}); + +test('phone verification helper infers major HeroSMS countries from phone prefixes', async () => { + const cases = [ + { phoneNumber: '12025550123', countryId: 187, countryLabel: 'USA' }, + { phoneNumber: '447911123456', countryId: 16, countryLabel: 'United Kingdom' }, + { phoneNumber: '819012345678', countryId: 151, countryLabel: 'Japan' }, + { phoneNumber: '4915112345678', countryId: 43, countryLabel: 'Germany' }, + { phoneNumber: '33612345678', countryId: 73, countryLabel: 'France' }, + { phoneNumber: '84943328460', countryId: 10, countryLabel: 'Vietnam' }, + { phoneNumber: '66950003333', countryId: 52, countryLabel: 'Thailand' }, + { phoneNumber: '628111111111', countryId: 6, countryLabel: 'Indonesia' }, + ]; + + for (const testCase of cases) { + const messages = []; + let currentState = { + heroSmsApiKey: 'demo-key', + heroSmsCountryId: 52, + heroSmsCountryLabel: 'Thailand', + freePhoneReuseEnabled: true, + freePhoneReuseAutoEnabled: true, + verificationResendCount: 0, + currentPhoneActivation: null, + freeReusablePhoneActivation: { + phoneNumber: testCase.phoneNumber, + provider: 'hero-sms', + serviceCode: 'dr', + successfulUses: 1, + maxUses: 3, + source: 'free-manual-reuse', + manualOnly: true, + }, + }; + + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + throw new Error(`HeroSMS should not be called for manual-only free reuse: ${parsedUrl.searchParams.get('action')}`); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...currentState }), + requestStop: async () => {}, + sendToContentScriptResilient: async (_source, message) => { + messages.push(message); + if (message.type === 'SUBMIT_PHONE_NUMBER') { + return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' }; + } + 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, /^PHONE_MANUAL_FREE_REUSE::/); + return true; + } + ); + + assert.equal(messages.length, 1, testCase.phoneNumber); + assert.equal(messages[0].payload.phoneNumber, testCase.phoneNumber); + assert.equal(messages[0].payload.countryId, testCase.countryId, testCase.phoneNumber); + assert.equal(messages[0].payload.countryLabel, testCase.countryLabel, testCase.phoneNumber); + } +}); + test('phone verification helper accepts HeroSMS WAIT_RETRY as free-reuse ready without using its suffix as code', async () => { const requests = []; const events = [];