fix(madao): 修复换号后国家区号残留

让 MaDao activation 使用 ISO country code,并避免 routing replace 沿用旧 ticket 的 country。
提交 OpenAI add-phone 前校验当前国家区号与手机号匹配,防止页面残留 Thailand 区号。
This commit is contained in:
Isulew
2026-05-29 12:12:19 +08:00
parent f26f7e949e
commit e50b0fbfeb
5 changed files with 325 additions and 29 deletions
+86 -24
View File
@@ -107,14 +107,14 @@
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' },
{ prefix: '84', id: 10, iso: 'VN', label: 'Vietnam' },
{ prefix: '66', id: 52, iso: 'TH', label: 'Thailand' },
{ prefix: '62', id: 6, iso: 'ID', label: 'Indonesia' },
{ prefix: '44', id: 16, iso: 'GB', label: 'United Kingdom' },
{ prefix: '81', id: 151, iso: 'JP', label: 'Japan' },
{ prefix: '49', id: 43, iso: 'DE', label: 'Germany' },
{ prefix: '33', id: 73, iso: 'FR', label: 'France' },
{ prefix: '1', id: 187, iso: 'US', label: 'USA' },
]);
const activationPriceHintsByKey = new Map();
let activePhoneVerificationLogStep = null;
@@ -587,6 +587,44 @@
return fallbackNormalized || DEFAULT_HERO_SMS_OPERATOR;
}
function normalizeMaDaoCountryCode(value = '', fallback = '') {
const raw = String(value || '').trim() || String(fallback || '').trim();
if (!raw) {
return '';
}
if (/^[a-z]{2}$/i.test(raw)) {
return raw.toUpperCase();
}
return raw;
}
function getRegionDisplayName(regionCode, locale = 'en') {
const normalizedRegionCode = normalizeMaDaoCountryCode(regionCode, '');
const normalizedLocale = String(locale || '').trim();
if (!/^[A-Z]{2}$/.test(normalizedRegionCode) || !normalizedLocale || typeof Intl?.DisplayNames !== 'function') {
return '';
}
try {
return String(
new Intl.DisplayNames([normalizedLocale], { type: 'region' }).of(normalizedRegionCode) || ''
).trim();
} catch {
return '';
}
}
function normalizeMaDaoCountryLabel(value = '', countryCode = '') {
const label = String(value || '').trim();
if (label) {
return label;
}
const normalizedCountryCode = normalizeMaDaoCountryCode(countryCode, '');
if (!normalizedCountryCode) {
return '';
}
return getRegionDisplayName(normalizedCountryCode, 'en') || normalizedCountryCode;
}
function inferHeroSmsCountryFromPhoneNumber(phoneNumber = '') {
const digits = String(phoneNumber || '').replace(/\D+/g, '');
if (!digits) {
@@ -598,10 +636,22 @@
}
return {
id: normalizeCountryId(match.id, 0),
iso: normalizeMaDaoCountryCode(match.iso, ''),
label: normalizeCountryLabel(match.label, `Country #${match.id}`),
};
}
function inferMaDaoCountryFromPhoneNumber(phoneNumber = '') {
const inferred = inferHeroSmsCountryFromPhoneNumber(phoneNumber);
if (!inferred?.iso) {
return null;
}
return {
id: inferred.iso,
label: normalizeMaDaoCountryLabel(inferred.label, inferred.iso),
};
}
function normalizePhoneCodeWaitSeconds(value) {
const parsed = Math.floor(Number(value));
if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -1391,6 +1441,9 @@
const rawProvider = String(record.provider || '').trim();
const provider = normalizePhoneSmsProvider(rawProvider);
const rawCountryId = record.countryId ?? record.country;
const inferredCountry = provider === PHONE_SMS_PROVIDER_MADAO
? inferMaDaoCountryFromPhoneNumber(phoneNumber)
: null;
const fallbackCountryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM
? 'england'
: (provider === PHONE_SMS_PROVIDER_MADAO ? '' : HERO_SMS_COUNTRY_ID);
@@ -1412,10 +1465,13 @@
? normalizeNexSmsCountryId(rawCountryId, 0)
: (
provider === PHONE_SMS_PROVIDER_MADAO
? String(rawCountryId || '').trim()
? normalizeMaDaoCountryCode(rawCountryId, inferredCountry?.id || '')
: normalizeCountryId(rawCountryId, fallbackCountryId)
)
);
const normalizedCountryLabel = provider === PHONE_SMS_PROVIDER_MADAO
? normalizeMaDaoCountryLabel(countryLabel, countryId)
: countryLabel;
const ignoredPhoneCodeKeys = normalizeStringList(record.ignoredPhoneCodeKeys);
return {
activationId,
@@ -1424,7 +1480,7 @@
serviceCode,
countryId,
...(provider === PHONE_SMS_PROVIDER_FIVE_SIM ? { countryCode: countryId } : {}),
...(countryLabel ? { countryLabel } : {}),
...(normalizedCountryLabel ? { countryLabel: normalizedCountryLabel } : {}),
successfulUses: normalizeUseCount(record.successfulUses),
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)),
...(expiresAt > 0 ? { expiresAt } : {}),
@@ -4888,6 +4944,18 @@
const providerId = getActivationProviderId(activation, fallbackState);
const candidates = resolveCountryCandidatesForProvider(fallbackState, providerId);
if (activation && typeof activation === 'object') {
if (providerId === PHONE_SMS_PROVIDER_MADAO) {
const countryId = normalizeMaDaoCountryCode(activation.countryId ?? activation.country, '');
const inferredCountry = inferMaDaoCountryFromPhoneNumber(activation.phoneNumber);
const resolvedCountryId = countryId || inferredCountry?.id || '';
return {
id: resolvedCountryId,
label: normalizeMaDaoCountryLabel(
activation.countryLabel || inferredCountry?.label || '',
resolvedCountryId
),
};
}
if (providerId === PHONE_SMS_PROVIDER_FIVE_SIM) {
const countryId = normalizeFiveSimCountryId(activation.countryId, '');
if (countryId) {
@@ -4898,14 +4966,6 @@
label: normalizeFiveSimCountryLabel(activation.countryLabel, countryId),
};
}
} else if (providerId === PHONE_SMS_PROVIDER_MADAO) {
const countryId = String(activation.countryId || '').trim();
if (countryId) {
return {
id: countryId,
label: normalizeCountryLabel(activation.countryLabel, countryId),
};
}
} else {
const inferredCountry = inferHeroSmsCountryFromPhoneNumber(activation.phoneNumber);
const rawCountryId = normalizeCountryId(activation.countryId, 0);
@@ -6668,8 +6728,8 @@
return normalizedCountryId >= 0 ? `${normalizedProvider}:${normalizedCountryId}` : '';
}
if (normalizedProvider === PHONE_SMS_PROVIDER_MADAO) {
const normalizedCountryId = String(countryId || '').trim();
return normalizedCountryId ? `${normalizedProvider}:${normalizedCountryId}` : '';
const normalizedCountryCode = normalizeMaDaoCountryCode(countryId, '');
return normalizedCountryCode ? `${normalizedProvider}:${normalizedCountryCode}` : '';
}
const normalizedCountryId = normalizeCountryId(countryId, 0);
return normalizedCountryId > 0 ? `${normalizedProvider}:${normalizedCountryId}` : '';
@@ -6712,7 +6772,7 @@
return matched?.label || `Country #${normalizedCountryId}`;
}
if (normalizedProvider === PHONE_SMS_PROVIDER_MADAO) {
return normalizedCountryKey || 'MaDao';
return normalizeMaDaoCountryLabel('', normalizedCountryKey) || normalizedCountryKey || 'Unknown country';
}
const normalizedCountryId = normalizeCountryId(normalizedCountryKey, 0);
const matched = resolveCountryCandidates(state)
@@ -6816,9 +6876,11 @@
const getCountryFailureKey = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => (
normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_FIVE_SIM
? normalizeFiveSimCountryId(countryId, '')
: (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_MADAO
? String(countryId || '').trim()
: String(normalizeCountryId(countryId, 0) || ''))
: (
normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_MADAO
? normalizeMaDaoCountryCode(countryId, '')
: String(normalizeCountryId(countryId, 0) || '')
)
);
const getCountryFailureCount = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => {
+22 -3
View File
@@ -199,6 +199,23 @@
return String(visibleSpan?.textContent || '').trim();
}
function phoneNumberMatchesDialCode(phoneNumber = '', dialCode = '') {
const digits = normalizePhoneDigits(phoneNumber);
const normalizedDialCode = normalizePhoneDigits(dialCode);
if (!digits || !normalizedDialCode) {
return false;
}
return digits.startsWith(normalizedDialCode) && digits.length > normalizedDialCode.length;
}
function selectedCountryMatchesPhoneNumber(phoneNumber = '') {
const digits = normalizePhoneDigits(phoneNumber);
if (!digits) {
return true;
}
return phoneNumberMatchesDialCode(phoneNumber, getDisplayedDialCode());
}
function toNationalPhoneNumber(value, dialCode) {
const digits = normalizePhoneDigits(value);
const normalizedDialCode = normalizePhoneDigits(dialCode);
@@ -364,15 +381,17 @@
const byLabel = findCountryOptionByLabel(countryLabel);
if (await trySelectCountryOption(select, byLabel)) {
return true;
if (selectedCountryMatchesPhoneNumber(phoneNumber)) {
return true;
}
}
const byPhoneNumber = findCountryOptionByPhoneNumber(phoneNumber);
if (await trySelectCountryOption(select, byPhoneNumber)) {
return true;
return selectedCountryMatchesPhoneNumber(phoneNumber);
}
return Boolean(getSelectedCountryOption());
return selectedCountryMatchesPhoneNumber(phoneNumber);
}
function getAddPhoneSubmitButton() {
-1
View File
@@ -366,7 +366,6 @@
routing_plan_id: activation?.madaoRoutingPlanId,
routing_plan_name: activation?.madaoRoutingPlanName,
service: activation?.serviceCode,
country: activation?.countryId,
});
if (!nextTicket) {
throw new Error('MaDao 返回的下一条路由激活记录无效。');
+62
View File
@@ -356,6 +356,68 @@ test('phone auth can auto-select country by dial code even when number has no pl
}
});
test('phone auth rejects stale selected country when it does not match phone dial code', async () => {
const originalDocument = global.document;
const originalEvent = global.Event;
const originalLocation = global.location;
const dom = createFakeAddPhoneDom({
options: [
{ value: 'TH', textContent: 'Thailand (+66)', buttonText: 'Thailand (+66)' },
{ value: 'GB', textContent: 'United Kingdom (+44)', buttonText: 'United Kingdom (+44)' },
],
selectedIndex: 0,
});
global.document = dom.document;
global.Event = class Event {
constructor(type) {
this.type = type;
}
};
global.location = { href: 'https://auth.openai.com/add-phone' };
try {
const helpers = api.createPhoneAuthHelpers({
fillInput: (element, value) => {
element.value = value;
},
getActionText: () => '',
getPageTextSnapshot: () => '',
getVerificationErrorText: () => '',
humanPause: async () => {},
isActionEnabled: () => true,
isAddPhonePageReady: () => true,
isConsentReady: () => false,
isPhoneVerificationPageReady: () => false,
isVisibleElement: () => true,
simulateClick: (element) => {
element.click?.();
},
sleep: async () => {},
throwIfStopped: () => {},
waitForElement: async () => null,
});
await assert.rejects(
() => helpers.submitPhoneNumber({
countryLabel: 'Country #10',
phoneNumber: '+84943328460',
}),
/Failed to select "Country #10" on the add-phone page\./
);
assert.equal(dom.select.value, 'TH');
assert.equal(dom.phoneInput.value, '');
assert.equal(dom.hiddenPhoneInput.value, '');
assert.equal(dom.wasSubmitClicked(), false);
} finally {
global.document = originalDocument;
global.Event = originalEvent;
global.location = originalLocation;
}
});
test('phone auth resend stops with PHONE_ROUTE_405_RECOVERY_FAILED instead of endless Try-again loop', async () => {
const originalDocument = global.document;
const originalLocation = global.location;
+155 -1
View File
@@ -285,7 +285,6 @@ test('phone verification helper acquires, polls and releases MaDao activation th
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
createMaDaoProvider: maDaoModule.createProvider,
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url);
@@ -365,6 +364,161 @@ test('phone verification helper acquires, polls and releases MaDao activation th
assert.deepStrictEqual(requests[2].body, { ticket_id: 'madao-1', action: 'finish' });
});
test('phone verification helper keeps MaDao routing replacement country as ISO code on resubmit', async () => {
const requests = [];
const messages = [];
let currentState = {
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822/',
madaoHttpSecret: 'secret-token',
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'rp-openai',
verificationResendCount: 0,
phoneVerificationReplacementLimit: 2,
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
};
let acquireCalls = 0;
let replaceCalls = 0;
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url);
const body = options.body ? JSON.parse(options.body) : null;
requests.push({ url: parsedUrl, options, body });
if (parsedUrl.pathname === '/api/acquire') {
acquireCalls += 1;
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: 'madao-ticket-routing-1',
provider: 'herosms',
service: 'openai',
country: 'TH',
phone_number: '+66950002222',
routing_plan_id: 'rp-openai',
routing_plan_name: 'OpenAI Plan',
routing_item_id: 'route-1',
status: 'waiting_code',
}),
};
}
if (parsedUrl.pathname === '/api/poll') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: body.ticket_id,
provider: body.ticket_id === 'madao-ticket-routing-1' ? 'herosms' : 'smsbower',
status: body.ticket_id === 'madao-ticket-routing-1' ? 'waiting_code' : 'code_received',
code: body.ticket_id === 'madao-ticket-routing-1' ? null : '654321',
}),
};
}
if (parsedUrl.pathname === '/api/routing/replace') {
replaceCalls += 1;
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
current_ticket_id: 'madao-ticket-routing-1',
current_ticket_release: {
ticket_id: 'madao-ticket-routing-1',
provider: 'herosms',
status: 'cancelled',
},
next_ticket: {
ticket_id: 'madao-ticket-routing-2',
provider: 'smsbower',
service: 'openai',
country: 'VN',
phone_number: '+84943328460',
routing_plan_id: 'rp-openai',
routing_plan_name: 'OpenAI Plan',
routing_item_id: 'route-2',
status: 'waiting_code',
},
}),
};
}
if (parsedUrl.pathname === '/api/release') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({ ok: true }),
};
}
throw new Error(`Unexpected MaDao path: ${parsedUrl.pathname}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
messages.push(message);
if (message.type === 'STEP8_GET_STATE') {
return {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
};
}
if (message.type === 'SUBMIT_PHONE_NUMBER') {
return {
phoneVerificationPage: true,
url: 'https://auth.openai.com/phone-verification',
};
}
if (message.type === 'RETURN_TO_ADD_PHONE') {
return {
addPhonePage: true,
url: 'https://auth.openai.com/add-phone',
};
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
return {
success: true,
consentReady: true,
url: 'https://auth.openai.com/authorize',
};
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const result = await helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
});
assert.deepStrictEqual(result, {
success: true,
consentReady: true,
url: 'https://auth.openai.com/authorize',
});
assert.equal(acquireCalls, 1);
assert.equal(replaceCalls, 1);
const phoneSubmissions = messages.filter((message) => message.type === 'SUBMIT_PHONE_NUMBER');
assert.equal(phoneSubmissions.length, 2);
assert.equal(phoneSubmissions[0].payload.phoneNumber, '+66950002222');
assert.equal(phoneSubmissions[0].payload.countryId, 'TH');
assert.equal(phoneSubmissions[0].payload.countryLabel, 'Thailand');
assert.equal(phoneSubmissions[1].payload.phoneNumber, '+84943328460');
assert.equal(phoneSubmissions[1].payload.countryId, 'VN');
assert.equal(phoneSubmissions[1].payload.countryLabel, 'Vietnam');
});
test('signup phone helper persists signup runtime state without touching add-phone activation', async () => {
const setStateCalls = [];
let currentState = {