From 3499cd18a4805b866f0434a6e945216335f88268 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Wed, 20 May 2026 05:15:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A8=B3=E5=AE=9A=205sim=20=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E8=AE=A2=E5=8D=95=E5=A4=8D=E7=94=A8=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background/phone-verification-flow.js | 81 +++++++++++++++++++++++++-- phone-sms/providers/five-sim.js | 47 +++++++++++++++- tests/five-sim-provider.test.js | 2 + tests/phone-verification-flow.test.js | 53 +++++++++++++++--- 4 files changed, 169 insertions(+), 14 deletions(-) diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index a43c2dd..11783d4 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -317,6 +317,36 @@ return Math.max(0, Math.floor(Number(value) || 0)); } + function normalizeStringList(value = []) { + const source = Array.isArray(value) ? value : []; + const seen = new Set(); + const normalized = []; + source.forEach((entry) => { + const text = String(entry || '').trim(); + if (!text || seen.has(text)) { + return; + } + seen.add(text); + normalized.push(text); + }); + return normalized; + } + + function buildPhoneSmsCodeKey(message = {}) { + return [ + message.id ?? message.ID ?? '', + message.created_at ?? message.date ?? '', + message.code ?? '', + message.text ?? '', + message.message ?? '', + ].map((part) => String(part || '').trim()).filter(Boolean).join('::'); + } + + function collectPhoneSmsCodeKeys(payload) { + const smsList = Array.isArray(payload?.sms) ? payload.sms : []; + return normalizeStringList(smsList.map((message) => buildPhoneSmsCodeKey(message)).filter(Boolean)); + } + function normalizePhoneDigits(value) { return String(value || '').replace(/\D+/g, ''); } @@ -1322,6 +1352,7 @@ ? normalizeNexSmsCountryId(rawCountryId, 0) : normalizeCountryId(rawCountryId, fallbackCountryId) ); + const ignoredPhoneCodeKeys = normalizeStringList(record.ignoredPhoneCodeKeys); return { activationId, phoneNumber, @@ -1337,6 +1368,7 @@ ...(record.source ? { source: String(record.source || '').trim() } : {}), ...(record.phoneCodeReceived ? { phoneCodeReceived: true } : {}), ...(record.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(record.phoneCodeReceivedAt) || 0) } : {}), + ...(ignoredPhoneCodeKeys.length ? { ignoredPhoneCodeKeys } : {}), }; } @@ -1382,12 +1414,16 @@ return null; } const recordedAt = Math.max(0, Number(record?.recordedAt) || 0); - return { + const reusableActivation = { ...normalized, provider: normalized.provider, source: 'free-manual-reuse', ...(recordedAt ? { recordedAt } : {}), }; + delete reusableActivation.phoneCodeReceived; + delete reusableActivation.phoneCodeReceivedAt; + delete reusableActivation.ignoredPhoneCodeKeys; + return reusableActivation; } function markActivationPhoneCodeReceived(activation) { @@ -3901,9 +3937,15 @@ const config = resolvePhoneConfig(state); if (config.provider === PHONE_SMS_PROVIDER_5SIM) { + const payload = await fetchFiveSimPayload( + config, + `/user/check/${encodeURIComponent(normalizedActivation.activationId)}`, + '5sim reuse activation baseline' + ); return { ...normalizedActivation, source: '5sim-retained-reuse', + ignoredPhoneCodeKeys: collectPhoneSmsCodeKeys(payload), }; } if (config.provider === PHONE_SMS_PROVIDER_NEXSMS) { @@ -4157,6 +4199,26 @@ }; } if (normalizedActivation.provider === PHONE_SMS_PROVIDER_5SIM) { + const provider = getFiveSimProviderForState(state); + if (provider) { + let retainedActivation = null; + try { + retainedActivation = await provider.reuseActivation(state, normalizedActivation); + } catch (error) { + return { + ok: false, + reason: 'five_sim_reuse_check_failed', + message: error.message || '5sim 复用手机号基线检查失败。', + }; + } + return { + ok: true, + activation: { + ...retainedActivation, + source: 'free-auto-reuse', + }, + }; + } return { ok: true, activation: { @@ -4337,10 +4399,20 @@ pollCount += 1; const smsList = Array.isArray(payload?.sms) ? payload.sms : []; + const ignoredPhoneCodeKeys = new Set(normalizeStringList(normalizedActivation.ignoredPhoneCodeKeys)); const directCode = extractVerificationCode(payload?.code || payload?.sms_code); - const smsCode = directCode || smsList - .map((smsItem) => extractVerificationCode(smsItem?.code || smsItem?.text || smsItem?.message || '')) - .find(Boolean); + let smsCode = ''; + for (let index = smsList.length - 1; index >= 0; index -= 1) { + const smsItem = smsList[index] || {}; + if (ignoredPhoneCodeKeys.has(buildPhoneSmsCodeKey(smsItem))) { + continue; + } + smsCode = extractVerificationCode(smsItem?.code || smsItem?.text || smsItem?.message || ''); + if (smsCode) { + break; + } + } + smsCode = directCode || smsCode; if (smsCode) { return smsCode; } @@ -5295,6 +5367,7 @@ }; delete nextReusableActivation.phoneCodeReceived; delete nextReusableActivation.phoneCodeReceivedAt; + delete nextReusableActivation.ignoredPhoneCodeKeys; await upsertReusableActivationPool(nextReusableActivation, { state }); if (!normalizePhoneSmsReuseEnabled(state)) { await clearReusableActivation(); diff --git a/phone-sms/providers/five-sim.js b/phone-sms/providers/five-sim.js index a942ec6..15312f6 100644 --- a/phone-sms/providers/five-sim.js +++ b/phone-sms/providers/five-sim.js @@ -229,6 +229,21 @@ return price; } + function normalizeStringList(value = []) { + const source = Array.isArray(value) ? value : []; + const seen = new Set(); + const normalized = []; + source.forEach((entry) => { + const text = String(entry || '').trim(); + if (!text || seen.has(text)) { + return; + } + seen.add(text); + normalized.push(text); + }); + return normalized; + } + function buildSortedUniquePriceCandidates(values = []) { return Array.from( new Set( @@ -549,6 +564,9 @@ operator: normalizeFiveSimOperator(record.operator || fallback.operator), ...(record.price !== undefined ? { price: Number(record.price) } : {}), ...(record.status ? { status: String(record.status) } : {}), + ...(normalizeStringList(record.ignoredPhoneCodeKeys || fallback.ignoredPhoneCodeKeys).length + ? { ignoredPhoneCodeKeys: normalizeStringList(record.ignoredPhoneCodeKeys || fallback.ignoredPhoneCodeKeys) } + : {}), }; } @@ -748,9 +766,14 @@ } // 5sim 的 /user/reuse 会按手机号重新创建付费订单,并不是保留原订单继续收短信。 // 真正的复用应继续轮询原 activationId,避免产生新的订单。 + const config = resolveConfig(state, deps); + const payload = await fetchJson(config, `/v1/user/check/${encodeURIComponent(normalizedActivation.activationId)}`, { + actionLabel: '5sim 复用手机号基线检查', + }); return { ...normalizedActivation, source: '5sim-retained-reuse', + ignoredPhoneCodeKeys: collectSmsCodeKeys(payload), }; } @@ -793,10 +816,30 @@ return digitMatch?.[1] || trimmed; } - function extractCodeFromOrder(payload) { + function buildSmsCodeKey(message = {}) { + const text = [ + message.id ?? message.ID ?? '', + message.created_at ?? message.date ?? '', + message.code ?? '', + message.text ?? '', + message.message ?? '', + ].map((part) => String(part || '').trim()).filter(Boolean).join('::'); + return text; + } + + function collectSmsCodeKeys(payload) { const smsList = Array.isArray(payload?.sms) ? payload.sms : []; + return normalizeStringList(smsList.map((message) => buildSmsCodeKey(message)).filter(Boolean)); + } + + function extractCodeFromOrder(payload, ignoredPhoneCodeKeys = []) { + const smsList = Array.isArray(payload?.sms) ? payload.sms : []; + const ignoredKeys = new Set(normalizeStringList(ignoredPhoneCodeKeys)); for (let index = smsList.length - 1; index >= 0; index -= 1) { const message = smsList[index] || {}; + if (ignoredKeys.has(buildSmsCodeKey(message))) { + continue; + } const code = extractVerificationCode(message.code) || extractVerificationCode(message.text); if (code) { return code; @@ -838,7 +881,7 @@ timeoutMs, }); } - const code = extractCodeFromOrder(payload); + const code = extractCodeFromOrder(payload, normalizedActivation.ignoredPhoneCodeKeys); if (code) { return code; } diff --git a/tests/five-sim-provider.test.js b/tests/five-sim-provider.test.js index bc9a041..f41dd42 100644 --- a/tests/five-sim-provider.test.js +++ b/tests/five-sim-provider.test.js @@ -112,6 +112,7 @@ test('5sim provider buys, checks, finishes, cancels, bans, and keeps original ac assert.equal(activation.countryId, 'vietnam'); assert.equal(code, '112233'); assert.equal(reused.activationId, '1001'); + assert.deepStrictEqual(reused.ignoredPhoneCodeKeys, ['code 112233']); const buy = requests.find((entry) => entry.url.pathname.includes('/buy/activation')); assert.equal(buy.url.searchParams.get('maxPrice'), '12'); assert.equal(buy.url.searchParams.get('reuse'), '1'); @@ -125,6 +126,7 @@ test('5sim provider buys, checks, finishes, cancels, bans, and keeps original ac '/v1/user/finish/1001', '/v1/user/cancel/1001', '/v1/user/ban/1001', + '/v1/user/check/1001', ] ); }); diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index 27a416d..7663ba5 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -2880,6 +2880,18 @@ test('phone verification helper reuses 5sim by keeping the original activation', fetchImpl: async (url) => { const parsedUrl = new URL(url); requests.push(parsedUrl.pathname); + if (parsedUrl.pathname === '/v1/user/check/600001') { + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ + id: '600001', + phone: '+44 7911-123-456', + status: 'RECEIVED', + sms: [{ id: 'old', code: '111111' }], + }), + }; + } throw new Error(`Unexpected 5sim request: ${parsedUrl.pathname}`); }, getState: async () => ({ @@ -2925,8 +2937,9 @@ test('phone verification helper reuses 5sim by keeping the original activation', successfulUses: 0, maxUses: 1, source: '5sim-retained-reuse', + ignoredPhoneCodeKeys: ['old::111111'], }); - assert.deepStrictEqual(requests, []); + assert.deepStrictEqual(requests, ['/v1/user/check/600001']); }); test('phone verification helper acquires a number from NexSMS with ordered fallback countries', async () => { @@ -6090,6 +6103,7 @@ test('phone verification helper preserves newly saved free-reuse activation afte test('phone verification helper auto free-reuses 5sim by polling the retained order without reuse or finish', async () => { const requests = []; + let checkCount = 0; let currentState = { phoneSmsProvider: '5sim', fiveSimApiKey: 'demo-key', @@ -6101,7 +6115,7 @@ test('phone verification helper auto free-reuses 5sim by polling the retained or phoneCodeWaitSeconds: 60, phoneCodeTimeoutWindows: 1, phoneCodePollIntervalSeconds: 1, - phoneCodePollMaxRounds: 1, + phoneCodePollMaxRounds: 2, currentPhoneActivation: null, reusablePhoneActivation: null, freeReusablePhoneActivation: { @@ -6114,6 +6128,8 @@ test('phone verification helper auto free-reuses 5sim by polling the retained or successfulUses: 1, maxUses: 3, source: 'free-manual-reuse', + phoneCodeReceived: true, + phoneCodeReceivedAt: 1716000000000, }, }; @@ -6127,6 +6143,7 @@ test('phone verification helper auto free-reuses 5sim by polling the retained or const parsedUrl = new URL(url); requests.push(parsedUrl); if (parsedUrl.pathname === '/v1/user/check/five-free-1') { + checkCount += 1; return { ok: true, status: 200, @@ -6134,7 +6151,9 @@ test('phone verification helper auto free-reuses 5sim by polling the retained or id: 'five-free-1', phone: '+84901122334', status: 'RECEIVED', - sms: [{ code: '334455' }], + sms: checkCount === 1 + ? [{ id: 'old', code: '111111' }] + : [{ id: 'old', code: '111111' }, { id: 'new', code: '334455' }], }), }; } @@ -6177,8 +6196,9 @@ test('phone verification helper auto free-reuses 5sim by polling the retained or assert.equal(currentState.freeReusablePhoneActivation.provider, '5sim'); assert.equal(currentState.freeReusablePhoneActivation.activationId, 'five-free-1'); assert.equal(currentState.freeReusablePhoneActivation.successfulUses, 2); + assert.equal(Object.prototype.hasOwnProperty.call(currentState.freeReusablePhoneActivation, 'phoneCodeReceived'), false); assert.equal(currentState.reusablePhoneActivation, null); - assert.deepStrictEqual(requests.map((url) => url.pathname), ['/v1/user/check/five-free-1']); + assert.deepStrictEqual(requests.map((url) => url.pathname), ['/v1/user/check/five-free-1', '/v1/user/check/five-free-1']); }); test('phone verification helper retires failed 5sim free-reuse record instead of retrying stale order', async () => { @@ -6207,6 +6227,8 @@ test('phone verification helper retires failed 5sim free-reuse record instead of successfulUses: 1, maxUses: 3, source: 'free-manual-reuse', + phoneCodeReceived: true, + phoneCodeReceivedAt: 1716000000000, }, }; @@ -6227,7 +6249,7 @@ test('phone verification helper retires failed 5sim free-reuse record instead of id: 'five-free-stale', phone: '+84901122999', status: 'FINISHED', - sms: [], + sms: [{ id: 'old', code: '111111' }], }), }; } @@ -6259,7 +6281,7 @@ test('phone verification helper retires failed 5sim free-reuse record instead of ); assert.equal(currentState.freeReusablePhoneActivation, null); - assert.deepStrictEqual(requests.map((url) => url.pathname), ['/v1/user/check/five-free-stale']); + assert.deepStrictEqual(requests.map((url) => url.pathname), ['/v1/user/check/five-free-stale', '/v1/user/check/five-free-stale']); }); test('phone verification helper replaces number immediately when resend is throttled and does not spam resend clicks', async () => { @@ -8484,6 +8506,7 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac test('phone verification helper keeps 5sim reusable activation on the original order', async () => { const requests = []; + let checkCount = 0; let currentState = { phoneSmsProvider: '5sim', fiveSimApiKey: 'demo-key', @@ -8495,7 +8518,7 @@ test('phone verification helper keeps 5sim reusable activation on the original o phoneCodeWaitSeconds: 60, phoneCodeTimeoutWindows: 1, phoneCodePollIntervalSeconds: 1, - phoneCodePollMaxRounds: 1, + phoneCodePollMaxRounds: 2, currentPhoneActivation: null, reusablePhoneActivation: { activationId: '4001', @@ -8519,7 +8542,19 @@ test('phone verification helper keeps 5sim reusable activation on the original o const parsedUrl = new URL(url); requests.push(parsedUrl); if (parsedUrl.pathname === '/v1/user/check/4001') { - return { ok: true, status: 200, text: async () => JSON.stringify({ id: 4001, phone: '+84901122334', status: 'RECEIVED', sms: [{ code: '654321' }] }) }; + checkCount += 1; + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ + id: 4001, + phone: '+84901122334', + status: 'RECEIVED', + sms: checkCount === 1 + ? [{ id: 'old', code: '111111' }] + : [{ id: 'old', code: '111111' }, { id: 'new', code: '654321' }], + }), + }; } if (parsedUrl.pathname.includes('/reuse/') || parsedUrl.pathname.includes('/finish/')) { throw new Error(`5sim free reuse should not call terminal/reuse endpoint: ${parsedUrl.pathname}`); @@ -8533,6 +8568,7 @@ test('phone verification helper keeps 5sim reusable activation on the original o return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' }; } if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + assert.equal(message.payload.code, '654321'); return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' }; } throw new Error(`Unexpected content-script message: ${message.type}`); @@ -8561,6 +8597,7 @@ test('phone verification helper keeps 5sim reusable activation on the original o requests.map((url) => url.pathname), [ '/v1/user/check/4001', + '/v1/user/check/4001', ] ); });