Merge pull request #293 from netcookies/fix/madao-country-iso-dev

fix(madao): 适配国家中文标签和线路归一化
This commit is contained in:
QLHazyCoder
2026-05-30 04:09:50 +08:00
committed by GitHub
17 changed files with 4738 additions and 3597 deletions
+11 -2
View File
@@ -21,6 +21,7 @@ importScripts(
'gopay-utils.js', 'gopay-utils.js',
'phone-sms/providers/hero-sms.js', 'phone-sms/providers/hero-sms.js',
'phone-sms/providers/five-sim.js', 'phone-sms/providers/five-sim.js',
'phone-sms/providers/nexsms.js',
'phone-sms/providers/madao.js', 'phone-sms/providers/madao.js',
'phone-sms/providers/registry.js', 'phone-sms/providers/registry.js',
'background/phone-verification-flow.js', 'background/phone-verification-flow.js',
@@ -1452,6 +1453,7 @@ const PERSISTED_SETTING_DEFAULTS = {
madaoRoutingPlanId: '', madaoRoutingPlanId: '',
madaoProviderId: '', madaoProviderId: '',
madaoCountry: '', madaoCountry: '',
madaoOperator: '',
madaoAutoPickCountry: true, madaoAutoPickCountry: true,
madaoReusePhone: true, madaoReusePhone: true,
madaoMinPrice: '', madaoMinPrice: '',
@@ -2244,6 +2246,13 @@ function normalizeMaDaoPrice(value = '') {
return normalizeHeroSmsMaxPrice(value); return normalizeHeroSmsMaxPrice(value);
} }
function normalizeMaDaoOperator(value = '') {
return String(value || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9_-]+/g, '');
}
function normalizePhonePreferredActivation(value) { function normalizePhonePreferredActivation(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) { if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null; return null;
@@ -3594,6 +3603,8 @@ function normalizePersistentSettingValue(key, value) {
return normalizeMaDaoProviderId(value); return normalizeMaDaoProviderId(value);
case 'madaoCountry': case 'madaoCountry':
return normalizeMaDaoCountry(value); return normalizeMaDaoCountry(value);
case 'madaoOperator':
return normalizeMaDaoOperator(value);
case 'madaoAutoPickCountry': case 'madaoAutoPickCountry':
case 'madaoReusePhone': case 'madaoReusePhone':
return Boolean(value); return Boolean(value);
@@ -13876,8 +13887,6 @@ const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.crea
setState, setState,
sleepWithStop, sleepWithStop,
throwIfStopped, throwIfStopped,
createFiveSimProvider: self.PhoneSmsFiveSimProvider?.createProvider,
createMaDaoProvider: self.PhoneSmsMaDaoProvider?.createProvider,
}); });
const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({ const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({
addLog, addLog,
File diff suppressed because it is too large Load Diff
+22 -3
View File
@@ -199,6 +199,23 @@
return String(visibleSpan?.textContent || '').trim(); 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) { function toNationalPhoneNumber(value, dialCode) {
const digits = normalizePhoneDigits(value); const digits = normalizePhoneDigits(value);
const normalizedDialCode = normalizePhoneDigits(dialCode); const normalizedDialCode = normalizePhoneDigits(dialCode);
@@ -364,15 +381,17 @@
const byLabel = findCountryOptionByLabel(countryLabel); const byLabel = findCountryOptionByLabel(countryLabel);
if (await trySelectCountryOption(select, byLabel)) { if (await trySelectCountryOption(select, byLabel)) {
return true; if (selectedCountryMatchesPhoneNumber(phoneNumber)) {
return true;
}
} }
const byPhoneNumber = findCountryOptionByPhoneNumber(phoneNumber); const byPhoneNumber = findCountryOptionByPhoneNumber(phoneNumber);
if (await trySelectCountryOption(select, byPhoneNumber)) { if (await trySelectCountryOption(select, byPhoneNumber)) {
return true; return selectedCountryMatchesPhoneNumber(phoneNumber);
} }
return Boolean(getSelectedCountryOption()); return selectedCountryMatchesPhoneNumber(phoneNumber);
} }
function getAddPhoneSubmitButton() { function getAddPhoneSubmitButton() {
+183 -4
View File
@@ -150,6 +150,10 @@
return String(value || '').trim() || fallback; return String(value || '').trim() || fallback;
} }
function normalizeCountryKey(value) {
return normalizeFiveSimCountryId(value, '');
}
function formatFiveSimCountryLabel(id = '', englishValue = '', fallback = DEFAULT_COUNTRY_LABEL) { function formatFiveSimCountryLabel(id = '', englishValue = '', fallback = DEFAULT_COUNTRY_LABEL) {
const countryId = normalizeFiveSimCountryId(id, ''); const countryId = normalizeFiveSimCountryId(id, '');
const english = normalizeFiveSimCountryLabel(englishValue || countryId || fallback, fallback); const english = normalizeFiveSimCountryLabel(englishValue || countryId || fallback, fallback);
@@ -181,6 +185,42 @@
return String(Math.round(numeric * 10000) / 10000); return String(Math.round(numeric * 10000) / 10000);
} }
function normalizePriceLimit(value = '') {
const normalized = normalizeFiveSimMaxPrice(value);
if (!normalized) {
return null;
}
const numeric = Number(normalized);
return Number.isFinite(numeric) && numeric > 0 ? numeric : null;
}
function resolvePriceRange(state = {}) {
const minPriceLimit = normalizePriceLimit(state?.fiveSimMinPrice);
const maxPriceLimit = normalizePriceLimit(state?.fiveSimMaxPrice);
return {
minPriceLimit,
maxPriceLimit,
hasMinPriceLimit: minPriceLimit !== null,
hasMaxPriceLimit: maxPriceLimit !== null,
invalidRange: minPriceLimit !== null && maxPriceLimit !== null && minPriceLimit > maxPriceLimit,
};
}
function formatPriceRangeText(minPriceLimit = null, maxPriceLimit = null) {
const minPrice = normalizePriceLimit(minPriceLimit);
const maxPrice = normalizePriceLimit(maxPriceLimit);
if (minPrice !== null && maxPrice !== null) {
return `${minPrice}~${maxPrice}`;
}
if (minPrice !== null) {
return `${minPrice}~`;
}
if (maxPrice !== null) {
return `~${maxPrice}`;
}
return 'unbounded';
}
function normalizeFiveSimCountryFallback(value = []) { function normalizeFiveSimCountryFallback(value = []) {
const source = Array.isArray(value) const source = Array.isArray(value)
? value ? value
@@ -371,6 +411,20 @@
} }
function resolveCountryCandidates(state = {}) { function resolveCountryCandidates(state = {}) {
const orderedCountries = normalizeFiveSimCountryFallback(state.fiveSimCountryOrder);
if (orderedCountries.length) {
const primaryCountryId = normalizeFiveSimCountryId(state.fiveSimCountryId, '');
return orderedCountries.map((entry) => {
const id = normalizeFiveSimCountryId(entry.id, '');
return {
id,
label: primaryCountryId && id === primaryCountryId
? formatFiveSimCountryLabel(id, state.fiveSimCountryLabel || entry.label || id, id)
: formatFiveSimCountryLabel(id, entry.label || id, id),
};
});
}
const primary = resolveCountryConfig(state); const primary = resolveCountryConfig(state);
const fallbackList = normalizeFiveSimCountryFallback(state.fiveSimCountryFallback); const fallbackList = normalizeFiveSimCountryFallback(state.fiveSimCountryFallback);
const seen = new Set([primary.id]); const seen = new Set([primary.id]);
@@ -391,6 +445,13 @@
return candidates; return candidates;
} }
function resolveCountryLabel(state = {}, countryId = DEFAULT_COUNTRY_ID) {
const countryKey = normalizeCountryKey(countryId);
const matched = resolveCountryCandidates(state)
.find((entry) => normalizeCountryKey(entry.id) === countryKey);
return matched?.label || formatFiveSimCountryLabel(countryKey, countryKey, countryKey || DEFAULT_COUNTRY_LABEL);
}
async function fetchBalance(state = {}, deps = {}) { async function fetchBalance(state = {}, deps = {}) {
const config = resolveConfig(state, deps); const config = resolveConfig(state, deps);
const payload = await fetchJson(config, '/v1/user/profile', { const payload = await fetchJson(config, '/v1/user/profile', {
@@ -496,6 +557,8 @@
async function resolvePricePlan(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) { async function resolvePricePlan(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
const userLimitText = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice); const userLimitText = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
const userLimit = userLimitText ? Number(userLimitText) : null; const userLimit = userLimitText ? Number(userLimitText) : null;
const userMinText = normalizeFiveSimMaxPrice(state.fiveSimMinPrice);
const userMinLimit = userMinText ? Number(userMinText) : null;
let priceCandidates = []; let priceCandidates = [];
try { try {
@@ -526,19 +589,24 @@
priceCandidates = buildSortedUniquePriceCandidates(priceCandidates); priceCandidates = buildSortedUniquePriceCandidates(priceCandidates);
const minCatalogPrice = priceCandidates.length > 0 ? priceCandidates[0] : null; const minCatalogPrice = priceCandidates.length > 0 ? priceCandidates[0] : null;
const rangeFilteredPrices = priceCandidates.filter((price) => (
(userMinLimit === null || price >= userMinLimit)
&& (userLimit === null || price <= userLimit)
));
if (userLimit !== null) { if (userLimit !== null) {
const bounded = priceCandidates.filter((price) => price <= userLimit); const bounded = rangeFilteredPrices;
return { return {
prices: bounded.length > 0 ? [userLimit, ...bounded.filter((price) => price !== userLimit)] : [userLimit], prices: bounded.length > 0 ? [userLimit, ...bounded.filter((price) => price !== userLimit)] : [userLimit],
userLimit, userLimit,
userMinLimit,
minCatalogPrice, minCatalogPrice,
}; };
} }
if (priceCandidates.length > 0) { if (rangeFilteredPrices.length > 0) {
return { prices: priceCandidates, userLimit: null, minCatalogPrice }; return { prices: rangeFilteredPrices, userLimit: null, userMinLimit, minCatalogPrice };
} }
return { prices: [null], userLimit: null, minCatalogPrice: null }; return { prices: [null], userLimit: null, userMinLimit, minCatalogPrice };
} }
function normalizeActivation(record, fallback = {}) { function normalizeActivation(record, fallback = {}) {
@@ -558,6 +626,7 @@
provider: PROVIDER_ID, provider: PROVIDER_ID,
serviceCode: DEFAULT_PRODUCT, serviceCode: DEFAULT_PRODUCT,
countryId, countryId,
countryCode: countryId,
countryLabel, countryLabel,
successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)), successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)),
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_MAX_USES)), maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_MAX_USES)),
@@ -570,6 +639,40 @@
}; };
} }
function resolveActivationCountry(activation = {}, state = {}) {
const normalizedActivation = normalizeActivation(activation)
|| (activation && typeof activation === 'object' ? activation : {});
const countryId = normalizeFiveSimCountryId(
normalizedActivation.countryCode ?? normalizedActivation.countryId ?? normalizedActivation.country,
DEFAULT_COUNTRY_ID
);
const matched = resolveCountryCandidates(state)
.find((entry) => normalizeCountryKey(entry.id) === countryId);
if (matched) {
return matched;
}
return {
id: countryId,
code: countryId,
label: normalizeFiveSimCountryLabel(
normalizedActivation.countryLabel,
formatFiveSimCountryLabel(countryId, countryId, countryId)
),
};
}
function getActivationCountryKey(activation = {}) {
return normalizeCountryKey(activation?.countryCode ?? activation?.countryId ?? activation?.country);
}
function getActivationPrice(activation = {}) {
return normalizePrice(
activation?.selectedPrice
?? activation?.price
?? activation?.maxPrice
);
}
function isNoNumbersPayload(payloadOrMessage) { function isNoNumbersPayload(payloadOrMessage) {
const text = describePayload(payloadOrMessage); const text = describePayload(payloadOrMessage);
return /no\s+free\s+phones|no\s+numbers|not\s+found/i.test(text); return /no\s+free\s+phones|no\s+numbers|not\s+found/i.test(text);
@@ -594,10 +697,17 @@
function assertMaxPriceCompatibleWithOperator(state = {}) { function assertMaxPriceCompatibleWithOperator(state = {}) {
const maxPrice = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice); const maxPrice = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
const minPrice = normalizeFiveSimMaxPrice(state.fiveSimMinPrice);
const operator = normalizeFiveSimOperator(state.fiveSimOperator); const operator = normalizeFiveSimOperator(state.fiveSimOperator);
if (maxPrice && operator !== DEFAULT_OPERATOR) { if (maxPrice && operator !== DEFAULT_OPERATOR) {
throw new Error('5sim 价格上限仅支持运营商为 "any" 时使用;请清空价格上限,或先把运营商切换为 any。'); throw new Error('5sim 价格上限仅支持运营商为 "any" 时使用;请清空价格上限,或先把运营商切换为 any。');
} }
if (minPrice && operator !== DEFAULT_OPERATOR) {
throw new Error('5sim 最低购买价仅支持运营商为 "any" 时使用;请清空最低购买价,或先把运营商切换为 any。');
}
if (minPrice && maxPrice && Number(minPrice) > Number(maxPrice)) {
throw new Error(`5sim 价格区间无效:最低购买价 ${minPrice} 高于价格上限 ${maxPrice}`);
}
} }
function normalizeReuseEnabled(state = {}) { function normalizeReuseEnabled(state = {}) {
@@ -777,6 +887,22 @@
}; };
} }
async function prepareActivationForReuse(state = {}, activation, _options = {}, deps = {}) {
try {
const retainedActivation = await reuseActivation(state, activation, deps);
return {
ok: true,
activation: retainedActivation,
};
} catch (error) {
return {
ok: false,
reason: 'reuse_check_failed',
message: error.message || '5sim 复用手机号基线检查失败。',
};
}
}
async function finishActivation(state = {}, activation, deps = {}) { async function finishActivation(state = {}, activation, deps = {}) {
const normalizedActivation = normalizeActivation(activation); const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) return ''; if (!normalizedActivation) return '';
@@ -807,6 +933,25 @@
return describePayload(payload); return describePayload(payload);
} }
async function requestAdditionalSms() {
return '';
}
async function rotateActivation(state = {}, activation, options = {}, deps = {}) {
const releaseAction = String(options?.releaseAction || '').trim().toLowerCase() === 'ban'
? 'ban'
: 'cancel';
if (releaseAction === 'ban') {
await banActivation(state, activation, deps);
} else {
await cancelActivation(state, activation, deps);
}
return {
currentTicketId: String(activation?.activationId || activation?.id || ''),
nextActivation: null,
};
}
function extractVerificationCode(rawCodeOrText) { function extractVerificationCode(rawCodeOrText) {
const trimmed = String(rawCodeOrText || '').trim(); const trimmed = String(rawCodeOrText || '').trim();
if (!trimmed) { if (!trimmed) {
@@ -913,9 +1058,18 @@
addLog: deps.addLog, addLog: deps.addLog,
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS, requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
}; };
const capabilities = Object.freeze({
supportsReusableActivation: true,
supportsAutomaticFreeReuse: true,
supportsFreeReusePreservation: true,
supportsPageResend: false,
supportsPageResendProbe: true,
requiresCountrySelection: true,
});
return { return {
id: PROVIDER_ID, id: PROVIDER_ID,
label: '5sim', label: '5sim',
capabilities,
defaultCountryId: DEFAULT_COUNTRY_ID, defaultCountryId: DEFAULT_COUNTRY_ID,
defaultCountryLabel: DEFAULT_COUNTRY_LABEL, defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
supportedCountries: SUPPORTED_COUNTRY_ITEMS, supportedCountries: SUPPORTED_COUNTRY_ITEMS,
@@ -925,18 +1079,38 @@
normalizeCountryLabel: normalizeFiveSimCountryLabel, normalizeCountryLabel: normalizeFiveSimCountryLabel,
formatCountryLabel: formatFiveSimCountryLabel, formatCountryLabel: formatFiveSimCountryLabel,
normalizeCountryFallback: normalizeFiveSimCountryFallback, normalizeCountryFallback: normalizeFiveSimCountryFallback,
normalizeCountryKey,
normalizeMaxPrice: normalizeFiveSimMaxPrice, normalizeMaxPrice: normalizeFiveSimMaxPrice,
normalizeOperator: normalizeFiveSimOperator, normalizeOperator: normalizeFiveSimOperator,
normalizeActivation,
resolveCountryCandidates, resolveCountryCandidates,
resolveCountryLabel,
resolveActivationCountry,
getActivationCountryKey,
getActivationPrice,
requestActivation: (state, options) => requestActivation(state, options, providerDeps), requestActivation: (state, options) => requestActivation(state, options, providerDeps),
reuseActivation: (state, activation) => reuseActivation(state, activation, providerDeps), reuseActivation: (state, activation) => reuseActivation(state, activation, providerDeps),
finishActivation: (state, activation) => finishActivation(state, activation, providerDeps), finishActivation: (state, activation) => finishActivation(state, activation, providerDeps),
cancelActivation: (state, activation) => cancelActivation(state, activation, providerDeps), cancelActivation: (state, activation) => cancelActivation(state, activation, providerDeps),
banActivation: (state, activation) => banActivation(state, activation, providerDeps), banActivation: (state, activation) => banActivation(state, activation, providerDeps),
requestAdditionalSms: (state, activation) => requestAdditionalSms(state, activation, providerDeps),
rotateActivation: (state, activation, options) => rotateActivation(state, activation, options, providerDeps),
pollActivationCode: (state, activation, options) => pollActivationCode(state, activation, options, providerDeps), pollActivationCode: (state, activation, options) => pollActivationCode(state, activation, options, providerDeps),
prepareActivationForReuse: (state, activation, options) => prepareActivationForReuse(state, activation, options, providerDeps),
canPersistReusableActivation: () => true,
canPreserveActivationForFreeReuse: (_state, activation) => Boolean(
normalizeActivation(activation)
&& activation
&& typeof activation === 'object'
&& activation.phoneCodeReceived
),
shouldUsePageResend: () => false,
shouldProbePageResend: () => true,
fetchBalance: (state) => fetchBalance(state, providerDeps), fetchBalance: (state) => fetchBalance(state, providerDeps),
fetchCountries: (state) => fetchCountries(state, providerDeps), fetchCountries: (state) => fetchCountries(state, providerDeps),
fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps), fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps),
resolvePriceRange,
formatPriceRangeText,
collectPriceEntries, collectPriceEntries,
describePayload, describePayload,
}; };
@@ -954,8 +1128,13 @@
normalizeFiveSimCountryFallback, normalizeFiveSimCountryFallback,
normalizeFiveSimCountryId, normalizeFiveSimCountryId,
normalizeFiveSimCountryLabel, normalizeFiveSimCountryLabel,
normalizeCountryKey,
formatFiveSimCountryLabel, formatFiveSimCountryLabel,
normalizeFiveSimMaxPrice, normalizeFiveSimMaxPrice,
normalizeFiveSimOperator, normalizeFiveSimOperator,
resolvePriceRange,
formatPriceRangeText,
normalizeActivation,
resolveActivationCountry,
}; };
}); });
File diff suppressed because it is too large Load Diff
+283 -11
View File
@@ -7,6 +7,19 @@
const DEFAULT_SERVICE = 'openai'; const DEFAULT_SERVICE = 'openai';
const DEFAULT_MODE = 'routing_plan'; const DEFAULT_MODE = 'routing_plan';
const DEFAULT_REQUEST_TIMEOUT_MS = 20000; const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
const DEFAULT_POLL_TIMEOUT_MS = 180000;
const DEFAULT_POLL_INTERVAL_MS = 5000;
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
const COUNTRY_BY_PHONE_PREFIX = Object.freeze([
{ prefix: '84', id: 'VN', label: 'Vietnam' },
{ prefix: '66', id: 'TH', label: 'Thailand' },
{ prefix: '62', id: 'ID', label: 'Indonesia' },
{ prefix: '44', id: 'GB', label: 'United Kingdom' },
{ prefix: '81', id: 'JP', label: 'Japan' },
{ prefix: '49', id: 'DE', label: 'Germany' },
{ prefix: '33', id: 'FR', label: 'France' },
{ prefix: '1', id: 'US', label: 'USA' },
]);
function normalizeText(value = '', fallback = '') { function normalizeText(value = '', fallback = '') {
return String(value || '').trim() || fallback; return String(value || '').trim() || fallback;
@@ -29,6 +42,15 @@
return normalizeText(value).toLowerCase().replace(/[^a-z0-9_-]+/g, ''); return normalizeText(value).toLowerCase().replace(/[^a-z0-9_-]+/g, '');
} }
function normalizeOperator(value = '') {
const rawValue = normalizeText(value);
const compactValue = rawValue.toLowerCase().replace(/[^a-z0-9]+/g, '');
if (!rawValue || compactValue === 'any' || compactValue === 'anyoperator') {
return '';
}
return rawValue.toLowerCase().replace(/[^a-z0-9_-]+/g, '');
}
function normalizeCountry(value = '') { function normalizeCountry(value = '') {
const trimmed = normalizeText(value); const trimmed = normalizeText(value);
if (!trimmed) { if (!trimmed) {
@@ -44,6 +66,52 @@
return lowered.replace(/[^a-z0-9_-]+/g, ''); return lowered.replace(/[^a-z0-9_-]+/g, '');
} }
function normalizeCountryKey(value) {
return normalizeCountry(value);
}
function getRegionDisplayName(regionCode, locale = 'en') {
const normalizedRegionCode = normalizeCountry(regionCode);
const normalizedLocale = normalizeText(locale);
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 normalizeCountryLabel(value = '', countryCode = '') {
const label = normalizeText(value);
if (label) {
return label;
}
const normalizedCountryCode = normalizeCountry(countryCode);
if (!normalizedCountryCode) {
return '';
}
return getRegionDisplayName(normalizedCountryCode, 'en') || normalizedCountryCode;
}
function inferCountryFromPhoneNumber(phoneNumber = '') {
const digits = String(phoneNumber || '').replace(/\D+/g, '');
if (!digits) {
return null;
}
const match = COUNTRY_BY_PHONE_PREFIX.find((entry) => digits.startsWith(entry.prefix));
if (!match) {
return null;
}
return {
id: normalizeCountry(match.id),
label: normalizeCountryLabel(match.label, match.id),
};
}
function normalizeBoolean(value, fallback = false) { function normalizeBoolean(value, fallback = false) {
if (value === undefined || value === null || value === '') { if (value === undefined || value === null || value === '') {
return Boolean(fallback); return Boolean(fallback);
@@ -196,6 +264,7 @@
service: normalizeText(options?.service || state?.madaoServiceName, DEFAULT_SERVICE), service: normalizeText(options?.service || state?.madaoServiceName, DEFAULT_SERVICE),
}; };
const country = normalizeCountry(options?.country || state?.madaoCountry); const country = normalizeCountry(options?.country || state?.madaoCountry);
const operator = normalizeOperator(options?.operator || state?.madaoOperator);
const minPrice = normalizePrice(options?.minPrice ?? state?.madaoMinPrice); const minPrice = normalizePrice(options?.minPrice ?? state?.madaoMinPrice);
const maxPrice = normalizePrice(options?.maxPrice ?? state?.madaoMaxPrice); const maxPrice = normalizePrice(options?.maxPrice ?? state?.madaoMaxPrice);
@@ -209,6 +278,9 @@
if (country) { if (country) {
request.country = country; request.country = country;
} }
if (operator) {
request.metadata = { operator };
}
if (minPrice !== null) { if (minPrice !== null) {
request.min_price = minPrice; request.min_price = minPrice;
} }
@@ -245,6 +317,68 @@
}; };
} }
function normalizeActivation(record = {}, fallback = {}) {
const direct = normalizeActivationFromAcquire(record, fallback);
if (direct) {
return direct;
}
const source = record && typeof record === 'object' && !Array.isArray(record) ? record : {};
const ticketId = normalizeText(source.activationId || source.ticketId || source.id || fallback.activationId);
const phoneNumber = normalizeText(source.phoneNumber || source.phone || fallback.phoneNumber);
if (!ticketId || !phoneNumber) {
return null;
}
const inferredCountry = inferCountryFromPhoneNumber(phoneNumber);
const countryId = normalizeCountry(source.countryId ?? source.country ?? fallback.countryId ?? inferredCountry?.id);
const price = normalizePrice(source.madaoPrice ?? source.price ?? fallback.madaoPrice ?? fallback.price);
return {
activationId: ticketId,
phoneNumber,
provider: PROVIDER_ID,
serviceCode: normalizeText(source.serviceCode || source.service || fallback.serviceCode, DEFAULT_SERVICE),
countryId,
countryLabel: normalizeCountryLabel(source.countryLabel || source.country_label || fallback.countryLabel, countryId),
maxUses: Math.max(1, Math.floor(Number(source.maxUses ?? fallback.maxUses) || 1)),
successfulUses: Math.max(0, Math.floor(Number(source.successfulUses ?? fallback.successfulUses) || 0)),
...(source.source ? { source: normalizeText(source.source) } : {}),
...(source.phoneCodeReceived ? { phoneCodeReceived: true } : {}),
...(source.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(source.phoneCodeReceivedAt) || 0) } : {}),
...(source.madaoProviderId ? { madaoProviderId: normalizeProviderId(source.madaoProviderId) } : {}),
...(source.madaoRoutingPlanId ? { madaoRoutingPlanId: normalizeText(source.madaoRoutingPlanId) } : {}),
...(source.madaoRoutingPlanName ? { madaoRoutingPlanName: normalizeText(source.madaoRoutingPlanName) } : {}),
...(source.madaoRoutingItemId ? { madaoRoutingItemId: normalizeText(source.madaoRoutingItemId) } : {}),
...(source.madaoAcquirePath ? { madaoAcquirePath: mapAcquirePath(source.madaoAcquirePath) } : {}),
...(source.madaoStatus ? { madaoStatus: mapTicketStatus(source.madaoStatus) } : {}),
...(price !== null ? { madaoPrice: price } : {}),
};
}
function resolveCountryLabel(_state = {}, countryId = '') {
return normalizeCountryLabel('', countryId);
}
function resolveActivationCountry(activation = {}) {
const normalizedActivation = normalizeActivation(activation)
|| (activation && typeof activation === 'object' ? activation : {});
const inferredCountry = inferCountryFromPhoneNumber(normalizedActivation.phoneNumber);
const countryId = normalizeCountry(normalizedActivation.countryId ?? normalizedActivation.country ?? inferredCountry?.id);
return {
id: countryId,
label: normalizeCountryLabel(normalizedActivation.countryLabel || inferredCountry?.label, countryId),
};
}
function getActivationCountryKey(activation = {}) {
const normalizedActivation = normalizeActivation(activation)
|| (activation && typeof activation === 'object' ? activation : {});
const inferredCountry = inferCountryFromPhoneNumber(normalizedActivation.phoneNumber);
return normalizeCountryKey(normalizedActivation.countryId ?? normalizedActivation.country ?? inferredCountry?.id);
}
function getActivationPrice(activation = {}) {
return normalizePrice(activation?.madaoPrice ?? activation?.selectedPrice ?? activation?.price ?? activation?.maxPrice);
}
function extractVerificationCode(value = '') { function extractVerificationCode(value = '') {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (!text) { if (!text) {
@@ -315,18 +449,75 @@
} }
async function pollActivationCode(state = {}, activation, options = {}, deps = {}) { async function pollActivationCode(state = {}, activation, options = {}, deps = {}) {
const payload = await pollActivation(state, activation, deps); const configuredTimeoutMs = Number(options.timeoutMs);
const code = extractCodeFromPollPayload(payload); const timeoutMs = Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs > 0
if (code) { ? Math.max(1000, configuredTimeoutMs)
return code; : 0;
if (!timeoutMs) {
const payload = await pollActivation(state, activation, deps);
const code = extractCodeFromPollPayload(payload);
if (code) {
return code;
}
if (typeof options.onStatus === 'function') {
await options.onStatus({
activation,
statusText: describePayload(payload) || 'PENDING',
});
}
return '';
} }
if (typeof options.onStatus === 'function') {
await options.onStatus({ const intervalMs = Math.max(1000, Number(options.intervalMs) || DEFAULT_POLL_INTERVAL_MS);
activation, const maxRoundsRaw = Math.floor(Number(options.maxRounds));
statusText: describePayload(payload) || 'PENDING', const maxRounds = Number.isFinite(maxRoundsRaw) && maxRoundsRaw > 0 ? maxRoundsRaw : 0;
}); const start = Date.now();
let pollCount = 0;
let lastResponse = '';
while (Date.now() - start < timeoutMs) {
if (maxRounds > 0 && pollCount >= maxRounds) {
break;
}
deps.throwIfStopped?.();
const payload = await pollActivation(state, activation, deps);
const code = extractCodeFromPollPayload(payload);
const statusText = normalizeText(
payload?.status
|| payload?.madaoStatus
|| payload?.message
|| payload?.text
|| describePayload(payload),
'PENDING'
);
lastResponse = statusText;
pollCount += 1;
if (typeof options.onStatus === 'function') {
await options.onStatus({
activation,
elapsedMs: Date.now() - start,
pollCount,
statusText,
timeoutMs,
});
}
if (code) {
return code;
}
if (/^(cancelled|canceled|failed|expired|timeout)$/i.test(statusText)) {
throw new Error(`MaDao 订单在收到短信前已结束:${statusText}`);
}
if (typeof options.onWaitingForCode === 'function') {
await options.onWaitingForCode({
activation,
elapsedMs: Date.now() - start,
pollCount,
statusText,
timeoutMs,
});
}
await deps.sleepWithStop?.(intervalMs);
} }
return ''; throw new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}等待手机验证码超时。${lastResponse ? ` MaDao 最后状态:${lastResponse}` : ''}`);
} }
async function releaseActivation(state = {}, activation, action = 'cancel', deps = {}) { async function releaseActivation(state = {}, activation, action = 'cancel', deps = {}) {
@@ -366,7 +557,6 @@
routing_plan_id: activation?.madaoRoutingPlanId, routing_plan_id: activation?.madaoRoutingPlanId,
routing_plan_name: activation?.madaoRoutingPlanName, routing_plan_name: activation?.madaoRoutingPlanName,
service: activation?.serviceCode, service: activation?.serviceCode,
country: activation?.countryId,
}); });
if (!nextTicket) { if (!nextTicket) {
throw new Error('MaDao 返回的下一条路由激活记录无效。'); throw new Error('MaDao 返回的下一条路由激活记录无效。');
@@ -399,15 +589,62 @@
}; };
} }
async function finishActivation(state = {}, activation, deps = {}) {
return releaseActivation(state, activation, 'finish', deps);
}
async function cancelActivation(state = {}, activation, deps = {}) {
return releaseActivation(state, activation, 'cancel', deps);
}
async function banActivation(state = {}, activation, deps = {}) {
return releaseActivation(state, activation, 'ban', deps);
}
async function reuseActivation(_state = {}, activation) {
return activation && typeof activation === 'object' ? { ...activation } : activation;
}
async function requestAdditionalSms() {
return '';
}
function resolveCountryCandidates() {
return [];
}
function createProvider(deps = {}) { function createProvider(deps = {}) {
const capabilities = Object.freeze({
supportsReusableActivation: false,
supportsAutomaticFreeReuse: false,
supportsFreeReusePreservation: false,
supportsPageResend: false,
supportsPageResendProbe: false,
supportsRouteReplace: true,
requiresCountrySelection: false,
});
return { return {
id: PROVIDER_ID, id: PROVIDER_ID,
label: 'MaDao', label: 'MaDao',
capabilities,
defaultProduct: DEFAULT_SERVICE, defaultProduct: DEFAULT_SERVICE,
normalizeCountryId: normalizeCountry,
normalizeCountryLabel,
normalizeCountryKey,
normalizeActivation,
resolveCountryLabel,
resolveActivationCountry,
getActivationCountryKey,
getActivationPrice,
requestActivation: (state, options = {}, runtimeDeps = {}) => acquireActivation(state, options, {
...deps,
...runtimeDeps,
}),
acquireActivation: (state, options = {}, runtimeDeps = {}) => acquireActivation(state, options, { acquireActivation: (state, options = {}, runtimeDeps = {}) => acquireActivation(state, options, {
...deps, ...deps,
...runtimeDeps, ...runtimeDeps,
}), }),
reuseActivation,
pollActivation: (state, activation, runtimeDeps = {}) => pollActivation(state, activation, { pollActivation: (state, activation, runtimeDeps = {}) => pollActivation(state, activation, {
...deps, ...deps,
...runtimeDeps, ...runtimeDeps,
@@ -420,10 +657,32 @@
...deps, ...deps,
...runtimeDeps, ...runtimeDeps,
}), }),
finishActivation: (state, activation, runtimeDeps = {}) => finishActivation(state, activation, {
...deps,
...runtimeDeps,
}),
cancelActivation: (state, activation, runtimeDeps = {}) => cancelActivation(state, activation, {
...deps,
...runtimeDeps,
}),
banActivation: (state, activation, runtimeDeps = {}) => banActivation(state, activation, {
...deps,
...runtimeDeps,
}),
requestAdditionalSms,
rotateActivation: (state, activation, options = {}, runtimeDeps = {}) => rotateActivation(state, activation, options, { rotateActivation: (state, activation, options = {}, runtimeDeps = {}) => rotateActivation(state, activation, options, {
...deps, ...deps,
...runtimeDeps, ...runtimeDeps,
}), }),
prepareActivationForReuse: async () => ({
ok: false,
reason: 'prepare_unsupported',
message: 'MaDao 不支持自动白嫖复用准备。',
}),
canPersistReusableActivation: () => false,
canPreserveActivationForFreeReuse: () => false,
shouldUsePageResend: () => false,
shouldProbePageResend: () => false,
replaceRoutingActivation: (state, activation, options = {}, runtimeDeps = {}) => replaceRoutingActivation(state, activation, options, { replaceRoutingActivation: (state, activation, options = {}, runtimeDeps = {}) => replaceRoutingActivation(state, activation, options, {
...deps, ...deps,
...runtimeDeps, ...runtimeDeps,
@@ -432,7 +691,9 @@
extractCodeFromPollPayload, extractCodeFromPollPayload,
mapAcquirePath, mapAcquirePath,
mapTicketStatus, mapTicketStatus,
normalizeActivation,
normalizeActivationFromAcquire, normalizeActivationFromAcquire,
resolveCountryCandidates,
resolveConfig: (state = {}, runtimeDeps = {}) => resolveConfig(state, { resolveConfig: (state = {}, runtimeDeps = {}) => resolveConfig(state, {
...deps, ...deps,
...runtimeDeps, ...runtimeDeps,
@@ -451,12 +712,23 @@
extractCodeFromPollPayload, extractCodeFromPollPayload,
mapAcquirePath, mapAcquirePath,
mapTicketStatus, mapTicketStatus,
normalizeActivation,
normalizeActivationFromAcquire, normalizeActivationFromAcquire,
normalizeCountry,
normalizeCountryKey,
normalizeCountryLabel,
normalizeOperator,
resolveActivationCountry,
pollActivation, pollActivation,
pollActivationCode, pollActivationCode,
releaseActivation, releaseActivation,
finishActivation,
cancelActivation,
banActivation,
replaceRoutingActivation, replaceRoutingActivation,
requestAdditionalSms,
resolveConfig, resolveConfig,
resolveCountryCandidates,
rotateActivation, rotateActivation,
}; };
}); });
File diff suppressed because it is too large Load Diff
+32 -34
View File
@@ -1553,46 +1553,43 @@
<span class="data-label">接入模式</span> <span class="data-label">接入模式</span>
<select id="select-madao-mode" class="data-select mono"> <select id="select-madao-mode" class="data-select mono">
<option value="routing_plan">路由计划</option> <option value="routing_plan">路由计划</option>
<option value="direct">直连参数</option> <option value="direct">直连模式</option>
</select> </select>
</div> </div>
<div class="data-row" id="row-madao-routing-plan-id" style="display:none;"> <div class="data-row" id="row-madao-routing-plan-id" style="display:none;">
<span class="data-label">路由计划</span> <span class="data-label">路由计划</span>
<input type="text" id="input-madao-routing-plan-id" class="data-input mono" <div class="data-inline data-value-actions">
placeholder="routing_plan_id" /> <select id="select-madao-routing-plan-id" class="data-select mono">
</div> <option value="">请先刷新路由计划</option>
<div class="data-row" id="row-madao-provider-id" style="display:none;"> </select>
<span class="data-label">直连平台</span> <button id="btn-madao-refresh-routing-plans" class="btn btn-ghost btn-xs data-inline-btn" type="button">刷新</button>
<input type="text" id="input-madao-provider-id" class="data-input mono"
placeholder="auto" />
</div>
<div class="data-row" id="row-madao-country" style="display:none;">
<span class="data-label">直连国家</span>
<input type="text" id="input-madao-country" class="data-input mono"
placeholder="local / TH / any" />
</div>
<div class="data-row" id="row-madao-auto-pick-country" style="display:none;">
<span class="data-label">自动选国家</span>
<div class="data-inline">
<label class="toggle-switch" for="input-madao-auto-pick-country">
<input type="checkbox" id="input-madao-auto-pick-country" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
<span class="data-value">让 MaDao 根据直连平台自动选择国家</span>
</div> </div>
</div> </div>
<div class="data-row" id="row-madao-reuse-phone" style="display:none;"> <div class="data-row" id="row-madao-provider-id" style="display:none;">
<span class="data-label">MaDao 复用</span> <span class="data-label">MaDao 服务商</span>
<div class="data-inline"> <div class="data-inline data-value-actions">
<label class="toggle-switch" for="input-madao-reuse-phone"> <select id="select-madao-provider-id" class="data-select mono">
<input type="checkbox" id="input-madao-reuse-phone" /> <option value="">请先刷新服务商</option>
<span class="toggle-switch-track" aria-hidden="true"> </select>
<span class="toggle-switch-thumb"></span> <button id="btn-madao-refresh-providers" class="btn btn-ghost btn-xs data-inline-btn" type="button">刷新</button>
</span> </div>
</label> </div>
<span class="data-value">允许 MaDao 复用符合条件的号码</span> <div class="data-row" id="row-madao-country" style="display:none;">
<span class="data-label">MaDao 国家</span>
<div class="data-inline data-value-actions">
<select id="select-madao-country" class="data-select mono">
<option value="">请先选择服务商</option>
</select>
<button id="btn-madao-refresh-countries" class="btn btn-ghost btn-xs data-inline-btn" type="button">刷新</button>
</div>
</div>
<div class="data-row" id="row-madao-operator" style="display:none;">
<span class="data-label">MaDao 线路</span>
<div class="data-inline data-value-actions">
<select id="select-madao-operator" class="data-select mono">
<option value="">任意线路</option>
</select>
<button id="btn-madao-refresh-operators" class="btn btn-ghost btn-xs data-inline-btn" type="button">刷新</button>
</div> </div>
</div> </div>
<div class="data-row" id="row-madao-price-range" style="display:none;"> <div class="data-row" id="row-madao-price-range" style="display:none;">
@@ -1896,6 +1893,7 @@
<script src="../gopay-utils.js"></script> <script src="../gopay-utils.js"></script>
<script src="../phone-sms/providers/hero-sms.js"></script> <script src="../phone-sms/providers/hero-sms.js"></script>
<script src="../phone-sms/providers/five-sim.js"></script> <script src="../phone-sms/providers/five-sim.js"></script>
<script src="../phone-sms/providers/nexsms.js"></script>
<script src="../phone-sms/providers/madao.js"></script> <script src="../phone-sms/providers/madao.js"></script>
<script src="../phone-sms/providers/registry.js"></script> <script src="../phone-sms/providers/registry.js"></script>
<script src="../icloud-utils.js"></script> <script src="../icloud-utils.js"></script>
+698 -61
View File
@@ -460,6 +460,7 @@ const rowMaDaoMode = document.getElementById('row-madao-mode');
const rowMaDaoRoutingPlanId = document.getElementById('row-madao-routing-plan-id'); const rowMaDaoRoutingPlanId = document.getElementById('row-madao-routing-plan-id');
const rowMaDaoProviderId = document.getElementById('row-madao-provider-id'); const rowMaDaoProviderId = document.getElementById('row-madao-provider-id');
const rowMaDaoCountry = document.getElementById('row-madao-country'); const rowMaDaoCountry = document.getElementById('row-madao-country');
const rowMaDaoOperator = document.getElementById('row-madao-operator');
const rowMaDaoAutoPickCountry = document.getElementById('row-madao-auto-pick-country'); const rowMaDaoAutoPickCountry = document.getElementById('row-madao-auto-pick-country');
const rowMaDaoReusePhone = document.getElementById('row-madao-reuse-phone'); const rowMaDaoReusePhone = document.getElementById('row-madao-reuse-phone');
const rowMaDaoPriceRange = document.getElementById('row-madao-price-range'); const rowMaDaoPriceRange = document.getElementById('row-madao-price-range');
@@ -494,9 +495,14 @@ const inputMaDaoBaseUrl = document.getElementById('input-madao-base-url');
const inputMaDaoHttpSecret = document.getElementById('input-madao-http-secret'); const inputMaDaoHttpSecret = document.getElementById('input-madao-http-secret');
const btnToggleMaDaoHttpSecret = document.getElementById('btn-toggle-madao-http-secret'); const btnToggleMaDaoHttpSecret = document.getElementById('btn-toggle-madao-http-secret');
const selectMaDaoMode = document.getElementById('select-madao-mode'); const selectMaDaoMode = document.getElementById('select-madao-mode');
const inputMaDaoRoutingPlanId = document.getElementById('input-madao-routing-plan-id'); const selectMaDaoRoutingPlanId = document.getElementById('select-madao-routing-plan-id');
const inputMaDaoProviderId = document.getElementById('input-madao-provider-id'); const btnMaDaoRefreshRoutingPlans = document.getElementById('btn-madao-refresh-routing-plans');
const inputMaDaoCountry = document.getElementById('input-madao-country'); const selectMaDaoProviderId = document.getElementById('select-madao-provider-id');
const btnMaDaoRefreshProviders = document.getElementById('btn-madao-refresh-providers');
const selectMaDaoCountry = document.getElementById('select-madao-country');
const btnMaDaoRefreshCountries = document.getElementById('btn-madao-refresh-countries');
const selectMaDaoOperator = document.getElementById('select-madao-operator');
const btnMaDaoRefreshOperators = document.getElementById('btn-madao-refresh-operators');
const inputMaDaoAutoPickCountry = document.getElementById('input-madao-auto-pick-country'); const inputMaDaoAutoPickCountry = document.getElementById('input-madao-auto-pick-country');
const inputMaDaoReusePhone = document.getElementById('input-madao-reuse-phone'); const inputMaDaoReusePhone = document.getElementById('input-madao-reuse-phone');
const inputMaDaoMinPrice = document.getElementById('input-madao-min-price'); const inputMaDaoMinPrice = document.getElementById('input-madao-min-price');
@@ -689,6 +695,10 @@ const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
const MADAO_MODE_ROUTING_PLAN = 'routing_plan'; const MADAO_MODE_ROUTING_PLAN = 'routing_plan';
const MADAO_MODE_DIRECT = 'direct'; const MADAO_MODE_DIRECT = 'direct';
const DEFAULT_MADAO_MODE = MADAO_MODE_ROUTING_PLAN; const DEFAULT_MADAO_MODE = MADAO_MODE_ROUTING_PLAN;
let maDaoRoutingPlanOptions = [];
let maDaoProviderOptions = [];
let maDaoCountryOptions = [];
let maDaoOperatorOptions = [];
const PHONE_SMS_PROVIDER_UI_DESCRIPTORS = Object.freeze({ const PHONE_SMS_PROVIDER_UI_DESCRIPTORS = Object.freeze({
[PHONE_SMS_PROVIDER_HERO]: Object.freeze({ [PHONE_SMS_PROVIDER_HERO]: Object.freeze({
rowKeys: Object.freeze([ rowKeys: Object.freeze([
@@ -734,8 +744,7 @@ const PHONE_SMS_PROVIDER_UI_DESCRIPTORS = Object.freeze({
directRowKeys: Object.freeze([ directRowKeys: Object.freeze([
'rowMaDaoProviderId', 'rowMaDaoProviderId',
'rowMaDaoCountry', 'rowMaDaoCountry',
'rowMaDaoAutoPickCountry', 'rowMaDaoOperator',
'rowMaDaoReusePhone',
'rowMaDaoPriceRange', 'rowMaDaoPriceRange',
]), ]),
}), }),
@@ -765,6 +774,7 @@ function getPhoneSmsProviderUiRowMap() {
rowMaDaoRoutingPlanId, rowMaDaoRoutingPlanId,
rowMaDaoProviderId, rowMaDaoProviderId,
rowMaDaoCountry, rowMaDaoCountry,
rowMaDaoOperator,
rowMaDaoAutoPickCountry, rowMaDaoAutoPickCountry,
rowMaDaoReusePhone, rowMaDaoReusePhone,
rowMaDaoPriceRange, rowMaDaoPriceRange,
@@ -4455,6 +4465,9 @@ function collectSettingsPayload() {
const normalizeMaDaoProviderIdSafe = typeof normalizeMaDaoProviderIdValue === 'function' const normalizeMaDaoProviderIdSafe = typeof normalizeMaDaoProviderIdValue === 'function'
? normalizeMaDaoProviderIdValue ? normalizeMaDaoProviderIdValue
: ((value = '') => String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '')); : ((value = '') => String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''));
const normalizeMaDaoOperatorSafe = typeof normalizeMaDaoOperatorValue === 'function'
? normalizeMaDaoOperatorValue
: ((value = '') => String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''));
const normalizeMaDaoCountrySafe = typeof normalizeMaDaoCountry === 'function' const normalizeMaDaoCountrySafe = typeof normalizeMaDaoCountry === 'function'
? normalizeMaDaoCountry ? normalizeMaDaoCountry
: ((value = '') => { : ((value = '') => {
@@ -4638,15 +4651,22 @@ function collectSettingsPayload() {
const maDaoModeValue = typeof selectMaDaoMode !== 'undefined' && selectMaDaoMode const maDaoModeValue = typeof selectMaDaoMode !== 'undefined' && selectMaDaoMode
? normalizeMaDaoModeSafe(selectMaDaoMode.value || latestState?.madaoMode) ? normalizeMaDaoModeSafe(selectMaDaoMode.value || latestState?.madaoMode)
: normalizeMaDaoModeSafe(latestState?.madaoMode); : normalizeMaDaoModeSafe(latestState?.madaoMode);
const maDaoRoutingPlanIdValue = typeof inputMaDaoRoutingPlanId !== 'undefined' && inputMaDaoRoutingPlanId const maDaoRoutingPlanIdValue = typeof selectMaDaoRoutingPlanId !== 'undefined' && selectMaDaoRoutingPlanId
? normalizeMaDaoIdentifierSafe(inputMaDaoRoutingPlanId.value || '') ? normalizeMaDaoIdentifierSafe(selectMaDaoRoutingPlanId.value || '')
: normalizeMaDaoIdentifierSafe(latestState?.madaoRoutingPlanId || ''); : normalizeMaDaoIdentifierSafe(latestState?.madaoRoutingPlanId || '');
const maDaoProviderIdValue = typeof inputMaDaoProviderId !== 'undefined' && inputMaDaoProviderId const maDaoDirectModeValue = typeof MADAO_MODE_DIRECT !== 'undefined'
? normalizeMaDaoProviderIdSafe(inputMaDaoProviderId.value || '') ? MADAO_MODE_DIRECT
: 'direct';
const shouldReadMaDaoDirectControls = maDaoModeValue === maDaoDirectModeValue;
const maDaoProviderIdValue = shouldReadMaDaoDirectControls && typeof selectMaDaoProviderId !== 'undefined' && selectMaDaoProviderId
? normalizeMaDaoProviderIdSafe(selectMaDaoProviderId.value || '')
: normalizeMaDaoProviderIdSafe(latestState?.madaoProviderId || ''); : normalizeMaDaoProviderIdSafe(latestState?.madaoProviderId || '');
const maDaoCountryValue = typeof inputMaDaoCountry !== 'undefined' && inputMaDaoCountry const maDaoCountryValue = shouldReadMaDaoDirectControls && typeof selectMaDaoCountry !== 'undefined' && selectMaDaoCountry
? normalizeMaDaoCountrySafe(inputMaDaoCountry.value || '') ? normalizeMaDaoCountrySafe(selectMaDaoCountry.value || '')
: normalizeMaDaoCountrySafe(latestState?.madaoCountry || ''); : normalizeMaDaoCountrySafe(latestState?.madaoCountry || '');
const maDaoOperatorValue = shouldReadMaDaoDirectControls && typeof selectMaDaoOperator !== 'undefined' && selectMaDaoOperator
? normalizeMaDaoOperatorSafe(selectMaDaoOperator.value || '')
: normalizeMaDaoOperatorSafe(latestState?.madaoOperator || '');
const maDaoAutoPickCountryValue = typeof inputMaDaoAutoPickCountry !== 'undefined' && inputMaDaoAutoPickCountry const maDaoAutoPickCountryValue = typeof inputMaDaoAutoPickCountry !== 'undefined' && inputMaDaoAutoPickCountry
? Boolean(inputMaDaoAutoPickCountry.checked) ? Boolean(inputMaDaoAutoPickCountry.checked)
: (latestState?.madaoAutoPickCountry !== undefined ? Boolean(latestState.madaoAutoPickCountry) : true); : (latestState?.madaoAutoPickCountry !== undefined ? Boolean(latestState.madaoAutoPickCountry) : true);
@@ -5273,6 +5293,7 @@ function collectSettingsPayload() {
madaoRoutingPlanId: maDaoRoutingPlanIdValue, madaoRoutingPlanId: maDaoRoutingPlanIdValue,
madaoProviderId: maDaoProviderIdValue, madaoProviderId: maDaoProviderIdValue,
madaoCountry: maDaoCountryValue, madaoCountry: maDaoCountryValue,
madaoOperator: maDaoOperatorValue,
madaoAutoPickCountry: maDaoAutoPickCountryValue, madaoAutoPickCountry: maDaoAutoPickCountryValue,
madaoReusePhone: maDaoReusePhoneValue, madaoReusePhone: maDaoReusePhoneValue,
madaoMinPrice: maDaoMinPriceValue, madaoMinPrice: maDaoMinPriceValue,
@@ -5675,10 +5696,23 @@ function normalizeMaDaoIdentifierValue(value = '') {
return String(value || '').trim(); return String(value || '').trim();
} }
function normalizeMaDaoRoutingPlanIdValue(value = '') {
return normalizeMaDaoIdentifierValue(value);
}
function normalizeMaDaoProviderIdValue(value = '') { function normalizeMaDaoProviderIdValue(value = '') {
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
} }
function normalizeMaDaoOperatorValue(value = '') {
const rawValue = String(value || '').trim();
const compactValue = rawValue.toLowerCase().replace(/[^a-z0-9]+/g, '');
if (!rawValue || compactValue === 'any' || compactValue === 'anyoperator') {
return '';
}
return rawValue.toLowerCase().replace(/[^a-z0-9_-]+/g, '');
}
function normalizeMaDaoCountry(value = '') { function normalizeMaDaoCountry(value = '') {
const trimmed = String(value || '').trim(); const trimmed = String(value || '').trim();
if (!trimmed) { if (!trimmed) {
@@ -5694,6 +5728,22 @@ function normalizeMaDaoCountry(value = '') {
return lowered.replace(/[^a-z0-9_-]+/g, ''); return lowered.replace(/[^a-z0-9_-]+/g, '');
} }
function formatMaDaoCountryDisplayLabel(value = '', label = '', labelZh = '') {
const country = normalizeMaDaoCountry(value);
const sourceLabelZh = String(labelZh || '').trim();
const sourceLabel = String(label || '').trim();
if (!country) {
return sourceLabelZh || sourceLabel;
}
if (country === 'local') {
return sourceLabelZh || '本地';
}
if (country === 'any') {
return sourceLabelZh || '任意国家';
}
return sourceLabelZh || sourceLabel || country;
}
function normalizeMaDaoPriceValue(value = '') { function normalizeMaDaoPriceValue(value = '') {
const rawValue = String(value ?? '').trim(); const rawValue = String(value ?? '').trim();
if (!rawValue) { if (!rawValue) {
@@ -5706,6 +5756,493 @@ function normalizeMaDaoPriceValue(value = '') {
return String(Math.round(numeric * 10000) / 10000); return String(Math.round(numeric * 10000) / 10000);
} }
function createSelectOptionElement(value = '', label = '', title = '') {
const option = typeof document !== 'undefined' && typeof document.createElement === 'function'
? document.createElement('option')
: { value: '', textContent: '', title: '', selected: false };
option.value = String(value || '');
option.textContent = String(label || value || '').trim();
const normalizedTitle = String(title || '').trim();
if (normalizedTitle) {
option.title = normalizedTitle;
}
return option;
}
function setSelectOptions(selectEl, items = [], options = {}) {
if (!selectEl) {
return;
}
const placeholder = String(options.placeholder || '').trim();
const includeEmpty = options.includeEmpty !== false;
const emptyValue = options.emptyValue !== undefined ? String(options.emptyValue) : '';
const selectedValue = String(options.value !== undefined ? options.value : selectEl.value || '').trim();
const optionItems = [];
if (includeEmpty) {
optionItems.push({
value: emptyValue,
label: placeholder || '请选择',
});
}
(Array.isArray(items) ? items : []).forEach((item) => {
const value = String(item?.value ?? '').trim();
if (!value) {
return;
}
optionItems.push({
value,
label: String(item?.label || value).trim(),
hint: String(item?.hint || '').trim(),
});
});
if (typeof selectEl.replaceChildren === 'function') {
selectEl.replaceChildren(...optionItems.map((item) => createSelectOptionElement(item.value, item.label, item.hint)));
} else if (typeof selectEl.appendChild === 'function') {
selectEl.innerHTML = '';
optionItems.forEach((item) => {
selectEl.appendChild(createSelectOptionElement(item.value, item.label, item.hint));
});
} else {
selectEl.options = optionItems.map((item) => createSelectOptionElement(item.value, item.label, item.hint));
}
const values = new Set(optionItems.map((item) => item.value));
const nextValue = values.has(selectedValue)
? selectedValue
: (includeEmpty ? emptyValue : (optionItems[0]?.value || ''));
selectEl.value = nextValue;
Array.from(selectEl.options || []).forEach((option) => {
option.selected = String(option.value || '') === nextValue;
});
}
function buildMaDaoRoutingPlanOptions(items = [], selectedValue = '') {
const selectedPlanId = normalizeMaDaoRoutingPlanIdValue(selectedValue);
const seen = new Set();
const normalizedItems = [];
(Array.isArray(items) ? items : []).forEach((item) => {
const value = normalizeMaDaoRoutingPlanIdValue(
item?.value
|| item?.id
|| item?.routing_plan_id
|| item?.routingPlanId
|| ''
);
if (!value || seen.has(value)) {
return;
}
seen.add(value);
normalizedItems.push({
value,
label: String(item?.label || item?.name || value).trim() || value,
hint: String(item?.hint || item?.description || '').trim(),
service: String(item?.service || '').trim().toLowerCase(),
});
});
if (selectedPlanId && !seen.has(selectedPlanId)) {
normalizedItems.unshift({
value: selectedPlanId,
label: selectedPlanId,
hint: '已保存的路由计划',
});
}
return normalizedItems;
}
function setMaDaoRoutingPlanSelectOptions(selectedValue = latestState?.madaoRoutingPlanId || '') {
const normalizedSelected = normalizeMaDaoRoutingPlanIdValue(selectedValue);
const options = buildMaDaoRoutingPlanOptions(maDaoRoutingPlanOptions, normalizedSelected);
setSelectOptions(selectMaDaoRoutingPlanId, options, {
placeholder: '请先刷新路由计划',
value: normalizedSelected,
});
}
function normalizeMaDaoOptionListItems(items = [], selectedValue = '', normalizeValue = normalizeMaDaoIdentifierValue, selectedHint = '已保存的选项') {
const normalizedSelected = normalizeValue(selectedValue);
const seen = new Set();
const normalizedItems = [];
(Array.isArray(items) ? items : []).forEach((item) => {
const value = normalizeValue(
item?.value
|| item?.id
|| item?.provider
|| item?.provider_value
|| item?.providerValue
|| item?.country
|| item?.operator
|| ''
);
if (!value || seen.has(value)) {
return;
}
seen.add(value);
normalizedItems.push({
value,
label: String(item?.label || item?.name || item?.display_name || item?.displayName || value).trim() || value,
labelZh: String(item?.label_zh || item?.labelZh || item?.display_name_zh || item?.displayNameZh || '').trim(),
hint: String(item?.hint || item?.description || item?.provider_value || item?.providerValue || '').trim(),
enabled: item?.enabled !== false,
});
});
if (normalizedSelected && !seen.has(normalizedSelected)) {
normalizedItems.unshift({
value: normalizedSelected,
label: normalizedSelected,
hint: selectedHint,
enabled: true,
});
}
return normalizedItems.filter((item) => item.enabled !== false);
}
function resolveMaDaoOptionSelectedValue(items = [], selectedValue = '', normalizeValue = normalizeMaDaoIdentifierValue) {
const normalizedSelected = normalizeValue(selectedValue);
if (!normalizedSelected) {
return '';
}
const sourceItems = Array.isArray(items) ? items : [];
for (const item of sourceItems) {
const value = normalizeValue(
item?.value
|| item?.id
|| item?.provider
|| item?.provider_value
|| item?.providerValue
|| item?.country
|| item?.operator
|| ''
);
if (!value) {
continue;
}
const aliases = [
item?.value,
item?.id,
item?.provider,
item?.provider_value,
item?.providerValue,
item?.country,
item?.operator,
item?.label,
item?.label_zh,
item?.labelZh,
item?.name,
item?.display_name,
item?.displayName,
];
if (aliases.some((alias) => normalizeValue(alias || '') === normalizedSelected)) {
return value;
}
}
return normalizedSelected;
}
function setMaDaoProviderSelectOptions(selectedValue = latestState?.madaoProviderId || '') {
const normalizedSelected = resolveMaDaoOptionSelectedValue(
maDaoProviderOptions,
selectedValue,
normalizeMaDaoProviderIdValue
);
const options = normalizeMaDaoOptionListItems(
maDaoProviderOptions,
normalizedSelected,
normalizeMaDaoProviderIdValue,
'已保存的服务商'
);
setSelectOptions(selectMaDaoProviderId, options, {
placeholder: '请先刷新服务商',
value: normalizedSelected,
});
}
function setMaDaoCountrySelectOptions(selectedValue = latestState?.madaoCountry || '') {
const normalizedSelected = resolveMaDaoOptionSelectedValue(
maDaoCountryOptions,
selectedValue,
normalizeMaDaoCountry
);
const options = normalizeMaDaoOptionListItems(
maDaoCountryOptions,
normalizedSelected,
normalizeMaDaoCountry,
'已保存的国家'
).map((item) => ({
...item,
label: formatMaDaoCountryDisplayLabel(item.value, item.label, item.labelZh),
}));
setSelectOptions(selectMaDaoCountry, options, {
placeholder: '请先选择服务商',
value: normalizedSelected,
});
}
function setMaDaoOperatorSelectOptions(selectedValue = latestState?.madaoOperator || '') {
const normalizedSelected = resolveMaDaoOptionSelectedValue(
maDaoOperatorOptions,
selectedValue,
normalizeMaDaoOperatorValue
);
const options = normalizeMaDaoOptionListItems(
maDaoOperatorOptions,
normalizedSelected,
normalizeMaDaoOperatorValue,
'已保存的线路'
);
setSelectOptions(selectMaDaoOperator, options, {
placeholder: '任意线路',
value: normalizedSelected,
});
}
function buildMaDaoRequestUrl(path = '', baseUrl = '') {
const normalizedBaseUrl = normalizeMaDaoBaseUrlValue(baseUrl || latestState?.madaoBaseUrl || DEFAULT_MADAO_BASE_URL);
return new URL(String(path || '').replace(/^\/+/, ''), `${normalizedBaseUrl.replace(/\/+$/, '')}/`).toString();
}
function buildMaDaoRequestHeaders(httpSecret = '') {
const headers = { Accept: 'application/json' };
const normalizedSecret = String(httpSecret || '').trim();
if (normalizedSecret) {
headers.Authorization = `Bearer ${normalizedSecret}`;
}
return headers;
}
async function fetchMaDaoJson(path = '', options = {}) {
const baseUrl = typeof inputMaDaoBaseUrl !== 'undefined' && inputMaDaoBaseUrl
? normalizeMaDaoBaseUrlValue(inputMaDaoBaseUrl.value || latestState?.madaoBaseUrl)
: normalizeMaDaoBaseUrlValue(latestState?.madaoBaseUrl);
const httpSecret = typeof inputMaDaoHttpSecret !== 'undefined' && inputMaDaoHttpSecret
? String(inputMaDaoHttpSecret.value || '')
: String(latestState?.madaoHttpSecret || '');
const response = await fetch(buildMaDaoRequestUrl(path, baseUrl), {
method: options.method || 'GET',
headers: {
...buildMaDaoRequestHeaders(httpSecret),
...(options.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
},
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
cache: 'no-store',
});
const text = await response.text();
let payload = null;
if (text) {
try {
payload = JSON.parse(text);
} catch {
payload = { message: text };
}
}
if (!response.ok) {
const message = String(payload?.message || payload?.error || response.statusText || `HTTP ${response.status}`).trim();
throw new Error(message || 'MaDao 请求失败');
}
return payload;
}
function getMaDaoRoutingPlansFromPayload(payload = {}) {
if (Array.isArray(payload)) {
return payload;
}
if (Array.isArray(payload?.plans)) {
return payload.plans;
}
if (Array.isArray(payload?.data?.plans)) {
return payload.data.plans;
}
if (Array.isArray(payload?.data)) {
return payload.data;
}
if (Array.isArray(payload?.items)) {
return payload.items;
}
return [];
}
function getMaDaoProvidersFromPayload(payload = {}) {
if (Array.isArray(payload)) {
return payload;
}
if (Array.isArray(payload?.providers)) {
return payload.providers;
}
if (Array.isArray(payload?.data?.providers)) {
return payload.data.providers;
}
if (Array.isArray(payload?.data)) {
return payload.data;
}
if (Array.isArray(payload?.items)) {
return payload.items;
}
return [];
}
function getMaDaoOptionItemsFromPayload(payload = {}) {
if (Array.isArray(payload)) {
return payload;
}
if (Array.isArray(payload?.items)) {
return payload.items;
}
if (Array.isArray(payload?.data?.items)) {
return payload.data.items;
}
if (Array.isArray(payload?.data)) {
return payload.data;
}
return [];
}
function getSelectedMaDaoProviderId() {
return normalizeMaDaoProviderIdValue(
typeof selectMaDaoProviderId !== 'undefined' && selectMaDaoProviderId
? selectMaDaoProviderId.value
: latestState?.madaoProviderId
);
}
function getSelectedMaDaoCountry() {
return normalizeMaDaoCountry(
typeof selectMaDaoCountry !== 'undefined' && selectMaDaoCountry
? selectMaDaoCountry.value
: latestState?.madaoCountry
);
}
async function loadMaDaoRoutingPlans(options = {}) {
const selectedValue = normalizeMaDaoRoutingPlanIdValue(
typeof selectMaDaoRoutingPlanId !== 'undefined' && selectMaDaoRoutingPlanId
? selectMaDaoRoutingPlanId.value
: latestState?.madaoRoutingPlanId
);
const payload = await fetchMaDaoJson('/api/routing-plans');
const plans = getMaDaoRoutingPlansFromPayload(payload);
maDaoRoutingPlanOptions = plans
.filter((plan) => plan?.enabled !== false)
.filter((plan) => {
const service = String(plan?.service || '').trim().toLowerCase();
return !service || service === 'openai';
})
.map((plan) => ({
value: normalizeMaDaoRoutingPlanIdValue(plan?.id || plan?.routing_plan_id || plan?.routingPlanId || plan?.value || ''),
label: String(plan?.name || plan?.label || plan?.id || plan?.routing_plan_id || '').trim(),
hint: String(plan?.description || plan?.hint || '').trim(),
service: String(plan?.service || '').trim().toLowerCase(),
}))
.filter((plan) => plan.value);
setMaDaoRoutingPlanSelectOptions(selectedValue);
updateHeroSmsPlatformDisplay();
if (!options.silent && typeof showToast === 'function') {
showToast('已刷新 MaDao 路由计划。', 'info', 1600);
}
return maDaoRoutingPlanOptions;
}
async function loadMaDaoProviders(options = {}) {
const selectedValue = normalizeMaDaoProviderIdValue(
(typeof selectMaDaoProviderId !== 'undefined' && selectMaDaoProviderId
? selectMaDaoProviderId.value
: '')
|| latestState?.madaoProviderId
);
const payload = await fetchMaDaoJson('/api/providers');
maDaoProviderOptions = getMaDaoProvidersFromPayload(payload)
.filter((provider) => provider?.enabled !== false)
.map((provider) => ({
value: normalizeMaDaoProviderIdValue(provider?.id || provider?.value || provider?.provider || ''),
label: String(provider?.name || provider?.label || provider?.id || provider?.value || '').trim(),
hint: String(provider?.description || provider?.hint || provider?.protocol_label || provider?.protocolLabel || provider?.kind || '').trim(),
enabled: provider?.enabled !== false,
}))
.filter((provider) => provider.value);
setMaDaoProviderSelectOptions(selectedValue);
if (!options.skipChildren && getSelectedMaDaoProviderId()) {
await loadMaDaoCountries({ silent: true }).catch(() => {
setMaDaoCountrySelectOptions(latestState?.madaoCountry || '');
setMaDaoOperatorSelectOptions(latestState?.madaoOperator || '');
});
}
updateHeroSmsPlatformDisplay();
if (!options.silent && typeof showToast === 'function') {
showToast('已刷新 MaDao 服务商。', 'info', 1600);
}
return maDaoProviderOptions;
}
async function loadMaDaoCountries(options = {}) {
const providerId = getSelectedMaDaoProviderId();
const selectedValue = normalizeMaDaoCountry(
(typeof selectMaDaoCountry !== 'undefined' && selectMaDaoCountry
? selectMaDaoCountry.value
: '')
|| latestState?.madaoCountry
);
if (!providerId) {
maDaoCountryOptions = [];
maDaoOperatorOptions = [];
setMaDaoCountrySelectOptions(selectedValue);
setMaDaoOperatorSelectOptions(latestState?.madaoOperator || '');
return maDaoCountryOptions;
}
const payload = await fetchMaDaoJson(`/api/providers/${encodeURIComponent(providerId)}/countries`);
maDaoCountryOptions = getMaDaoOptionItemsFromPayload(payload);
setMaDaoCountrySelectOptions(selectedValue);
if (!options.skipChildren) {
await loadMaDaoOperators({ silent: true }).catch(() => {
setMaDaoOperatorSelectOptions(latestState?.madaoOperator || '');
});
}
updateHeroSmsPlatformDisplay();
if (!options.silent && typeof showToast === 'function') {
showToast('已刷新 MaDao 国家。', 'info', 1600);
}
return maDaoCountryOptions;
}
async function loadMaDaoOperators(options = {}) {
const providerId = getSelectedMaDaoProviderId();
const country = getSelectedMaDaoCountry();
const selectedValue = normalizeMaDaoOperatorValue(
typeof selectMaDaoOperator !== 'undefined' && selectMaDaoOperator
? selectMaDaoOperator.value
: latestState?.madaoOperator
);
if (!providerId) {
maDaoOperatorOptions = [];
setMaDaoOperatorSelectOptions(selectedValue);
return maDaoOperatorOptions;
}
const payload = await fetchMaDaoJson(`/api/providers/${encodeURIComponent(providerId)}/operators`, {
method: 'POST',
body: country ? { country } : {},
});
maDaoOperatorOptions = getMaDaoOptionItemsFromPayload(payload);
setMaDaoOperatorSelectOptions(selectedValue);
updateHeroSmsPlatformDisplay();
if (!options.silent && typeof showToast === 'function') {
showToast('已刷新 MaDao 线路。', 'info', 1600);
}
return maDaoOperatorOptions;
}
function getSelectedMaDaoRoutingPlanLabel(value = '') {
const selectedValue = normalizeMaDaoRoutingPlanIdValue(
value
|| (typeof selectMaDaoRoutingPlanId !== 'undefined' && selectMaDaoRoutingPlanId
? selectMaDaoRoutingPlanId.value
: latestState?.madaoRoutingPlanId)
|| ''
);
if (!selectedValue) {
return 'routing plan';
}
const matched = maDaoRoutingPlanOptions.find((item) => normalizeMaDaoRoutingPlanIdValue(item?.value) === selectedValue);
return String(matched?.label || selectedValue).trim() || 'routing plan';
}
function normalizePhoneSmsProviderValue(value = '') { function normalizePhoneSmsProviderValue(value = '') {
const rootScope = typeof window !== 'undefined' ? window : globalThis; const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderId) { if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderId) {
@@ -7071,7 +7608,7 @@ function updateHeroSmsPlatformDisplay() {
: (provider === PHONE_SMS_PROVIDER_NEXSMS : (provider === PHONE_SMS_PROVIDER_NEXSMS
? (getSelectedNexSmsCountries()[0] || { id: DEFAULT_NEX_SMS_COUNTRY_ORDER[0], label: `Country #${DEFAULT_NEX_SMS_COUNTRY_ORDER[0]}` }) ? (getSelectedNexSmsCountries()[0] || { id: DEFAULT_NEX_SMS_COUNTRY_ORDER[0], label: `Country #${DEFAULT_NEX_SMS_COUNTRY_ORDER[0]}` })
: (provider === PHONE_SMS_PROVIDER_MADAO : (provider === PHONE_SMS_PROVIDER_MADAO
? { id: normalizeMaDaoCountry(inputMaDaoCountry?.value || latestState?.madaoCountry || ''), label: normalizeMaDaoModeValue(selectMaDaoMode?.value || latestState?.madaoMode) === MADAO_MODE_DIRECT ? (normalizeMaDaoCountry(inputMaDaoCountry?.value || latestState?.madaoCountry || '') || 'auto') : 'routing plan' } ? { id: '', label: normalizeMaDaoModeValue(selectMaDaoMode?.value || latestState?.madaoMode) === MADAO_MODE_DIRECT ? 'direct' : getSelectedMaDaoRoutingPlanLabel() }
: getSelectedHeroSmsCountryOption())); : getSelectedHeroSmsCountryOption()));
const countryText = selected?.label ? ` / ${selected.label}` : ''; const countryText = selected?.label ? ` / ${selected.label}` : '';
displayHeroSmsPlatform.textContent = `${getPhoneSmsProviderLabel(provider)} / OpenAI${countryText}`; displayHeroSmsPlatform.textContent = `${getPhoneSmsProviderLabel(provider)} / OpenAI${countryText}`;
@@ -11817,7 +12354,18 @@ function applySettingsState(state) {
updatePhoneSmsProviderOrderSummary(restoredPhoneSmsProviderOrder); updatePhoneSmsProviderOrderSummary(restoredPhoneSmsProviderOrder);
if (previousPhoneSmsProvider !== restoredPhoneSmsProvider) { if (previousPhoneSmsProvider !== restoredPhoneSmsProvider) {
heroSmsCountrySelectionOrder = []; heroSmsCountrySelectionOrder = [];
loadHeroSmsCountries({ silent: true }).catch(() => { }); if (restoredPhoneSmsProvider === PHONE_SMS_PROVIDER_MADAO) {
loadMaDaoRoutingPlans({ silent: true }).catch(() => {
setMaDaoRoutingPlanSelectOptions(state?.madaoRoutingPlanId || '');
});
loadMaDaoProviders({ silent: true }).catch(() => {
setMaDaoProviderSelectOptions(state?.madaoProviderId || '');
setMaDaoCountrySelectOptions(state?.madaoCountry || '');
setMaDaoOperatorSelectOptions(state?.madaoOperator || '');
});
} else {
loadHeroSmsCountries({ silent: true }).catch(() => { });
}
} }
if (inputHeroSmsApiKey) { if (inputHeroSmsApiKey) {
inputHeroSmsApiKey.value = restoredPhoneSmsProvider === PHONE_SMS_PROVIDER_FIVE_SIM inputHeroSmsApiKey.value = restoredPhoneSmsProvider === PHONE_SMS_PROVIDER_FIVE_SIM
@@ -11854,14 +12402,17 @@ function applySettingsState(state) {
if (typeof selectMaDaoMode !== 'undefined' && selectMaDaoMode) { if (typeof selectMaDaoMode !== 'undefined' && selectMaDaoMode) {
selectMaDaoMode.value = normalizeMaDaoModeValue(state?.madaoMode); selectMaDaoMode.value = normalizeMaDaoModeValue(state?.madaoMode);
} }
if (typeof inputMaDaoRoutingPlanId !== 'undefined' && inputMaDaoRoutingPlanId) { if (typeof selectMaDaoRoutingPlanId !== 'undefined' && selectMaDaoRoutingPlanId) {
inputMaDaoRoutingPlanId.value = normalizeMaDaoIdentifierValue(state?.madaoRoutingPlanId || ''); setMaDaoRoutingPlanSelectOptions(state?.madaoRoutingPlanId || '');
} }
if (typeof inputMaDaoProviderId !== 'undefined' && inputMaDaoProviderId) { if (typeof selectMaDaoProviderId !== 'undefined' && selectMaDaoProviderId) {
inputMaDaoProviderId.value = normalizeMaDaoProviderIdValue(state?.madaoProviderId || ''); setMaDaoProviderSelectOptions(state?.madaoProviderId || '');
} }
if (typeof inputMaDaoCountry !== 'undefined' && inputMaDaoCountry) { if (typeof selectMaDaoCountry !== 'undefined' && selectMaDaoCountry) {
inputMaDaoCountry.value = normalizeMaDaoCountry(state?.madaoCountry || ''); setMaDaoCountrySelectOptions(state?.madaoCountry || '');
}
if (typeof selectMaDaoOperator !== 'undefined' && selectMaDaoOperator) {
setMaDaoOperatorSelectOptions(state?.madaoOperator || '');
} }
if (typeof inputMaDaoAutoPickCountry !== 'undefined' && inputMaDaoAutoPickCountry) { if (typeof inputMaDaoAutoPickCountry !== 'undefined' && inputMaDaoAutoPickCountry) {
inputMaDaoAutoPickCountry.checked = state?.madaoAutoPickCountry !== undefined inputMaDaoAutoPickCountry.checked = state?.madaoAutoPickCountry !== undefined
@@ -17159,15 +17710,35 @@ function buildPhoneSmsProviderStatePatch(provider = getSelectedPhoneSmsProvider(
}; };
} }
if (normalizedProvider === PHONE_SMS_PROVIDER_MADAO) { if (normalizedProvider === PHONE_SMS_PROVIDER_MADAO) {
const maDaoMode = normalizeMaDaoModeValue(selectMaDaoMode?.value || latestState?.madaoMode);
const maDaoDirectModeValue = typeof MADAO_MODE_DIRECT !== 'undefined'
? MADAO_MODE_DIRECT
: 'direct';
const shouldReadMaDaoDirectControls = maDaoMode === maDaoDirectModeValue;
return { return {
madaoBaseUrl: normalizeMaDaoBaseUrlValue(inputMaDaoBaseUrl?.value || latestState?.madaoBaseUrl), madaoBaseUrl: normalizeMaDaoBaseUrlValue(inputMaDaoBaseUrl?.value || latestState?.madaoBaseUrl),
madaoHttpSecret: String(inputMaDaoHttpSecret?.value || ''), madaoHttpSecret: String(inputMaDaoHttpSecret?.value || ''),
madaoMode: normalizeMaDaoModeValue(selectMaDaoMode?.value || latestState?.madaoMode), madaoMode: maDaoMode,
madaoRoutingPlanId: normalizeMaDaoIdentifierValue(inputMaDaoRoutingPlanId?.value || ''), madaoRoutingPlanId: normalizeMaDaoRoutingPlanIdValue(
madaoProviderId: normalizeMaDaoProviderIdValue(inputMaDaoProviderId?.value || ''), typeof selectMaDaoRoutingPlanId !== 'undefined' && selectMaDaoRoutingPlanId
madaoCountry: normalizeMaDaoCountry(inputMaDaoCountry?.value || ''), ? selectMaDaoRoutingPlanId.value
madaoAutoPickCountry: Boolean(inputMaDaoAutoPickCountry?.checked), : latestState?.madaoRoutingPlanId
madaoReusePhone: Boolean(inputMaDaoReusePhone?.checked), ),
madaoProviderId: shouldReadMaDaoDirectControls
? normalizeMaDaoProviderIdValue(selectMaDaoProviderId?.value || '')
: normalizeMaDaoProviderIdValue(latestState?.madaoProviderId || ''),
madaoCountry: shouldReadMaDaoDirectControls
? normalizeMaDaoCountry(selectMaDaoCountry?.value || '')
: normalizeMaDaoCountry(latestState?.madaoCountry || ''),
madaoOperator: shouldReadMaDaoDirectControls
? normalizeMaDaoOperatorValue(selectMaDaoOperator?.value || '')
: normalizeMaDaoOperatorValue(latestState?.madaoOperator || ''),
madaoAutoPickCountry: typeof inputMaDaoAutoPickCountry !== 'undefined' && inputMaDaoAutoPickCountry
? Boolean(inputMaDaoAutoPickCountry.checked)
: (latestState?.madaoAutoPickCountry !== undefined ? Boolean(latestState.madaoAutoPickCountry) : true),
madaoReusePhone: typeof inputMaDaoReusePhone !== 'undefined' && inputMaDaoReusePhone
? Boolean(inputMaDaoReusePhone.checked)
: (latestState?.madaoReusePhone !== undefined ? Boolean(latestState.madaoReusePhone) : true),
madaoMinPrice: normalizeMaDaoPriceValue(inputMaDaoMinPrice?.value || ''), madaoMinPrice: normalizeMaDaoPriceValue(inputMaDaoMinPrice?.value || ''),
madaoMaxPrice: normalizeMaDaoPriceValue(inputMaDaoMaxPrice?.value || ''), madaoMaxPrice: normalizeMaDaoPriceValue(inputMaDaoMaxPrice?.value || ''),
}; };
@@ -17210,14 +17781,17 @@ function applyPhoneSmsProviderFieldsToInputs(provider = getSelectedPhoneSmsProvi
if (typeof selectMaDaoMode !== 'undefined' && selectMaDaoMode) { if (typeof selectMaDaoMode !== 'undefined' && selectMaDaoMode) {
selectMaDaoMode.value = normalizeMaDaoModeValue(state?.madaoMode); selectMaDaoMode.value = normalizeMaDaoModeValue(state?.madaoMode);
} }
if (typeof inputMaDaoRoutingPlanId !== 'undefined' && inputMaDaoRoutingPlanId) { if (typeof selectMaDaoRoutingPlanId !== 'undefined' && selectMaDaoRoutingPlanId) {
inputMaDaoRoutingPlanId.value = normalizeMaDaoIdentifierValue(state?.madaoRoutingPlanId || ''); setMaDaoRoutingPlanSelectOptions(state?.madaoRoutingPlanId || '');
} }
if (typeof inputMaDaoProviderId !== 'undefined' && inputMaDaoProviderId) { if (typeof selectMaDaoProviderId !== 'undefined' && selectMaDaoProviderId) {
inputMaDaoProviderId.value = normalizeMaDaoProviderIdValue(state?.madaoProviderId || ''); setMaDaoProviderSelectOptions(state?.madaoProviderId || '');
} }
if (typeof inputMaDaoCountry !== 'undefined' && inputMaDaoCountry) { if (typeof selectMaDaoCountry !== 'undefined' && selectMaDaoCountry) {
inputMaDaoCountry.value = normalizeMaDaoCountry(state?.madaoCountry || ''); setMaDaoCountrySelectOptions(state?.madaoCountry || '');
}
if (typeof selectMaDaoOperator !== 'undefined' && selectMaDaoOperator) {
setMaDaoOperatorSelectOptions(state?.madaoOperator || '');
} }
if (typeof inputMaDaoAutoPickCountry !== 'undefined' && inputMaDaoAutoPickCountry) { if (typeof inputMaDaoAutoPickCountry !== 'undefined' && inputMaDaoAutoPickCountry) {
inputMaDaoAutoPickCountry.checked = state?.madaoAutoPickCountry !== undefined inputMaDaoAutoPickCountry.checked = state?.madaoAutoPickCountry !== undefined
@@ -17296,6 +17870,15 @@ async function switchPhoneSmsProvider(nextProvider) {
applyNexSmsCountrySelection( applyNexSmsCountrySelection(
Array.isArray(latestState?.nexSmsCountryOrder) ? latestState.nexSmsCountryOrder : [] Array.isArray(latestState?.nexSmsCountryOrder) ? latestState.nexSmsCountryOrder : []
); );
} else if (normalizedNextProvider === PHONE_SMS_PROVIDER_MADAO) {
await loadMaDaoRoutingPlans({ silent: true }).catch(() => {
setMaDaoRoutingPlanSelectOptions(latestState?.madaoRoutingPlanId || '');
});
await loadMaDaoProviders({ silent: true }).catch(() => {
setMaDaoProviderSelectOptions(latestState?.madaoProviderId || '');
setMaDaoCountrySelectOptions(latestState?.madaoCountry || '');
setMaDaoOperatorSelectOptions(latestState?.madaoOperator || '');
});
} else if (normalizedNextProvider === PHONE_SMS_PROVIDER_HERO_SMS) { } else if (normalizedNextProvider === PHONE_SMS_PROVIDER_HERO_SMS) {
await loadHeroSmsCountries({ silent: true }); await loadHeroSmsCountries({ silent: true });
const restoredPrimary = { const restoredPrimary = {
@@ -17502,38 +18085,89 @@ selectMaDaoMode?.addEventListener('change', () => {
selectMaDaoMode.value = normalizeMaDaoModeValue(selectMaDaoMode.value); selectMaDaoMode.value = normalizeMaDaoModeValue(selectMaDaoMode.value);
updateHeroSmsPlatformDisplay(); updateHeroSmsPlatformDisplay();
updatePhoneVerificationSettingsUI(); updatePhoneVerificationSettingsUI();
if (selectMaDaoMode.value === MADAO_MODE_ROUTING_PLAN) {
loadMaDaoRoutingPlans({ silent: true }).catch(() => {
setMaDaoRoutingPlanSelectOptions(latestState?.madaoRoutingPlanId || '');
});
} else {
loadMaDaoProviders({ silent: true }).catch(() => {
setMaDaoProviderSelectOptions(latestState?.madaoProviderId || '');
setMaDaoCountrySelectOptions(latestState?.madaoCountry || '');
setMaDaoOperatorSelectOptions(latestState?.madaoOperator || '');
});
}
markSettingsDirty(true); markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { }); saveSettings({ silent: true }).catch(() => { });
}); });
inputMaDaoRoutingPlanId?.addEventListener('input', () => { selectMaDaoRoutingPlanId?.addEventListener('change', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
inputMaDaoRoutingPlanId?.addEventListener('blur', () => {
inputMaDaoRoutingPlanId.value = normalizeMaDaoIdentifierValue(inputMaDaoRoutingPlanId.value);
saveSettings({ silent: true }).catch(() => { });
});
inputMaDaoProviderId?.addEventListener('input', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
inputMaDaoProviderId?.addEventListener('blur', () => {
inputMaDaoProviderId.value = normalizeMaDaoProviderIdValue(inputMaDaoProviderId.value);
saveSettings({ silent: true }).catch(() => { });
});
inputMaDaoCountry?.addEventListener('input', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
inputMaDaoCountry?.addEventListener('blur', () => {
inputMaDaoCountry.value = normalizeMaDaoCountry(inputMaDaoCountry.value);
updateHeroSmsPlatformDisplay(); updateHeroSmsPlatformDisplay();
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { }); saveSettings({ silent: true }).catch(() => { });
}); });
btnMaDaoRefreshRoutingPlans?.addEventListener('click', () => {
loadMaDaoRoutingPlans().catch((error) => {
showToast(`刷新 MaDao 路由计划失败:${error?.message || error}`, 'warn', 2200);
});
});
selectMaDaoProviderId?.addEventListener('change', () => {
maDaoCountryOptions = [];
maDaoOperatorOptions = [];
setMaDaoCountrySelectOptions('');
setMaDaoOperatorSelectOptions('');
syncLatestState({
madaoProviderId: normalizeMaDaoProviderIdValue(selectMaDaoProviderId.value),
madaoCountry: '',
madaoOperator: '',
});
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
loadMaDaoCountries({ silent: true }).catch((error) => {
showToast(`刷新 MaDao 国家失败:${error?.message || error}`, 'warn', 2200);
});
});
btnMaDaoRefreshProviders?.addEventListener('click', () => {
loadMaDaoProviders().catch((error) => {
showToast(`刷新 MaDao 服务商失败:${error?.message || error}`, 'warn', 2200);
});
});
selectMaDaoCountry?.addEventListener('change', () => {
maDaoOperatorOptions = [];
setMaDaoOperatorSelectOptions('');
updateHeroSmsPlatformDisplay();
syncLatestState({
madaoCountry: normalizeMaDaoCountry(selectMaDaoCountry.value),
madaoOperator: '',
});
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
loadMaDaoOperators({ silent: true }).catch((error) => {
showToast(`刷新 MaDao 线路失败:${error?.message || error}`, 'warn', 2200);
});
});
btnMaDaoRefreshCountries?.addEventListener('click', () => {
loadMaDaoCountries().catch((error) => {
showToast(`刷新 MaDao 国家失败:${error?.message || error}`, 'warn', 2200);
});
});
selectMaDaoOperator?.addEventListener('change', () => {
updateHeroSmsPlatformDisplay();
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
btnMaDaoRefreshOperators?.addEventListener('click', () => {
loadMaDaoOperators().catch((error) => {
showToast(`刷新 MaDao 线路失败:${error?.message || error}`, 'warn', 2200);
});
});
inputMaDaoAutoPickCountry?.addEventListener('change', () => { inputMaDaoAutoPickCountry?.addEventListener('change', () => {
markSettingsDirty(true); markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { }); saveSettings({ silent: true }).catch(() => { });
@@ -18597,14 +19231,17 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
selectMaDaoMode.value = normalizeMaDaoModeValue(message.payload.madaoMode); selectMaDaoMode.value = normalizeMaDaoModeValue(message.payload.madaoMode);
updatePhoneVerificationSettingsUI(); updatePhoneVerificationSettingsUI();
} }
if (message.payload.madaoRoutingPlanId !== undefined && inputMaDaoRoutingPlanId) { if (message.payload.madaoRoutingPlanId !== undefined && selectMaDaoRoutingPlanId) {
inputMaDaoRoutingPlanId.value = normalizeMaDaoIdentifierValue(message.payload.madaoRoutingPlanId); setMaDaoRoutingPlanSelectOptions(message.payload.madaoRoutingPlanId);
} }
if (message.payload.madaoProviderId !== undefined && inputMaDaoProviderId) { if (message.payload.madaoProviderId !== undefined && selectMaDaoProviderId) {
inputMaDaoProviderId.value = normalizeMaDaoProviderIdValue(message.payload.madaoProviderId); setMaDaoProviderSelectOptions(message.payload.madaoProviderId);
} }
if (message.payload.madaoCountry !== undefined && inputMaDaoCountry) { if (message.payload.madaoCountry !== undefined && selectMaDaoCountry) {
inputMaDaoCountry.value = normalizeMaDaoCountry(message.payload.madaoCountry); setMaDaoCountrySelectOptions(message.payload.madaoCountry);
}
if (message.payload.madaoOperator !== undefined && selectMaDaoOperator) {
setMaDaoOperatorSelectOptions(message.payload.madaoOperator);
} }
if (message.payload.madaoAutoPickCountry !== undefined && inputMaDaoAutoPickCountry) { if (message.payload.madaoAutoPickCountry !== undefined && inputMaDaoAutoPickCountry) {
inputMaDaoAutoPickCountry.checked = Boolean(message.payload.madaoAutoPickCountry); inputMaDaoAutoPickCountry.checked = Boolean(message.payload.madaoAutoPickCountry);
@@ -71,6 +71,7 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeMaDaoIdentifier'), extractFunction('normalizeMaDaoIdentifier'),
extractFunction('normalizeMaDaoProviderId'), extractFunction('normalizeMaDaoProviderId'),
extractFunction('normalizeMaDaoCountry'), extractFunction('normalizeMaDaoCountry'),
extractFunction('normalizeMaDaoOperator'),
extractFunction('normalizeMaDaoPrice'), extractFunction('normalizeMaDaoPrice'),
extractFunction('normalizePhonePreferredActivation'), extractFunction('normalizePhonePreferredActivation'),
extractFunction('normalizePhoneVerificationReplacementLimit'), extractFunction('normalizePhoneVerificationReplacementLimit'),
@@ -382,6 +383,7 @@ return {
assert.equal(api.normalizePersistentSettingValue('madaoProviderId', ' Upstream A! '), 'upstreama'); assert.equal(api.normalizePersistentSettingValue('madaoProviderId', ' Upstream A! '), 'upstreama');
assert.equal(api.normalizePersistentSettingValue('madaoCountry', ' gb '), 'GB'); assert.equal(api.normalizePersistentSettingValue('madaoCountry', ' gb '), 'GB');
assert.equal(api.normalizePersistentSettingValue('madaoCountry', 'ANY'), 'any'); assert.equal(api.normalizePersistentSettingValue('madaoCountry', 'ANY'), 'any');
assert.equal(api.normalizePersistentSettingValue('madaoOperator', ' Operator A! '), 'operatora');
assert.equal(api.normalizePersistentSettingValue('madaoAutoPickCountry', 1), true); assert.equal(api.normalizePersistentSettingValue('madaoAutoPickCountry', 1), true);
assert.equal(api.normalizePersistentSettingValue('madaoReusePhone', 0), false); assert.equal(api.normalizePersistentSettingValue('madaoReusePhone', 0), false);
assert.equal(api.normalizePersistentSettingValue('madaoMinPrice', '0.123456'), '0.1235'); assert.equal(api.normalizePersistentSettingValue('madaoMinPrice', '0.123456'), '0.1235');
@@ -419,6 +419,7 @@ return {
madaoRoutingPlanId: 'rp-openai', madaoRoutingPlanId: 'rp-openai',
madaoProviderId: 'upstream-a', madaoProviderId: 'upstream-a',
madaoCountry: 'GB', madaoCountry: 'GB',
madaoOperator: 'operator-a',
madaoAutoPickCountry: false, madaoAutoPickCountry: false,
madaoReusePhone: true, madaoReusePhone: true,
madaoMinPrice: '0.12', madaoMinPrice: '0.12',
@@ -452,6 +453,7 @@ return {
assert.equal(api.getPayloadInput().madaoRoutingPlanId, 'rp-openai'); assert.equal(api.getPayloadInput().madaoRoutingPlanId, 'rp-openai');
assert.equal(api.getPayloadInput().madaoProviderId, 'upstream-a'); assert.equal(api.getPayloadInput().madaoProviderId, 'upstream-a');
assert.equal(api.getPayloadInput().madaoCountry, 'GB'); assert.equal(api.getPayloadInput().madaoCountry, 'GB');
assert.equal(api.getPayloadInput().madaoOperator, 'operator-a');
assert.equal(api.getPayloadInput().madaoAutoPickCountry, false); assert.equal(api.getPayloadInput().madaoAutoPickCountry, false);
assert.equal(api.getPayloadInput().madaoReusePhone, true); assert.equal(api.getPayloadInput().madaoReusePhone, true);
assert.equal(api.getPayloadInput().madaoMinPrice, '0.12'); assert.equal(api.getPayloadInput().madaoMinPrice, '0.12');
@@ -118,6 +118,7 @@ const PERSISTED_SETTING_DEFAULTS = {
madaoRoutingPlanId: '', madaoRoutingPlanId: '',
madaoProviderId: '', madaoProviderId: '',
madaoCountry: '', madaoCountry: '',
madaoOperator: '',
madaoAutoPickCountry: true, madaoAutoPickCountry: true,
madaoReusePhone: true, madaoReusePhone: true,
madaoMinPrice: '', madaoMinPrice: '',
@@ -219,6 +220,7 @@ function normalizeMaDaoBaseUrl(value = '') {
function normalizeMaDaoMode(value = '') { return String(value || '').trim().toLowerCase() === 'direct' ? 'direct' : DEFAULT_MADAO_MODE; } function normalizeMaDaoMode(value = '') { return String(value || '').trim().toLowerCase() === 'direct' ? 'direct' : DEFAULT_MADAO_MODE; }
function normalizeMaDaoIdentifier(value = '') { return String(value || '').trim(); } function normalizeMaDaoIdentifier(value = '') { return String(value || '').trim(); }
function normalizeMaDaoProviderId(value = '') { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); } function normalizeMaDaoProviderId(value = '') { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); }
function normalizeMaDaoOperator(value = '') { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); }
function normalizeMaDaoCountry(value = '') { function normalizeMaDaoCountry(value = '') {
const trimmed = String(value || '').trim(); const trimmed = String(value || '').trim();
if (!trimmed) return ''; if (!trimmed) return '';
@@ -306,6 +308,7 @@ test('buildPersistentSettingsPayload writes canonical settings schema into persi
assert.equal(payload.phoneSmsProvider, 'hero-sms'); assert.equal(payload.phoneSmsProvider, 'hero-sms');
assert.equal(payload.madaoBaseUrl, DEFAULT_MADAO_BASE_URL_FOR_TEST); assert.equal(payload.madaoBaseUrl, DEFAULT_MADAO_BASE_URL_FOR_TEST);
assert.equal(payload.madaoMode, DEFAULT_MADAO_MODE_FOR_TEST); assert.equal(payload.madaoMode, DEFAULT_MADAO_MODE_FOR_TEST);
assert.equal(payload.madaoOperator, '');
assert.equal(payload.madaoAutoPickCountry, true); assert.equal(payload.madaoAutoPickCountry, true);
assert.equal(payload.madaoReusePhone, true); assert.equal(payload.madaoReusePhone, true);
assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false); assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false);
@@ -724,6 +727,7 @@ test('buildPersistentSettingsPayload persists normalized MaDao flat settings out
madaoRoutingPlanId: ' rp-openai ', madaoRoutingPlanId: ' rp-openai ',
madaoProviderId: ' Upstream A! ', madaoProviderId: ' Upstream A! ',
madaoCountry: ' gb ', madaoCountry: ' gb ',
madaoOperator: ' Operator A! ',
madaoAutoPickCountry: 0, madaoAutoPickCountry: 0,
madaoReusePhone: 1, madaoReusePhone: 1,
madaoMinPrice: '0.123456', madaoMinPrice: '0.123456',
@@ -737,6 +741,7 @@ test('buildPersistentSettingsPayload persists normalized MaDao flat settings out
assert.equal(payload.madaoRoutingPlanId, 'rp-openai'); assert.equal(payload.madaoRoutingPlanId, 'rp-openai');
assert.equal(payload.madaoProviderId, 'upstreama'); assert.equal(payload.madaoProviderId, 'upstreama');
assert.equal(payload.madaoCountry, 'GB'); assert.equal(payload.madaoCountry, 'GB');
assert.equal(payload.madaoOperator, 'operatora');
assert.equal(payload.madaoAutoPickCountry, false); assert.equal(payload.madaoAutoPickCountry, false);
assert.equal(payload.madaoReusePhone, true); assert.equal(payload.madaoReusePhone, true);
assert.equal(payload.madaoMinPrice, '0.1235'); assert.equal(payload.madaoMinPrice, '0.1235');
+35
View File
@@ -38,6 +38,7 @@ test('MaDao direct acquire sends only direct fields and normalizes activation da
madaoRoutingPlanId: 'route-plan-should-not-be-sent', madaoRoutingPlanId: 'route-plan-should-not-be-sent',
madaoProviderId: 'Upstream A!', madaoProviderId: 'Upstream A!',
madaoCountry: 'gb', madaoCountry: 'gb',
madaoOperator: 'Operator A!',
madaoAutoPickCountry: 'false', madaoAutoPickCountry: 'false',
madaoReusePhone: '1', madaoReusePhone: '1',
madaoMinPrice: '0.01', madaoMinPrice: '0.01',
@@ -54,6 +55,9 @@ test('MaDao direct acquire sends only direct fields and normalizes activation da
auto_pick_country: false, auto_pick_country: false,
reuse_phone: true, reuse_phone: true,
country: 'GB', country: 'GB',
metadata: {
operator: 'operatora',
},
min_price: 0.01, min_price: 0.01,
max_price: 0.2, max_price: 0.2,
}); });
@@ -100,6 +104,37 @@ test('MaDao routing acquire sends routing plan only', async () => {
assert.equal(activation.madaoRoutingItemId, 'route-1'); assert.equal(activation.madaoRoutingItemId, 'route-1');
}); });
test('MaDao direct acquire treats any operator as default route', async () => {
const requests = [];
const provider = api.createProvider({
fetchImpl: async (_url, options = {}) => {
requests.push({ body: JSON.parse(options.body) });
return createJsonResponse({
ticket_id: 'ticket-any',
phone_number: '+66111111111',
country: 'TH',
provider: 'fivesim',
});
},
});
await provider.acquireActivation({
madaoMode: 'direct',
madaoProviderId: 'fivesim',
madaoCountry: 'TH',
madaoOperator: 'Any operator',
});
assert.equal(requests.length, 1);
assert.deepEqual(requests[0].body, {
provider: 'fivesim',
service: 'openai',
auto_pick_country: true,
reuse_phone: true,
country: 'TH',
});
});
test('MaDao poll extracts codes from nested messages and reports pending status', async () => { test('MaDao poll extracts codes from nested messages and reports pending status', async () => {
const statusEvents = []; const statusEvents = [];
const provider = api.createProvider({ const provider = api.createProvider({
+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 () => { test('phone auth resend stops with PHONE_ROUTE_405_RECOVERY_FAILED instead of endless Try-again loop', async () => {
const originalDocument = global.document; const originalDocument = global.document;
const originalLocation = global.location; const originalLocation = global.location;
+44 -1
View File
@@ -3,6 +3,8 @@ const assert = require('node:assert/strict');
const fs = require('node:fs'); const fs = require('node:fs');
const source = fs.readFileSync('phone-sms/providers/registry.js', 'utf8'); const source = fs.readFileSync('phone-sms/providers/registry.js', 'utf8');
const maDaoSource = fs.readFileSync('phone-sms/providers/madao.js', 'utf8');
const nexSmsSource = fs.readFileSync('phone-sms/providers/nexsms.js', 'utf8');
function loadRegistry(root = {}) { function loadRegistry(root = {}) {
return new Function('self', `${source}; return self.PhoneSmsProviderRegistry;`)(root); return new Function('self', `${source}; return self.PhoneSmsProviderRegistry;`)(root);
@@ -16,6 +18,9 @@ test('phone sms provider registry normalizes ids, order and labels consistently'
PhoneSmsFiveSimProvider: { PhoneSmsFiveSimProvider: {
createProvider: (deps = {}) => ({ provider: '5sim', deps }), createProvider: (deps = {}) => ({ provider: '5sim', deps }),
}, },
PhoneSmsNexSmsProvider: {
createProvider: (deps = {}) => ({ provider: 'nexsms', deps }),
},
PhoneSmsMaDaoProvider: { PhoneSmsMaDaoProvider: {
createProvider: (deps = {}) => ({ provider: 'madao', deps }), createProvider: (deps = {}) => ({ provider: 'madao', deps }),
}, },
@@ -52,5 +57,43 @@ test('phone sms provider registry normalizes ids, order and labels consistently'
registry.createProvider('madao', { foo: 2 }), registry.createProvider('madao', { foo: 2 }),
{ provider: 'madao', deps: { foo: 2 } } { provider: 'madao', deps: { foo: 2 } }
); );
assert.throws(() => registry.createProvider('nexsms'), /接码平台模块未加载:nexsms/); assert.deepStrictEqual(
registry.createProvider('nexsms', { foo: 3 }),
{ provider: 'nexsms', deps: { foo: 3 } }
);
});
test('phone sms provider registry can create the real MaDao provider module', () => {
const maDaoModule = new Function('self', `${maDaoSource}; return self.PhoneSmsMaDaoProvider;`)({});
const registry = loadRegistry({
PhoneSmsMaDaoProvider: maDaoModule,
});
const provider = registry.createProvider('madao', { fetchImpl: 'demo-fetch' });
assert.equal(provider.id, 'madao');
assert.equal(provider.label, 'MaDao');
assert.equal(provider.defaultProduct, 'openai');
assert.equal(typeof provider.acquireActivation, 'function');
assert.equal(typeof provider.pollActivation, 'function');
assert.equal(typeof provider.releaseActivation, 'function');
assert.equal(provider.mapTicketStatus('waiting_code'), 'waiting_code');
});
test('phone sms provider registry can create the real NexSMS provider module', () => {
const nexSmsModule = new Function('self', `${nexSmsSource}; return self.PhoneSmsNexSmsProvider;`)({});
const registry = loadRegistry({
PhoneSmsNexSmsProvider: nexSmsModule,
});
const provider = registry.createProvider('nexsms', { fetchImpl: 'demo-fetch' });
assert.equal(provider.id, 'nexsms');
assert.equal(provider.label, 'NexSMS');
assert.equal(provider.defaultProduct, 'OpenAI');
assert.equal(provider.defaultServiceCode, 'ot');
assert.equal(typeof provider.fetchBalance, 'function');
assert.equal(typeof provider.fetchPrices, 'function');
assert.equal(provider.normalizeCountryId('8'), 8);
assert.equal(provider.normalizeServiceCode(' OT '), 'ot');
}); });
+334 -86
View File
@@ -3,10 +3,19 @@ const assert = require('node:assert/strict');
const fs = require('node:fs'); const fs = require('node:fs');
const source = fs.readFileSync('background/phone-verification-flow.js', 'utf8'); const source = fs.readFileSync('background/phone-verification-flow.js', 'utf8');
const globalScope = {}; const heroSmsSource = fs.readFileSync('phone-sms/providers/hero-sms.js', 'utf8');
const api = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(globalScope); const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
const nexSmsSource = fs.readFileSync('phone-sms/providers/nexsms.js', 'utf8');
const maDaoSource = fs.readFileSync('phone-sms/providers/madao.js', 'utf8'); const maDaoSource = fs.readFileSync('phone-sms/providers/madao.js', 'utf8');
const maDaoModule = new Function('self', `${maDaoSource}; return self.PhoneSmsMaDaoProvider;`)({}); const registrySource = fs.readFileSync('phone-sms/providers/registry.js', 'utf8');
const globalScope = {};
new Function('self', `${heroSmsSource}; return self.PhoneSmsHeroSmsProvider;`)(globalScope);
new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)(globalScope);
new Function('self', `${nexSmsSource}; return self.PhoneSmsNexSmsProvider;`)(globalScope);
new Function('self', `${maDaoSource}; return self.PhoneSmsMaDaoProvider;`)(globalScope);
new Function('self', `${registrySource}; return self.PhoneSmsProviderRegistry;`)(globalScope);
const api = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(globalScope);
const maDaoModule = globalScope.PhoneSmsMaDaoProvider;
function buildHeroSmsPricesPayload({ country = '52', service = 'dr', cost = 0.08, count = 25370, physicalCount = 14528 } = {}) { function buildHeroSmsPricesPayload({ country = '52', service = 'dr', cost = 0.08, count = 25370, physicalCount = 14528 } = {}) {
return JSON.stringify({ return JSON.stringify({
@@ -157,6 +166,110 @@ test('phone verification helper reads latest HeroSMS operator from persistent st
assert.equal(getNumberRequest.searchParams.get('operator'), 'dtac'); assert.equal(getNumberRequest.searchParams.get('operator'), 'dtac');
}); });
test('phone verification helper creates 5sim adapter through provider registry when available', async () => {
const createCalls = [];
const root = {
PhoneSmsProviderRegistry: {
createProvider: (providerId, deps = {}) => {
createCalls.push({ providerId, deps });
return {
requestActivation: async () => ({
activationId: '5sim-registry-1',
phoneNumber: '+447700900001',
provider: '5sim',
serviceCode: 'openai',
countryId: 'england',
}),
};
},
},
};
const registryApi = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(root);
const helpers = registryApi.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async () => {
throw new Error('5sim registry adapter should own network access');
},
getState: async () => ({ phoneSmsProvider: '5sim' }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const activation = await helpers.requestPhoneActivation({ phoneSmsProvider: '5sim' });
assert.equal(createCalls.length, 1);
assert.equal(createCalls[0].providerId, '5sim');
assert.equal(typeof createCalls[0].deps.fetchImpl, 'function');
assert.equal(activation.activationId, '5sim-registry-1');
});
test('phone verification helper creates MaDao adapter through provider registry when available', async () => {
const createCalls = [];
const requests = [];
const root = {
PhoneSmsProviderRegistry: {
createProvider: (providerId, deps = {}) => {
createCalls.push({ providerId, deps });
return maDaoModule.createProvider(deps);
},
},
};
const registryApi = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(root);
const currentState = {
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822/',
madaoHttpSecret: 'secret-token',
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'rp-openai',
};
const helpers = registryApi.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url);
requests.push({ url: parsedUrl, options, body: options.body ? JSON.parse(options.body) : null });
if (parsedUrl.pathname === '/api/acquire') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: 'madao-registry-1',
phone_number: '+14155550123',
service: 'openai',
country: 'CA',
provider: 'upstream-a',
routing_plan_id: 'rp-openai',
routing_item_id: 'route-1',
status: 'waiting_code',
}),
};
}
throw new Error(`Unexpected MaDao path: ${parsedUrl.pathname}`);
},
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const activation = await helpers.requestPhoneActivation(currentState);
assert.equal(createCalls.length, 1);
assert.equal(createCalls[0].providerId, 'madao');
assert.equal(typeof createCalls[0].deps.fetchImpl, 'function');
assert.equal(activation.activationId, 'madao-registry-1');
assert.equal(activation.countryId, 'CA');
assert.deepStrictEqual(requests[0].body, {
provider: 'auto',
service: 'openai',
routing_plan_id: 'rp-openai',
});
});
test('phone verification helper acquires, polls and releases MaDao activation through provider adapter', async () => { test('phone verification helper acquires, polls and releases MaDao activation through provider adapter', async () => {
const requests = []; const requests = [];
let currentState = { let currentState = {
@@ -172,7 +285,6 @@ test('phone verification helper acquires, polls and releases MaDao activation th
}; };
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
createMaDaoProvider: maDaoModule.createProvider,
ensureStep8SignupPageReady: async () => {}, ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url, options = {}) => { fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
@@ -252,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' }); 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 () => { test('signup phone helper persists signup runtime state without touching add-phone activation', async () => {
const setStateCalls = []; const setStateCalls = [];
let currentState = { let currentState = {
@@ -974,8 +1241,6 @@ test('signup phone helper does not let a hung page-state probe stall HeroSMS pol
}); });
test('signup phone helper fails stale email-verification on 5sim RECEIVED without code during SMS polling', async () => { test('signup phone helper fails stale email-verification on 5sim RECEIVED without code during SMS polling', async () => {
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
let checkCount = 0; let checkCount = 0;
let pageStateReads = 0; let pageStateReads = 0;
const contentMessages = []; const contentMessages = [];
@@ -1005,7 +1270,6 @@ test('signup phone helper fails stale email-verification on 5sim RECEIVED withou
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
ensureStep8SignupPageReady: async () => {}, ensureStep8SignupPageReady: async () => {},
createFiveSimProvider: fiveSimModule.createProvider,
fetchImpl: async (url) => { fetchImpl: async (url) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
if (parsedUrl.pathname === '/v1/user/check/5001') { if (parsedUrl.pathname === '/v1/user/check/5001') {
@@ -2352,29 +2616,28 @@ test('phone verification helper acquires a number from 5sim with fallback countr
heroSmsActivationRetryRounds: 1, heroSmsActivationRetryRounds: 1,
}); });
assert.deepStrictEqual(activation, { assert.equal(activation.activationId, '9876543');
activationId: '9876543', assert.equal(activation.phoneNumber, '+447911123456');
phoneNumber: '+447911123456', assert.equal(activation.provider, '5sim');
provider: '5sim', assert.equal(activation.serviceCode, 'openai');
serviceCode: 'openai', assert.equal(activation.countryId, 'england');
countryId: 'england', assert.equal(activation.countryCode, 'england');
countryCode: 'england', assert.equal(activation.successfulUses, 0);
countryLabel: 'England', assert.equal(activation.maxUses, 3);
successfulUses: 0, assert.equal(requests.length, 6);
maxUses: 3, assert.equal(requests[0].pathname, '/v1/guest/products/thailand/any');
}); assert.equal(requests[1].pathname, '/v1/guest/prices');
assert.equal(requests.length, 4); assert.equal(requests[1].search.get('country'), 'thailand');
assert.equal(requests[0].pathname, '/v1/guest/prices'); assert.equal(requests[1].search.get('product'), 'openai');
assert.equal(requests[0].search.get('country'), 'thailand'); assert.equal(requests[2].pathname, '/v1/user/buy/activation/thailand/any/openai');
assert.equal(requests[0].search.get('product'), 'openai'); assert.equal(requests[2].search.get('maxPrice'), '0.08');
assert.equal(requests[1].pathname, '/v1/user/buy/activation/thailand/any/openai'); assert.equal(requests[2].search.get('reuse'), '1');
assert.equal(requests[1].search.get('maxPrice'), '0.08'); assert.equal(requests[2].headers.Authorization, 'Bearer five-token');
assert.equal(requests[1].search.get('reuse'), '1'); assert.equal(requests[3].pathname, '/v1/guest/products/england/any');
assert.equal(requests[1].headers.Authorization, 'Bearer five-token'); assert.equal(requests[4].pathname, '/v1/guest/prices');
assert.equal(requests[2].pathname, '/v1/guest/prices'); assert.equal(requests[4].search.get('country'), 'england');
assert.equal(requests[2].search.get('country'), 'england'); assert.equal(requests[4].search.get('product'), 'openai');
assert.equal(requests[2].search.get('product'), 'openai'); assert.equal(requests[5].pathname, '/v1/user/buy/activation/england/any/openai');
assert.equal(requests[3].pathname, '/v1/user/buy/activation/england/any/openai');
}); });
test('phone verification helper preserves fallback provider while refreshing latest settings', async () => { test('phone verification helper preserves fallback provider while refreshing latest settings', async () => {
@@ -2514,13 +2777,14 @@ test('phone verification helper preserves fallback provider while refreshing lat
assert.deepStrictEqual( assert.deepStrictEqual(
fiveSimRequests.map((entry) => entry.url.pathname), fiveSimRequests.map((entry) => entry.url.pathname),
[ [
'/v1/guest/products/vietnam/any',
'/v1/guest/prices', '/v1/guest/prices',
'/v1/user/buy/activation/vietnam/any/openai', '/v1/user/buy/activation/vietnam/any/openai',
'/v1/user/check/5020', '/v1/user/check/5020',
'/v1/user/finish/5020', '/v1/user/finish/5020',
] ]
); );
assert.equal(fiveSimRequests[1].options.headers.Authorization, 'Bearer five-token'); assert.equal(fiveSimRequests[2].options.headers.Authorization, 'Bearer five-token');
}); });
test('phone verification helper prefers phoneSmsReuseEnabled over legacy heroSmsReuseEnabled for 5sim acquisition', async () => { test('phone verification helper prefers phoneSmsReuseEnabled over legacy heroSmsReuseEnabled for 5sim acquisition', async () => {
@@ -2593,20 +2857,18 @@ test('phone verification helper prefers phoneSmsReuseEnabled over legacy heroSms
heroSmsActivationRetryRounds: 1, heroSmsActivationRetryRounds: 1,
}); });
assert.deepStrictEqual(activation, { assert.equal(activation.activationId, '1234567');
activationId: '1234567', assert.equal(activation.phoneNumber, '+66880000000');
phoneNumber: '+66880000000', assert.equal(activation.provider, '5sim');
provider: '5sim', assert.equal(activation.serviceCode, 'openai');
serviceCode: 'openai', assert.equal(activation.countryId, 'thailand');
countryId: 'thailand', assert.equal(activation.countryCode, 'thailand');
countryCode: 'thailand', assert.equal(activation.successfulUses, 0);
countryLabel: 'Thailand', assert.equal(activation.maxUses, 3);
successfulUses: 0, assert.equal(requests[0].pathname, '/v1/guest/products/thailand/any');
maxUses: 3, assert.equal(requests[1].pathname, '/v1/guest/prices');
}); assert.equal(requests[2].pathname, '/v1/user/buy/activation/thailand/any/openai');
assert.equal(requests[0].pathname, '/v1/guest/prices'); assert.equal(requests[2].search.get('reuse'), null);
assert.equal(requests[1].pathname, '/v1/user/buy/activation/thailand/any/openai');
assert.equal(requests[1].search.get('reuse'), null);
}); });
test('phone verification helper treats fiveSimReuseEnabled as legacy-only when phoneSmsReuseEnabled is absent', async () => { test('phone verification helper treats fiveSimReuseEnabled as legacy-only when phoneSmsReuseEnabled is absent', async () => {
@@ -2679,20 +2941,18 @@ test('phone verification helper treats fiveSimReuseEnabled as legacy-only when p
heroSmsActivationRetryRounds: 1, heroSmsActivationRetryRounds: 1,
}); });
assert.deepStrictEqual(activation, { assert.equal(activation.activationId, '1234568');
activationId: '1234568', assert.equal(activation.phoneNumber, '+66880000001');
phoneNumber: '+66880000001', assert.equal(activation.provider, '5sim');
provider: '5sim', assert.equal(activation.serviceCode, 'openai');
serviceCode: 'openai', assert.equal(activation.countryId, 'thailand');
countryId: 'thailand', assert.equal(activation.countryCode, 'thailand');
countryCode: 'thailand', assert.equal(activation.successfulUses, 0);
countryLabel: 'Thailand', assert.equal(activation.maxUses, 3);
successfulUses: 0, assert.equal(requests[0].pathname, '/v1/guest/products/thailand/any');
maxUses: 3, assert.equal(requests[1].pathname, '/v1/guest/prices');
}); assert.equal(requests[2].pathname, '/v1/user/buy/activation/thailand/any/openai');
assert.equal(requests[0].pathname, '/v1/guest/prices'); assert.equal(requests[2].search.get('reuse'), '1');
assert.equal(requests[1].pathname, '/v1/user/buy/activation/thailand/any/openai');
assert.equal(requests[1].search.get('reuse'), '1');
}); });
test('phone verification helper rejects 5sim maxPrice with custom operator before buying', async () => { test('phone verification helper rejects 5sim maxPrice with custom operator before buying', async () => {
@@ -3115,7 +3375,7 @@ test('phone verification helper polls and parses 5sim verification codes', async
assert.equal(code, '246810'); assert.equal(code, '246810');
assert.equal(checkCount, 2); assert.equal(checkCount, 2);
assert.deepStrictEqual(statusUpdates, ['PENDING']); assert.deepStrictEqual(statusUpdates, ['PENDING', 'RECEIVED']);
}); });
test('phone verification helper treats HeroSMS STATUS_WAIT_RETRY payload status as pending', async () => { test('phone verification helper treats HeroSMS STATUS_WAIT_RETRY payload status as pending', async () => {
@@ -3242,18 +3502,16 @@ test('phone verification helper reuses 5sim by keeping the original activation',
} }
); );
assert.deepStrictEqual(nextActivation, { assert.equal(nextActivation.activationId, '600001');
activationId: '600001', assert.equal(nextActivation.phoneNumber, '+44 7911-123-456');
phoneNumber: '+44 7911-123-456', assert.equal(nextActivation.provider, '5sim');
provider: '5sim', assert.equal(nextActivation.serviceCode, 'openai');
serviceCode: 'openai', assert.equal(nextActivation.countryId, 'england');
countryId: 'england', assert.equal(nextActivation.countryCode, 'england');
countryCode: 'england', assert.equal(nextActivation.successfulUses, 0);
successfulUses: 0, assert.equal(nextActivation.maxUses, 1);
maxUses: 1, assert.equal(nextActivation.source, '5sim-retained-reuse');
source: '5sim-retained-reuse', assert.deepStrictEqual(nextActivation.ignoredPhoneCodeKeys, ['old::111111']);
ignoredPhoneCodeKeys: ['old::111111'],
});
assert.deepStrictEqual(requests, ['/v1/user/check/600001']); assert.deepStrictEqual(requests, ['/v1/user/check/600001']);
}); });
@@ -6448,12 +6706,9 @@ test('phone verification helper auto free-reuses 5sim by polling the retained or
}, },
}; };
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
ensureStep8SignupPageReady: async () => {}, ensureStep8SignupPageReady: async () => {},
createFiveSimProvider: fiveSimModule.createProvider,
fetchImpl: async (url) => { fetchImpl: async (url) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
requests.push(parsedUrl); requests.push(parsedUrl);
@@ -6547,12 +6802,9 @@ test('phone verification helper retires failed 5sim free-reuse record instead of
}, },
}; };
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
ensureStep8SignupPageReady: async () => {}, ensureStep8SignupPageReady: async () => {},
createFiveSimProvider: fiveSimModule.createProvider,
fetchImpl: async (url) => { fetchImpl: async (url) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
requests.push(parsedUrl); requests.push(parsedUrl);
@@ -8724,12 +8976,9 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
reusablePhoneActivation: null, reusablePhoneActivation: null,
}; };
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
ensureStep8SignupPageReady: async () => {}, ensureStep8SignupPageReady: async () => {},
createFiveSimProvider: fiveSimModule.createProvider,
fetchImpl: async (url, options = {}) => { fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
requests.push({ url: parsedUrl, options }); requests.push({ url: parsedUrl, options });
@@ -8838,7 +9087,6 @@ test('phone verification helper uses MaDao routing replace when add-phone reject
}; };
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
createMaDaoProvider: maDaoModule.createProvider,
ensureStep8SignupPageReady: async () => {}, ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url, options = {}) => { fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
@@ -8966,6 +9214,7 @@ test('phone verification helper reacquires MaDao direct numbers after releasing
madaoRoutingPlanId: 'rp-stale', madaoRoutingPlanId: 'rp-stale',
madaoProviderId: 'upstream-a', madaoProviderId: 'upstream-a',
madaoCountry: 'gb', madaoCountry: 'gb',
madaoOperator: 'operator-a',
madaoAutoPickCountry: false, madaoAutoPickCountry: false,
madaoReusePhone: true, madaoReusePhone: true,
madaoMinPrice: '0.01', madaoMinPrice: '0.01',
@@ -8981,7 +9230,6 @@ test('phone verification helper reacquires MaDao direct numbers after releasing
}; };
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
createMaDaoProvider: maDaoModule.createProvider,
ensureStep8SignupPageReady: async () => {}, ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url, options = {}) => { fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
@@ -9076,6 +9324,9 @@ test('phone verification helper reacquires MaDao direct numbers after releasing
auto_pick_country: false, auto_pick_country: false,
reuse_phone: true, reuse_phone: true,
country: 'GB', country: 'GB',
metadata: {
operator: 'operator-a',
},
min_price: 0.01, min_price: 0.01,
max_price: 0.2, max_price: 0.2,
}); });
@@ -9114,12 +9365,9 @@ test('phone verification helper keeps 5sim reusable activation on the original o
}, },
}; };
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
ensureStep8SignupPageReady: async () => {}, ensureStep8SignupPageReady: async () => {},
createFiveSimProvider: fiveSimModule.createProvider,
fetchImpl: async (url) => { fetchImpl: async (url) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
requests.push(parsedUrl); requests.push(parsedUrl);
@@ -156,15 +156,28 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
assert.match(html, /id="row-madao-mode"/); assert.match(html, /id="row-madao-mode"/);
assert.match(html, /id="select-madao-mode"/); assert.match(html, /id="select-madao-mode"/);
assert.match(html, /id="row-madao-routing-plan-id"/); assert.match(html, /id="row-madao-routing-plan-id"/);
assert.match(html, /id="input-madao-routing-plan-id"/); assert.match(html, /id="select-madao-routing-plan-id"/);
assert.match(html, /id="btn-madao-refresh-routing-plans"/);
assert.doesNotMatch(html, /id="input-madao-routing-plan-id"/);
assert.match(html, /id="row-madao-provider-id"/); assert.match(html, /id="row-madao-provider-id"/);
assert.match(html, /id="input-madao-provider-id"/); assert.match(html, /id="select-madao-provider-id"/);
assert.match(html, /id="btn-madao-refresh-providers"/);
assert.doesNotMatch(html, /id="input-madao-provider-id"/);
assert.match(html, /id="row-madao-country"/); assert.match(html, /id="row-madao-country"/);
assert.match(html, /id="input-madao-country"/); assert.match(html, /id="select-madao-country"/);
assert.match(html, /id="row-madao-auto-pick-country"/); assert.match(html, /id="btn-madao-refresh-countries"/);
assert.match(html, /id="input-madao-auto-pick-country"/); assert.doesNotMatch(html, /id="input-madao-country"/);
assert.match(html, /id="row-madao-reuse-phone"/); assert.match(html, /id="row-madao-operator"/);
assert.match(html, /id="input-madao-reuse-phone"/); assert.match(html, /id="select-madao-operator"/);
assert.match(html, /id="btn-madao-refresh-operators"/);
assert.doesNotMatch(html, /id="row-madao-auto-pick-country"/);
assert.doesNotMatch(html, /id="input-madao-auto-pick-country"/);
assert.doesNotMatch(html, /id="row-madao-reuse-phone"/);
assert.doesNotMatch(html, /id="input-madao-reuse-phone"/);
assert.doesNotMatch(html, /直连平台/);
assert.doesNotMatch(html, /直连国家/);
assert.doesNotMatch(html, /自动选国家/);
assert.doesNotMatch(html, /MaDao 复用/);
assert.match(html, /id="row-madao-price-range"/); assert.match(html, /id="row-madao-price-range"/);
assert.match(html, /id="input-madao-min-price"/); assert.match(html, /id="input-madao-min-price"/);
assert.match(html, /id="input-madao-max-price"/); assert.match(html, /id="input-madao-max-price"/);
@@ -190,6 +203,228 @@ test('sidepanel loads live SMS country lists silently during startup', () => {
assert.doesNotMatch(sidepanelSource, /console\.error\('加载 (?:HeroSMS|5sim|NexSMS) 国家列表失败:'/); assert.doesNotMatch(sidepanelSource, /console\.error\('加载 (?:HeroSMS|5sim|NexSMS) 国家列表失败:'/);
}); });
test('MaDao routing plan select loads options from helper API and preserves saved plan', async () => {
const requests = [];
const fetchImpl = async (url, options = {}) => {
requests.push({ url, options });
return {
ok: true,
status: 200,
statusText: 'OK',
async text() {
return JSON.stringify({
plans: [
{ id: 'openai-plan', name: 'OpenAI Plan', service: 'openai', enabled: true },
{ id: 'kiro-plan', name: 'Kiro Plan', service: 'kiro', enabled: true },
{ id: 'disabled-plan', name: 'Disabled Plan', service: 'openai', enabled: false },
],
});
},
};
};
const api = new Function('fetch', `
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
let latestState = {
madaoBaseUrl: 'http://madao.local/api/acquire',
madaoHttpSecret: 'madao-secret',
madaoRoutingPlanId: 'stored-plan',
};
let maDaoRoutingPlanOptions = [];
let displayText = '';
let toastText = '';
const inputMaDaoBaseUrl = { value: 'http://madao.local/api/acquire' };
const inputMaDaoHttpSecret = { value: 'madao-secret' };
const selectMaDaoRoutingPlanId = {
value: 'stored-plan',
options: [],
replaceChildren(...children) {
this.options = children;
},
};
function updateHeroSmsPlatformDisplay() {
displayText = getSelectedMaDaoRoutingPlanLabel();
}
function showToast(message) {
toastText = message;
}
${extractFunction('normalizeMaDaoBaseUrlValue')}
${extractFunction('normalizeMaDaoIdentifierValue')}
${extractFunction('normalizeMaDaoRoutingPlanIdValue')}
${extractFunction('createSelectOptionElement')}
${extractFunction('setSelectOptions')}
${extractFunction('buildMaDaoRoutingPlanOptions')}
${extractFunction('setMaDaoRoutingPlanSelectOptions')}
${extractFunction('buildMaDaoRequestUrl')}
${extractFunction('buildMaDaoRequestHeaders')}
${extractFunction('fetchMaDaoJson')}
${extractFunction('getMaDaoRoutingPlansFromPayload')}
${extractFunction('loadMaDaoRoutingPlans')}
${extractFunction('getSelectedMaDaoRoutingPlanLabel')}
return {
loadMaDaoRoutingPlans,
selectMaDaoRoutingPlanId,
get options() { return selectMaDaoRoutingPlanId.options.map((option) => ({ value: option.value, label: option.textContent, selected: option.selected })); },
get displayText() { return displayText; },
get toastText() { return toastText; },
};
`)(fetchImpl);
const plans = await api.loadMaDaoRoutingPlans();
assert.deepStrictEqual(plans.map((plan) => plan.value), ['openai-plan']);
assert.equal(api.selectMaDaoRoutingPlanId.value, 'stored-plan');
assert.deepStrictEqual(api.options.map((option) => option.value), ['', 'stored-plan', 'openai-plan']);
assert.equal(api.options.find((option) => option.value === 'stored-plan').selected, true);
assert.equal(api.displayText, 'stored-plan');
assert.equal(api.toastText, '已刷新 MaDao 路由计划。');
assert.equal(requests[0].url, 'http://madao.local/api/routing-plans');
assert.equal(requests[0].options.headers.Authorization, 'Bearer madao-secret');
});
test('MaDao direct selects load provider country and operator options from daemon API', async () => {
const requests = [];
const fetchImpl = async (url, options = {}) => {
const parsedUrl = new URL(url);
requests.push({
pathname: parsedUrl.pathname,
options,
body: options.body ? JSON.parse(options.body) : null,
});
if (parsedUrl.pathname === '/api/providers') {
return {
ok: true,
status: 200,
statusText: 'OK',
async text() {
return JSON.stringify({
providers: [
{ id: 'stored-provider', name: 'Stored Provider', enabled: true, protocol_label: 'REST' },
{ id: 'disabled-provider', name: 'Disabled Provider', enabled: false },
],
});
},
};
}
if (parsedUrl.pathname === '/api/providers/stored-provider/countries') {
return {
ok: true,
status: 200,
statusText: 'OK',
async text() {
return JSON.stringify({
provider: 'stored-provider',
items: [
{ value: 'GB', label: 'United Kingdom', label_zh: '英国', provider_value: 'england' },
{ value: 'TH', label: 'Thailand', label_zh: '泰国', provider_value: '52' },
],
});
},
};
}
if (parsedUrl.pathname === '/api/providers/stored-provider/operators') {
return {
ok: true,
status: 200,
statusText: 'OK',
async text() {
return JSON.stringify({
provider: 'stored-provider',
items: [
{ value: 'any', label: 'Any operator' },
{ value: 'operator-a', label: 'Operator A' },
{ value: 'operator-b', label: 'Operator B' },
],
});
},
};
}
throw new Error(`Unexpected MaDao path: ${parsedUrl.pathname}`);
};
const api = new Function('fetch', `
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
let latestState = {
madaoBaseUrl: 'http://madao.local/api/acquire',
madaoHttpSecret: 'madao-secret',
madaoProviderId: 'stored-provider',
madaoCountry: 'england',
madaoOperator: 'operator-a',
};
let maDaoProviderOptions = [];
let maDaoCountryOptions = [];
let maDaoOperatorOptions = [];
let displayText = '';
let toastText = '';
const inputMaDaoBaseUrl = { value: 'http://madao.local/api/acquire' };
const inputMaDaoHttpSecret = { value: 'madao-secret' };
const selectMaDaoProviderId = { value: 'stored-provider', options: [], replaceChildren(...children) { this.options = children; } };
const selectMaDaoCountry = { value: 'england', options: [], replaceChildren(...children) { this.options = children; } };
const selectMaDaoOperator = { value: 'operator-a', options: [], replaceChildren(...children) { this.options = children; } };
function updateHeroSmsPlatformDisplay() {
displayText = [selectMaDaoProviderId.value, selectMaDaoCountry.value, selectMaDaoOperator.value].filter(Boolean).join('/');
}
function showToast(message) {
toastText = message;
}
${extractFunction('normalizeMaDaoBaseUrlValue')}
${extractFunction('normalizeMaDaoIdentifierValue')}
${extractFunction('normalizeMaDaoProviderIdValue')}
${extractFunction('normalizeMaDaoOperatorValue')}
${extractFunction('normalizeMaDaoCountry')}
${extractFunction('formatMaDaoCountryDisplayLabel')}
${extractFunction('createSelectOptionElement')}
${extractFunction('setSelectOptions')}
${extractFunction('normalizeMaDaoOptionListItems')}
${extractFunction('resolveMaDaoOptionSelectedValue')}
${extractFunction('setMaDaoProviderSelectOptions')}
${extractFunction('setMaDaoCountrySelectOptions')}
${extractFunction('setMaDaoOperatorSelectOptions')}
${extractFunction('buildMaDaoRequestUrl')}
${extractFunction('buildMaDaoRequestHeaders')}
${extractFunction('fetchMaDaoJson')}
${extractFunction('getMaDaoProvidersFromPayload')}
${extractFunction('getMaDaoOptionItemsFromPayload')}
${extractFunction('getSelectedMaDaoProviderId')}
${extractFunction('getSelectedMaDaoCountry')}
${extractFunction('loadMaDaoProviders')}
${extractFunction('loadMaDaoCountries')}
${extractFunction('loadMaDaoOperators')}
return {
loadMaDaoProviders,
selectMaDaoProviderId,
selectMaDaoCountry,
selectMaDaoOperator,
get providerOptions() { return selectMaDaoProviderId.options.map((option) => ({ value: option.value, label: option.textContent, selected: option.selected })); },
get countryOptions() { return selectMaDaoCountry.options.map((option) => ({ value: option.value, label: option.textContent, selected: option.selected })); },
get operatorOptions() { return selectMaDaoOperator.options.map((option) => ({ value: option.value, label: option.textContent, selected: option.selected })); },
get displayText() { return displayText; },
get toastText() { return toastText; },
};
`)(fetchImpl);
const providers = await api.loadMaDaoProviders();
assert.deepStrictEqual(providers.map((provider) => provider.value), ['stored-provider']);
assert.equal(api.selectMaDaoProviderId.value, 'stored-provider');
assert.equal(api.selectMaDaoCountry.value, 'GB');
assert.equal(api.selectMaDaoOperator.value, 'operator-a');
assert.deepStrictEqual(api.providerOptions.map((option) => option.value), ['', 'stored-provider']);
assert.deepStrictEqual(api.countryOptions.map((option) => option.value), ['', 'GB', 'TH']);
assert.deepStrictEqual(api.countryOptions.map((option) => option.label), ['请先选择服务商', '英国', '泰国']);
assert.deepStrictEqual(api.operatorOptions.map((option) => option.value), ['', 'operator-a', 'operator-b']);
assert.deepStrictEqual(api.operatorOptions.map((option) => option.label), ['任意线路', 'Operator A', 'Operator B']);
assert.equal(api.displayText, 'stored-provider/GB/operator-a');
assert.equal(api.toastText, '已刷新 MaDao 服务商。');
assert.deepStrictEqual(requests.map((request) => request.pathname), [
'/api/providers',
'/api/providers/stored-provider/countries',
'/api/providers/stored-provider/operators',
]);
assert.deepStrictEqual(requests[2].body, { country: 'GB' });
assert.equal(requests[0].options.headers.Authorization, 'Bearer madao-secret');
});
test('HeroSMS country parser accepts keyed country maps from the live API', () => { test('HeroSMS country parser accepts keyed country maps from the live API', () => {
const api = new Function(` const api = new Function(`
${extractFunction('normalizeHeroSmsCountryPayloadEntries')} ${extractFunction('normalizeHeroSmsCountryPayloadEntries')}
@@ -704,6 +939,7 @@ const rowMaDaoMode = { style: { display: 'none' } };
const rowMaDaoRoutingPlanId = { style: { display: 'none' } }; const rowMaDaoRoutingPlanId = { style: { display: 'none' } };
const rowMaDaoProviderId = { style: { display: 'none' } }; const rowMaDaoProviderId = { style: { display: 'none' } };
const rowMaDaoCountry = { style: { display: 'none' } }; const rowMaDaoCountry = { style: { display: 'none' } };
const rowMaDaoOperator = { style: { display: 'none' } };
const rowMaDaoAutoPickCountry = { style: { display: 'none' } }; const rowMaDaoAutoPickCountry = { style: { display: 'none' } };
const rowMaDaoReusePhone = { style: { display: 'none' } }; const rowMaDaoReusePhone = { style: { display: 'none' } };
const rowMaDaoPriceRange = { style: { display: 'none' } }; const rowMaDaoPriceRange = { style: { display: 'none' } };
@@ -785,8 +1021,7 @@ const PHONE_SMS_PROVIDER_UI_DESCRIPTORS = ${JSON.stringify({
directRowKeys: [ directRowKeys: [
'rowMaDaoProviderId', 'rowMaDaoProviderId',
'rowMaDaoCountry', 'rowMaDaoCountry',
'rowMaDaoAutoPickCountry', 'rowMaDaoOperator',
'rowMaDaoReusePhone',
'rowMaDaoPriceRange', 'rowMaDaoPriceRange',
], ],
}, },
@@ -862,6 +1097,7 @@ return {
rowMaDaoRoutingPlanId, rowMaDaoRoutingPlanId,
rowMaDaoProviderId, rowMaDaoProviderId,
rowMaDaoCountry, rowMaDaoCountry,
rowMaDaoOperator,
rowMaDaoAutoPickCountry, rowMaDaoAutoPickCountry,
rowMaDaoReusePhone, rowMaDaoReusePhone,
rowMaDaoPriceRange, rowMaDaoPriceRange,
@@ -941,6 +1177,7 @@ return {
assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none'); assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none');
assert.equal(api.rowMaDaoProviderId.style.display, 'none'); assert.equal(api.rowMaDaoProviderId.style.display, 'none');
assert.equal(api.rowMaDaoCountry.style.display, 'none'); assert.equal(api.rowMaDaoCountry.style.display, 'none');
assert.equal(api.rowMaDaoOperator.style.display, 'none');
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none'); assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
assert.equal(api.rowMaDaoReusePhone.style.display, 'none'); assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
assert.equal(api.rowMaDaoPriceRange.style.display, 'none'); assert.equal(api.rowMaDaoPriceRange.style.display, 'none');
@@ -1057,6 +1294,7 @@ return {
assert.equal(api.rowMaDaoRoutingPlanId.style.display, ''); assert.equal(api.rowMaDaoRoutingPlanId.style.display, '');
assert.equal(api.rowMaDaoProviderId.style.display, 'none'); assert.equal(api.rowMaDaoProviderId.style.display, 'none');
assert.equal(api.rowMaDaoCountry.style.display, 'none'); assert.equal(api.rowMaDaoCountry.style.display, 'none');
assert.equal(api.rowMaDaoOperator.style.display, 'none');
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none'); assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
assert.equal(api.rowMaDaoReusePhone.style.display, 'none'); assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
assert.equal(api.rowMaDaoPriceRange.style.display, 'none'); assert.equal(api.rowMaDaoPriceRange.style.display, 'none');
@@ -1069,8 +1307,9 @@ return {
assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none'); assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none');
assert.equal(api.rowMaDaoProviderId.style.display, ''); assert.equal(api.rowMaDaoProviderId.style.display, '');
assert.equal(api.rowMaDaoCountry.style.display, ''); assert.equal(api.rowMaDaoCountry.style.display, '');
assert.equal(api.rowMaDaoAutoPickCountry.style.display, ''); assert.equal(api.rowMaDaoOperator.style.display, '');
assert.equal(api.rowMaDaoReusePhone.style.display, ''); assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
assert.equal(api.rowMaDaoPriceRange.style.display, ''); assert.equal(api.rowMaDaoPriceRange.style.display, '');
}); });
@@ -1160,9 +1399,10 @@ const inputNexSmsServiceCode = { value: 'ot' };
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/acquire' }; const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/acquire' };
const inputMaDaoHttpSecret = { value: 'madao-secret' }; const inputMaDaoHttpSecret = { value: 'madao-secret' };
const selectMaDaoMode = { value: 'direct' }; const selectMaDaoMode = { value: 'direct' };
const inputMaDaoRoutingPlanId = { value: 'plan-1' }; const selectMaDaoRoutingPlanId = { value: 'plan-1' };
const inputMaDaoProviderId = { value: 'Local Provider!!' }; const selectMaDaoProviderId = { value: 'Local Provider!!' };
const inputMaDaoCountry = { value: 'th' }; const selectMaDaoCountry = { value: 'th' };
const selectMaDaoOperator = { value: 'Operator A!' };
const inputMaDaoAutoPickCountry = { checked: false }; const inputMaDaoAutoPickCountry = { checked: false };
const inputMaDaoReusePhone = { checked: true }; const inputMaDaoReusePhone = { checked: true };
const inputMaDaoMinPrice = { value: '0.02' }; const inputMaDaoMinPrice = { value: '0.02' };
@@ -1266,7 +1506,9 @@ ${extractFunction('normalizeMaDaoBaseUrlValue')}
${extractFunction('normalizeMaDaoModeValue')} ${extractFunction('normalizeMaDaoModeValue')}
${extractFunction('normalizeMaDaoIdentifierValue')} ${extractFunction('normalizeMaDaoIdentifierValue')}
${extractFunction('normalizeMaDaoProviderIdValue')} ${extractFunction('normalizeMaDaoProviderIdValue')}
${extractFunction('normalizeMaDaoOperatorValue')}
${extractFunction('normalizeMaDaoCountry')} ${extractFunction('normalizeMaDaoCountry')}
${extractFunction('formatMaDaoCountryDisplayLabel')}
${extractFunction('normalizeMaDaoPriceValue')} ${extractFunction('normalizeMaDaoPriceValue')}
${extractFunction('normalizeFiveSimCountryCode')} ${extractFunction('normalizeFiveSimCountryCode')}
${extractFunction('normalizeFiveSimCountryOrderValue')} ${extractFunction('normalizeFiveSimCountryOrderValue')}
@@ -1338,6 +1580,7 @@ return { collectSettingsPayload };
assert.equal(payload.madaoRoutingPlanId, 'plan-1'); assert.equal(payload.madaoRoutingPlanId, 'plan-1');
assert.equal(payload.madaoProviderId, 'localprovider'); assert.equal(payload.madaoProviderId, 'localprovider');
assert.equal(payload.madaoCountry, 'TH'); assert.equal(payload.madaoCountry, 'TH');
assert.equal(payload.madaoOperator, 'operatora');
assert.equal(payload.madaoAutoPickCountry, false); assert.equal(payload.madaoAutoPickCountry, false);
assert.equal(payload.madaoReusePhone, true); assert.equal(payload.madaoReusePhone, true);
assert.equal(payload.madaoMinPrice, '0.02'); assert.equal(payload.madaoMinPrice, '0.02');
@@ -1389,6 +1632,7 @@ let latestState = {
madaoRoutingPlanId: 'plan-old', madaoRoutingPlanId: 'plan-old',
madaoProviderId: 'provider-old', madaoProviderId: 'provider-old',
madaoCountry: 'TH', madaoCountry: 'TH',
madaoOperator: 'operator-old',
madaoAutoPickCountry: true, madaoAutoPickCountry: true,
madaoReusePhone: true, madaoReusePhone: true,
madaoMinPrice: '0.01', madaoMinPrice: '0.01',
@@ -1434,9 +1678,34 @@ const inputNexSmsServiceCode = { value: 'ot' };
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/poll' }; const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/poll' };
const inputMaDaoHttpSecret = { value: 'madao-live-secret' }; const inputMaDaoHttpSecret = { value: 'madao-live-secret' };
const selectMaDaoMode = { value: 'direct' }; const selectMaDaoMode = { value: 'direct' };
const inputMaDaoRoutingPlanId = { value: 'plan-live' }; const selectMaDaoRoutingPlanId = {
const inputMaDaoProviderId = { value: 'Provider Live!' }; value: 'plan-live',
const inputMaDaoCountry = { value: 'local' }; options: [],
replaceChildren(...children) {
this.options = children;
},
};
const selectMaDaoProviderId = {
value: 'Provider Live!',
options: [],
replaceChildren(...children) {
this.options = children;
},
};
const selectMaDaoCountry = {
value: 'local',
options: [],
replaceChildren(...children) {
this.options = children;
},
};
const selectMaDaoOperator = {
value: 'Operator Live!',
options: [],
replaceChildren(...children) {
this.options = children;
},
};
const inputMaDaoAutoPickCountry = { checked: false }; const inputMaDaoAutoPickCountry = { checked: false };
const inputMaDaoReusePhone = { checked: false }; const inputMaDaoReusePhone = { checked: false };
const inputMaDaoMinPrice = { value: '0.02' }; const inputMaDaoMinPrice = { value: '0.02' };
@@ -1454,6 +1723,10 @@ let heroSmsCountrySelectionOrder = [];
let phoneSmsProviderOrderSelection = ['hero-sms', '5sim']; let phoneSmsProviderOrderSelection = ['hero-sms', '5sim'];
let lastPhoneSmsProviderBeforeChange = null; let lastPhoneSmsProviderBeforeChange = null;
let savedPayload = null; let savedPayload = null;
let maDaoRoutingPlanOptions = [];
let maDaoProviderOptions = [];
let maDaoCountryOptions = [];
let maDaoOperatorOptions = [];
${extractFunction('normalizePhoneSmsProvider')} ${extractFunction('normalizePhoneSmsProvider')}
${extractFunction('normalizePhoneSmsProviderValue')} ${extractFunction('normalizePhoneSmsProviderValue')}
@@ -1483,9 +1756,21 @@ ${extractFunction('normalizeNexSmsServiceCodeValue')}
${extractFunction('normalizeMaDaoBaseUrlValue')} ${extractFunction('normalizeMaDaoBaseUrlValue')}
${extractFunction('normalizeMaDaoModeValue')} ${extractFunction('normalizeMaDaoModeValue')}
${extractFunction('normalizeMaDaoIdentifierValue')} ${extractFunction('normalizeMaDaoIdentifierValue')}
${extractFunction('normalizeMaDaoRoutingPlanIdValue')}
${extractFunction('normalizeMaDaoProviderIdValue')} ${extractFunction('normalizeMaDaoProviderIdValue')}
${extractFunction('normalizeMaDaoOperatorValue')}
${extractFunction('normalizeMaDaoCountry')} ${extractFunction('normalizeMaDaoCountry')}
${extractFunction('normalizeMaDaoPriceValue')} ${extractFunction('normalizeMaDaoPriceValue')}
${extractFunction('formatMaDaoCountryDisplayLabel')}
${extractFunction('createSelectOptionElement')}
${extractFunction('setSelectOptions')}
${extractFunction('normalizeMaDaoOptionListItems')}
${extractFunction('resolveMaDaoOptionSelectedValue')}
${extractFunction('buildMaDaoRoutingPlanOptions')}
${extractFunction('setMaDaoRoutingPlanSelectOptions')}
${extractFunction('setMaDaoProviderSelectOptions')}
${extractFunction('setMaDaoCountrySelectOptions')}
${extractFunction('setMaDaoOperatorSelectOptions')}
${extractFunction('normalizePhoneSmsMinPriceValue')} ${extractFunction('normalizePhoneSmsMinPriceValue')}
${extractFunction('normalizePhoneSmsMaxPriceValue')} ${extractFunction('normalizePhoneSmsMaxPriceValue')}
${extractFunction('normalizeHeroSmsCountryId')} ${extractFunction('normalizeHeroSmsCountryId')}
@@ -1508,6 +1793,7 @@ function syncLatestState(patch) { latestState = { ...latestState, ...patch }; }
function loadHeroSmsCountries() { return Promise.resolve(); } function loadHeroSmsCountries() { return Promise.resolve(); }
function loadFiveSimCountries() { return Promise.resolve(); } function loadFiveSimCountries() { return Promise.resolve(); }
function loadNexSmsCountries() { return Promise.resolve(); } function loadNexSmsCountries() { return Promise.resolve(); }
function loadMaDaoProviders() { return Promise.resolve(); }
function applyHeroSmsFallbackSelection() {} function applyHeroSmsFallbackSelection() {}
function applyFiveSimCountrySelection() {} function applyFiveSimCountrySelection() {}
function applyNexSmsCountrySelection() {} function applyNexSmsCountrySelection() {}
@@ -1564,6 +1850,71 @@ return {
assert.equal(api.savedPayload.fiveSimMinPrice, '0.88'); assert.equal(api.savedPayload.fiveSimMinPrice, '0.88');
}); });
test('buildPhoneSmsProviderStatePatch preserves MaDao hidden reuse defaults when controls are absent', () => {
const api = new Function(`
let latestState = {
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822',
madaoHttpSecret: 'secret-old',
madaoMode: 'direct',
madaoRoutingPlanId: 'plan-old',
madaoProviderId: 'provider-old',
madaoCountry: 'TH',
madaoOperator: 'any',
madaoAutoPickCountry: true,
madaoReusePhone: true,
madaoMinPrice: '0.01',
madaoMaxPrice: '0.09',
};
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_HERO = PHONE_SMS_PROVIDER_HERO_SMS;
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const PHONE_SMS_PROVIDER_MADAO = 'madao';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
const MADAO_MODE_ROUTING_PLAN = 'routing_plan';
const MADAO_MODE_DIRECT = 'direct';
const DEFAULT_MADAO_MODE = MADAO_MODE_ROUTING_PLAN;
const selectMaDaoMode = { value: 'direct' };
const selectMaDaoRoutingPlanId = { value: 'plan-live' };
const selectMaDaoProviderId = { value: 'provider-live' };
const selectMaDaoCountry = { value: 'GB' };
const selectMaDaoOperator = { value: 'Any operator' };
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/acquire' };
const inputMaDaoHttpSecret = { value: 'secret-live' };
const inputMaDaoMinPrice = { value: '0.02' };
const inputMaDaoMaxPrice = { value: '0.20' };
${extractFunction('normalizePhoneSmsProvider')}
${extractFunction('normalizeMaDaoBaseUrlValue')}
${extractFunction('normalizeMaDaoModeValue')}
${extractFunction('normalizeMaDaoIdentifierValue')}
${extractFunction('normalizeMaDaoRoutingPlanIdValue')}
${extractFunction('normalizeMaDaoProviderIdValue')}
${extractFunction('normalizeMaDaoOperatorValue')}
${extractFunction('normalizeMaDaoCountry')}
${extractFunction('normalizeMaDaoPriceValue')}
${extractFunction('normalizePhoneSmsMinPriceValue')}
${extractFunction('normalizePhoneSmsMaxPriceValue')}
${extractFunction('buildPhoneSmsProviderStatePatch')}
return { buildPhoneSmsProviderStatePatch };
`)();
const patch = api.buildPhoneSmsProviderStatePatch('madao');
assert.equal(patch.madaoAutoPickCountry, true);
assert.equal(patch.madaoReusePhone, true);
assert.equal(patch.madaoOperator, '');
assert.equal(patch.madaoProviderId, 'provider-live');
assert.equal(patch.madaoCountry, 'GB');
});
test('HeroSMS operator helpers normalize keyed operator payloads', () => { test('HeroSMS operator helpers normalize keyed operator payloads', () => {
const api = new Function(` const api = new Function(`
const DEFAULT_HERO_SMS_OPERATOR = 'any'; const DEFAULT_HERO_SMS_OPERATOR = 'any';