fix: 稳定 5sim 保留订单复用状态
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user