feat: add MaDao phone SMS provider

This commit is contained in:
QLHazyCoder
2026-05-29 00:44:30 +08:00
parent 4b739d91b5
commit 9ef9cb4c46
15 changed files with 3291 additions and 311 deletions
+87
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/madao.js',
'phone-sms/providers/registry.js', 'phone-sms/providers/registry.js',
'background/phone-verification-flow.js', 'background/phone-verification-flow.js',
'background/account-run-history.js', 'background/account-run-history.js',
@@ -699,11 +700,13 @@ const PHONE_SMS_PROVIDER_5SIM = '5sim';
const PHONE_SMS_PROVIDER_HERO_SMS = PHONE_SMS_PROVIDER_HERO; const PHONE_SMS_PROVIDER_HERO_SMS = PHONE_SMS_PROVIDER_HERO;
const PHONE_SMS_PROVIDER_FIVE_SIM = PHONE_SMS_PROVIDER_5SIM; const PHONE_SMS_PROVIDER_FIVE_SIM = PHONE_SMS_PROVIDER_5SIM;
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms'; const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const PHONE_SMS_PROVIDER_MADAO = 'madao';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO; const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO;
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = Object.freeze([ const DEFAULT_PHONE_SMS_PROVIDER_ORDER = Object.freeze([
PHONE_SMS_PROVIDER_HERO, PHONE_SMS_PROVIDER_HERO,
PHONE_SMS_PROVIDER_5SIM, PHONE_SMS_PROVIDER_5SIM,
PHONE_SMS_PROVIDER_NEXSMS, PHONE_SMS_PROVIDER_NEXSMS,
PHONE_SMS_PROVIDER_MADAO,
]); ]);
const DEFAULT_FIVE_SIM_BASE_URL = 'https://5sim.net/v1'; const DEFAULT_FIVE_SIM_BASE_URL = 'https://5sim.net/v1';
const DEFAULT_FIVE_SIM_PRODUCT = 'openai'; const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
@@ -712,6 +715,8 @@ const DEFAULT_FIVE_SIM_COUNTRY_ORDER = Object.freeze(['thailand']);
const DEFAULT_NEX_SMS_BASE_URL = 'https://api.nexsms.net'; const DEFAULT_NEX_SMS_BASE_URL = 'https://api.nexsms.net';
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot'; const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
const DEFAULT_NEX_SMS_COUNTRY_ORDER = Object.freeze([1]); const DEFAULT_NEX_SMS_COUNTRY_ORDER = Object.freeze([1]);
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
const DEFAULT_MADAO_MODE = 'routing_plan';
const DEFAULT_HERO_SMS_REUSE_ENABLED = true; const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country'; const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price'; const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
@@ -1477,6 +1482,16 @@ const PERSISTED_SETTING_DEFAULTS = {
nexSmsApiKey: '', nexSmsApiKey: '',
nexSmsCountryOrder: [...DEFAULT_NEX_SMS_COUNTRY_ORDER], nexSmsCountryOrder: [...DEFAULT_NEX_SMS_COUNTRY_ORDER],
nexSmsServiceCode: DEFAULT_NEX_SMS_SERVICE_CODE, nexSmsServiceCode: DEFAULT_NEX_SMS_SERVICE_CODE,
madaoBaseUrl: DEFAULT_MADAO_BASE_URL,
madaoHttpSecret: '',
madaoMode: DEFAULT_MADAO_MODE,
madaoRoutingPlanId: '',
madaoProviderId: '',
madaoCountry: '',
madaoAutoPickCountry: true,
madaoReusePhone: true,
madaoMinPrice: '',
madaoMaxPrice: '',
phonePreferredActivation: null, phonePreferredActivation: null,
}; };
@@ -1864,6 +1879,9 @@ function normalizePhoneSmsProvider(value = '') {
if (normalized === PHONE_SMS_PROVIDER_NEXSMS) { if (normalized === PHONE_SMS_PROVIDER_NEXSMS) {
return PHONE_SMS_PROVIDER_NEXSMS; return PHONE_SMS_PROVIDER_NEXSMS;
} }
if (normalized === PHONE_SMS_PROVIDER_MADAO) {
return PHONE_SMS_PROVIDER_MADAO;
}
return PHONE_SMS_PROVIDER_HERO_SMS; return PHONE_SMS_PROVIDER_HERO_SMS;
} }
function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) { function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) {
@@ -2212,6 +2230,56 @@ function normalizeNexSmsServiceCode(value = '', fallback = DEFAULT_NEX_SMS_SERVI
return fallbackNormalized || DEFAULT_NEX_SMS_SERVICE_CODE; return fallbackNormalized || DEFAULT_NEX_SMS_SERVICE_CODE;
} }
function normalizeMaDaoBaseUrl(value = '') {
const normalized = normalizeLocalHttpBaseUrl(value, DEFAULT_MADAO_BASE_URL);
try {
const parsed = new URL(normalized);
parsed.pathname = parsed.pathname.replace(
/\/api\/(?:acquire|poll|release|routing\/replace)$/i,
''
);
parsed.search = '';
parsed.hash = '';
return parsed.toString().replace(/\/$/, '');
} catch {
return DEFAULT_MADAO_BASE_URL;
}
}
function normalizeMaDaoMode(value = '') {
return String(value || '').trim().toLowerCase() === 'direct' ? 'direct' : DEFAULT_MADAO_MODE;
}
function normalizeMaDaoIdentifier(value = '') {
return String(value || '').trim();
}
function normalizeMaDaoProviderId(value = '') {
return String(value || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9_-]+/g, '');
}
function normalizeMaDaoCountry(value = '') {
const trimmed = String(value || '').trim();
if (!trimmed) {
return '';
}
const lowered = trimmed.toLowerCase();
if (lowered === 'any' || lowered === 'local') {
return lowered;
}
if (/^[a-z]{2}$/i.test(trimmed)) {
return trimmed.toUpperCase();
}
return lowered.replace(/[^a-z0-9_-]+/g, '');
}
function normalizeMaDaoPrice(value = '') {
return normalizeHeroSmsMaxPrice(value);
}
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;
@@ -3606,6 +3674,24 @@ function normalizePersistentSettingValue(key, value) {
return normalizeNexSmsCountryOrder(value); return normalizeNexSmsCountryOrder(value);
case 'nexSmsServiceCode': case 'nexSmsServiceCode':
return normalizeNexSmsServiceCode(value); return normalizeNexSmsServiceCode(value);
case 'madaoBaseUrl':
return normalizeMaDaoBaseUrl(value);
case 'madaoHttpSecret':
return String(value || '');
case 'madaoMode':
return normalizeMaDaoMode(value);
case 'madaoRoutingPlanId':
return normalizeMaDaoIdentifier(value);
case 'madaoProviderId':
return normalizeMaDaoProviderId(value);
case 'madaoCountry':
return normalizeMaDaoCountry(value);
case 'madaoAutoPickCountry':
case 'madaoReusePhone':
return Boolean(value);
case 'madaoMinPrice':
case 'madaoMaxPrice':
return normalizeMaDaoPrice(value);
case 'phonePreferredActivation': case 'phonePreferredActivation':
return normalizePhonePreferredActivation(value); return normalizePhonePreferredActivation(value);
default: default:
@@ -13878,6 +13964,7 @@ const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.crea
sleepWithStop, sleepWithStop,
throwIfStopped, throwIfStopped,
createFiveSimProvider: self.PhoneSmsFiveSimProvider?.createProvider, createFiveSimProvider: self.PhoneSmsFiveSimProvider?.createProvider,
createMaDaoProvider: self.PhoneSmsMaDaoProvider?.createProvider,
}); });
const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({ const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({
addLog, addLog,
+301 -28
View File
@@ -30,6 +30,7 @@
DEFAULT_NEX_SMS_SERVICE_CODE = 'ot', DEFAULT_NEX_SMS_SERVICE_CODE = 'ot',
DEFAULT_HERO_SMS_REUSE_ENABLED = true, DEFAULT_HERO_SMS_REUSE_ENABLED = true,
createFiveSimProvider = null, createFiveSimProvider = null,
createMaDaoProvider = null,
HERO_SMS_COUNTRY_ID = 52, HERO_SMS_COUNTRY_ID = 52,
HERO_SMS_COUNTRY_LABEL = 'Thailand', HERO_SMS_COUNTRY_LABEL = 'Thailand',
HERO_SMS_SERVICE_CODE = 'dr', HERO_SMS_SERVICE_CODE = 'dr',
@@ -84,11 +85,13 @@
const PHONE_SMS_PROVIDER_HERO_SMS = PHONE_SMS_PROVIDER_HERO; const PHONE_SMS_PROVIDER_HERO_SMS = PHONE_SMS_PROVIDER_HERO;
const PHONE_SMS_PROVIDER_FIVE_SIM = PHONE_SMS_PROVIDER_5SIM; const PHONE_SMS_PROVIDER_FIVE_SIM = PHONE_SMS_PROVIDER_5SIM;
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms'; const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const PHONE_SMS_PROVIDER_MADAO = 'madao';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO; const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO;
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = Object.freeze([ const DEFAULT_PHONE_SMS_PROVIDER_ORDER = Object.freeze([
PHONE_SMS_PROVIDER_HERO, PHONE_SMS_PROVIDER_HERO,
PHONE_SMS_PROVIDER_5SIM, PHONE_SMS_PROVIDER_5SIM,
PHONE_SMS_PROVIDER_NEXSMS, PHONE_SMS_PROVIDER_NEXSMS,
PHONE_SMS_PROVIDER_MADAO,
]); ]);
const MAX_PHONE_REUSABLE_POOL = 12; const MAX_PHONE_REUSABLE_POOL = 12;
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::'; const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
@@ -192,6 +195,9 @@
if (normalized === PHONE_SMS_PROVIDER_NEXSMS) { if (normalized === PHONE_SMS_PROVIDER_NEXSMS) {
return PHONE_SMS_PROVIDER_NEXSMS; return PHONE_SMS_PROVIDER_NEXSMS;
} }
if (normalized === PHONE_SMS_PROVIDER_MADAO) {
return PHONE_SMS_PROVIDER_MADAO;
}
return PHONE_SMS_PROVIDER_HERO; return PHONE_SMS_PROVIDER_HERO;
} }
function isFiveSimProvider(state = {}) { function isFiveSimProvider(state = {}) {
@@ -489,6 +495,25 @@
return /phone_max_usage_exceeded|phone_number_in_use|already\s+linked\s+to\s+the\s+maximum\s+number\s+of\s+accounts|phone\s+number\s+is\s+already\s+(?:in\s+use|linked|registered)|phone\s+number\s+has\s+already\s+been\s+used|already\s+associated\s+with\s+another\s+account|not\s+eligible\s+to\s+be\s+used|cannot\s+be\s+used\s+for\s+verification|号码.*(?:已|被).*(?:使用|占用|绑定|注册)|手机号.*(?:已|被).*(?:使用|占用|绑定|注册)|该手机号.*(?:已|被).*(?:使用|占用|绑定|注册)/i.test(text); return /phone_max_usage_exceeded|phone_number_in_use|already\s+linked\s+to\s+the\s+maximum\s+number\s+of\s+accounts|phone\s+number\s+is\s+already\s+(?:in\s+use|linked|registered)|phone\s+number\s+has\s+already\s+been\s+used|already\s+associated\s+with\s+another\s+account|not\s+eligible\s+to\s+be\s+used|cannot\s+be\s+used\s+for\s+verification|号码.*(?:已|被).*(?:使用|占用|绑定|注册)|手机号.*(?:已|被).*(?:使用|占用|绑定|注册)|该手机号.*(?:已|被).*(?:使用|占用|绑定|注册)/i.test(text);
} }
function isPhoneNumberUsedFailureReason(value) {
const normalized = String(value || '').trim().toLowerCase();
if (!normalized) {
return false;
}
return normalized === 'phone_number_used'
|| normalized === 'phone_number_in_use'
|| normalized === 'phone_max_usage_exceeded'
|| isPhoneNumberUsedError(normalized);
}
function getPhoneReplacementReleaseAction(reason = '') {
const normalized = String(reason || '').trim().toLowerCase();
if (normalized === 'code_rejected' || isPhoneNumberUsedFailureReason(normalized)) {
return 'ban';
}
return 'cancel';
}
function isPhoneNumberInvalidError(value) { function isPhoneNumberInvalidError(value) {
const text = String(value || '').trim(); const text = String(value || '').trim();
if (!text) { if (!text) {
@@ -680,7 +705,7 @@
}); });
if (normalized.length) { if (normalized.length) {
return normalized.slice(0, 3); return normalized.slice(0, DEFAULT_PHONE_SMS_PROVIDER_ORDER.length);
} }
const fallback = Array.isArray(fallbackOrder) ? fallbackOrder : []; const fallback = Array.isArray(fallbackOrder) ? fallbackOrder : [];
@@ -696,7 +721,7 @@
fallbackNormalized.push(provider); fallbackNormalized.push(provider);
}); });
return fallbackNormalized.slice(0, 3); return fallbackNormalized.slice(0, DEFAULT_PHONE_SMS_PROVIDER_ORDER.length);
} }
function resolvePhoneProviderOrder(state = {}, preferredProvider = '') { function resolvePhoneProviderOrder(state = {}, preferredProvider = '') {
const currentProvider = normalizePhoneSmsProvider( const currentProvider = normalizePhoneSmsProvider(
@@ -723,7 +748,7 @@
return fallbackOrder; return fallbackOrder;
} }
const withoutCurrent = fallbackOrder.filter((provider) => provider !== currentProvider); const withoutCurrent = fallbackOrder.filter((provider) => provider !== currentProvider);
return [currentProvider, ...withoutCurrent].slice(0, 3); return [currentProvider, ...withoutCurrent].slice(0, DEFAULT_PHONE_SMS_PROVIDER_ORDER.length);
} }
function reorderPriceCandidates(prices = [], acquirePriority = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY, preferredPrice = null) { function reorderPriceCandidates(prices = [], acquirePriority = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY, preferredPrice = null) {
@@ -962,6 +987,9 @@
if (provider === PHONE_SMS_PROVIDER_NEXSMS) { if (provider === PHONE_SMS_PROVIDER_NEXSMS) {
return 'NexSMS'; return 'NexSMS';
} }
if (provider === PHONE_SMS_PROVIDER_MADAO) {
return 'MaDao';
}
return 'HeroSMS'; return 'HeroSMS';
} }
@@ -1200,6 +1228,25 @@
return createResolvedFiveSimProvider(); return createResolvedFiveSimProvider();
} }
function createResolvedMaDaoProvider() {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
const factory = createMaDaoProvider || rootScope.PhoneSmsMaDaoProvider?.createProvider;
if (typeof factory !== 'function') {
return null;
}
return factory({
addLog,
fetchImpl,
requestTimeoutMs: DEFAULT_PHONE_REQUEST_TIMEOUT_MS,
sleepWithStop,
throwIfStopped,
});
}
function getMaDaoAdapterForState(_state = {}) {
return createResolvedMaDaoProvider();
}
function normalizeFiveSimCountryId(value, fallback = 'england') { function normalizeFiveSimCountryId(value, fallback = 'england') {
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
return normalized || fallback; return normalized || fallback;
@@ -1362,14 +1409,18 @@
const rawProvider = String(record.provider || '').trim(); const rawProvider = String(record.provider || '').trim();
const provider = normalizePhoneSmsProvider(rawProvider); const provider = normalizePhoneSmsProvider(rawProvider);
const rawCountryId = record.countryId ?? record.country; const rawCountryId = record.countryId ?? record.country;
const fallbackCountryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM ? 'england' : HERO_SMS_COUNTRY_ID; const fallbackCountryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM
? 'england'
: (provider === PHONE_SMS_PROVIDER_MADAO ? '' : HERO_SMS_COUNTRY_ID);
const expiresAt = normalizeTimestampMs(record.expiresAt); const expiresAt = normalizeTimestampMs(record.expiresAt);
const serviceCode = String( const serviceCode = String(
record.serviceCode record.serviceCode
|| ( || (
provider === PHONE_SMS_PROVIDER_FIVE_SIM provider === PHONE_SMS_PROVIDER_FIVE_SIM
? DEFAULT_FIVE_SIM_PRODUCT ? DEFAULT_FIVE_SIM_PRODUCT
: (provider === PHONE_SMS_PROVIDER_NEXSMS ? DEFAULT_NEX_SMS_SERVICE_CODE : HERO_SMS_SERVICE_CODE) : (provider === PHONE_SMS_PROVIDER_NEXSMS
? DEFAULT_NEX_SMS_SERVICE_CODE
: (provider === PHONE_SMS_PROVIDER_MADAO ? 'openai' : HERO_SMS_SERVICE_CODE))
) )
).trim(); ).trim();
const countryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM const countryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM
@@ -1377,7 +1428,11 @@
: ( : (
provider === PHONE_SMS_PROVIDER_NEXSMS provider === PHONE_SMS_PROVIDER_NEXSMS
? normalizeNexSmsCountryId(rawCountryId, 0) ? normalizeNexSmsCountryId(rawCountryId, 0)
: normalizeCountryId(rawCountryId, fallbackCountryId) : (
provider === PHONE_SMS_PROVIDER_MADAO
? String(rawCountryId || '').trim()
: normalizeCountryId(rawCountryId, fallbackCountryId)
)
); );
const ignoredPhoneCodeKeys = normalizeStringList(record.ignoredPhoneCodeKeys); const ignoredPhoneCodeKeys = normalizeStringList(record.ignoredPhoneCodeKeys);
return { return {
@@ -1396,6 +1451,15 @@
...(record.phoneCodeReceived ? { phoneCodeReceived: true } : {}), ...(record.phoneCodeReceived ? { phoneCodeReceived: true } : {}),
...(record.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(record.phoneCodeReceivedAt) || 0) } : {}), ...(record.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(record.phoneCodeReceivedAt) || 0) } : {}),
...(ignoredPhoneCodeKeys.length ? { ignoredPhoneCodeKeys } : {}), ...(ignoredPhoneCodeKeys.length ? { ignoredPhoneCodeKeys } : {}),
...(provider === PHONE_SMS_PROVIDER_MADAO ? {
...(record.madaoProviderId ? { madaoProviderId: String(record.madaoProviderId || '').trim() } : {}),
...(record.madaoRoutingPlanId ? { madaoRoutingPlanId: String(record.madaoRoutingPlanId || '').trim() } : {}),
...(record.madaoRoutingPlanName ? { madaoRoutingPlanName: String(record.madaoRoutingPlanName || '').trim() } : {}),
...(record.madaoRoutingItemId ? { madaoRoutingItemId: String(record.madaoRoutingItemId || '').trim() } : {}),
...(record.madaoAcquirePath ? { madaoAcquirePath: String(record.madaoAcquirePath || '').trim() } : {}),
...(record.madaoStatus ? { madaoStatus: String(record.madaoStatus || '').trim() } : {}),
...(record.madaoPrice !== undefined && record.madaoPrice !== null ? { madaoPrice: Number(record.madaoPrice) } : {}),
} : {}),
}; };
} }
@@ -1570,7 +1634,7 @@
const rawCountryId = record.countryId ?? record.country; const rawCountryId = record.countryId ?? record.country;
const countryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM const countryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM
? normalizeFiveSimCountryId(rawCountryId, '') ? normalizeFiveSimCountryId(rawCountryId, '')
: Math.floor(Number(rawCountryId)); : (provider === PHONE_SMS_PROVIDER_MADAO ? String(rawCountryId || '').trim() : Math.floor(Number(rawCountryId)));
const countryLabel = String(record.countryLabel || '').trim(); const countryLabel = String(record.countryLabel || '').trim();
const statusAction = String(record.statusAction || '').trim(); const statusAction = String(record.statusAction || '').trim();
@@ -1584,6 +1648,10 @@
if (countryId) { if (countryId) {
fallback.countryId = countryId; fallback.countryId = countryId;
} }
} else if (provider === PHONE_SMS_PROVIDER_MADAO) {
if (countryId) {
fallback.countryId = countryId;
}
} else if (Number.isFinite(countryId) && countryId > 0) { } else if (Number.isFinite(countryId) && countryId > 0) {
fallback.countryId = countryId; fallback.countryId = countryId;
if (provider === PHONE_SMS_PROVIDER_5SIM) { if (provider === PHONE_SMS_PROVIDER_5SIM) {
@@ -2119,6 +2187,13 @@
}; };
} }
if (provider === PHONE_SMS_PROVIDER_MADAO) {
return {
provider,
baseUrl: normalizeUrl(state.madaoBaseUrl, 'http://127.0.0.1:7822').replace(/\/+$/, ''),
};
}
const apiKey = normalizeApiKey(state.heroSmsApiKey); const apiKey = normalizeApiKey(state.heroSmsApiKey);
if (!apiKey) { if (!apiKey) {
throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。');
@@ -2150,6 +2225,16 @@
'phoneSmsProvider', 'phoneSmsProvider',
'phoneSmsProviderOrder', 'phoneSmsProviderOrder',
'phoneSmsReuseEnabled', 'phoneSmsReuseEnabled',
'madaoBaseUrl',
'madaoHttpSecret',
'madaoMode',
'madaoRoutingPlanId',
'madaoProviderId',
'madaoCountry',
'madaoAutoPickCountry',
'madaoReusePhone',
'madaoMinPrice',
'madaoMaxPrice',
'heroSmsApiKey', 'heroSmsApiKey',
'heroSmsBaseUrl', 'heroSmsBaseUrl',
'heroSmsCountryId', 'heroSmsCountryId',
@@ -3691,6 +3776,13 @@
state = await mergeLatestPhoneSettingsState(state, { state = await mergeLatestPhoneSettingsState(state, {
preservePhoneSmsProvider: Boolean(options?.preservePhoneSmsProvider), preservePhoneSmsProvider: Boolean(options?.preservePhoneSmsProvider),
}); });
if (normalizePhoneSmsProvider(state?.phoneSmsProvider) === PHONE_SMS_PROVIDER_MADAO) {
const provider = getMaDaoAdapterForState(state);
if (!provider || typeof provider.acquireActivation !== 'function') {
throw new Error('MaDao 接码平台模块未加载。');
}
return provider.acquireActivation(state, options);
}
if (normalizePhoneSmsProvider(state?.phoneSmsProvider) === PHONE_SMS_PROVIDER_FIVE_SIM) { if (normalizePhoneSmsProvider(state?.phoneSmsProvider) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state); const provider = getFiveSimProviderForState(state);
if (provider) { if (provider) {
@@ -4041,6 +4133,9 @@
return provider.reuseActivation(scopeStateToActivationProvider(state, normalizedActivation), normalizedActivation); return provider.reuseActivation(scopeStateToActivationProvider(state, normalizedActivation), normalizedActivation);
} }
} }
if (getActivationProviderId(normalizedActivation, state) === PHONE_SMS_PROVIDER_MADAO) {
return normalizedActivation;
}
const scopedState = scopeStateToActivationProvider(state, normalizedActivation); const scopedState = scopeStateToActivationProvider(state, normalizedActivation);
const config = resolvePhoneConfig(scopedState); const config = resolvePhoneConfig(scopedState);
@@ -4141,6 +4236,13 @@
return; return;
} }
} }
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_MADAO) {
const provider = getMaDaoAdapterForState(state);
if (provider?.releaseActivation) {
await provider.releaseActivation(scopeStateToActivationProvider(state, activation), activation, 'finish');
}
return;
}
await setPhoneActivationStatus(state, activation, 6, 'HeroSMS setStatus(6)'); await setPhoneActivationStatus(state, activation, 6, 'HeroSMS setStatus(6)');
} }
@@ -4162,6 +4264,13 @@
return; return;
} }
} }
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_MADAO) {
const provider = getMaDaoAdapterForState(state);
if (provider?.releaseActivation) {
await provider.releaseActivation(scopeStateToActivationProvider(state, activation), activation, 'cancel');
}
return;
}
await setPhoneActivationStatus(state, activation, 8, 'HeroSMS setStatus(8)'); await setPhoneActivationStatus(state, activation, 8, 'HeroSMS setStatus(8)');
} catch (_) { } catch (_) {
// Best-effort cleanup. // Best-effort cleanup.
@@ -4263,6 +4372,13 @@
return; return;
} }
} }
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_MADAO) {
const provider = getMaDaoAdapterForState(state);
if (provider?.releaseActivation) {
await provider.releaseActivation(scopeStateToActivationProvider(state, activation), activation, 'ban');
}
return;
}
await setPhoneActivationStatus(state, activation, 8, 'HeroSMS setStatus(8)'); await setPhoneActivationStatus(state, activation, 8, 'HeroSMS setStatus(8)');
} catch (_) { } catch (_) {
// Best-effort cleanup. // Best-effort cleanup.
@@ -4492,6 +4608,53 @@
} }
}; };
if (config.provider === PHONE_SMS_PROVIDER_MADAO) {
const provider = getMaDaoAdapterForState(scopedState);
if (!provider || typeof provider.pollActivation !== 'function') {
throw new Error('MaDao 接码平台模块未加载。');
}
while (Date.now() - start < timeoutMs) {
if (maxRounds > 0 && pollCount >= maxRounds) {
break;
}
throwIfStopped();
const payload = await provider.pollActivation(scopedState, normalizedActivation);
const code = typeof provider.extractCodeFromPollPayload === 'function'
? provider.extractCodeFromPollPayload(payload)
: extractVerificationCode(payload?.code || payload?.sms_code || payload?.message || payload?.text || '');
const statusText = String(
payload?.status
|| payload?.madaoStatus
|| payload?.message
|| payload?.text
|| 'PENDING'
).trim();
lastResponse = statusText;
pollCount += 1;
if (typeof options.onStatus === 'function') {
await options.onStatus({
activation: normalizedActivation,
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}`);
}
await emitWaitingForCode(statusText || 'PENDING');
await sleepWithStop(intervalMs);
}
throw buildPhoneCodeTimeoutError(lastResponse);
}
if (config.provider === PHONE_SMS_PROVIDER_5SIM) { if (config.provider === PHONE_SMS_PROVIDER_5SIM) {
while (Date.now() - start < timeoutMs) { while (Date.now() - start < timeoutMs) {
if (maxRounds > 0 && pollCount >= maxRounds) { if (maxRounds > 0 && pollCount >= maxRounds) {
@@ -4733,6 +4896,9 @@
if (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_NEXSMS) { if (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_NEXSMS) {
return resolveNexSmsCountryCandidates(state); return resolveNexSmsCountryCandidates(state);
} }
if (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_MADAO) {
return [];
}
return resolveCountryCandidates(state); return resolveCountryCandidates(state);
} }
@@ -4750,6 +4916,14 @@
label: normalizeFiveSimCountryLabel(activation.countryLabel, countryId), label: normalizeFiveSimCountryLabel(activation.countryLabel, countryId),
}; };
} }
} else if (providerId === PHONE_SMS_PROVIDER_MADAO) {
const countryId = String(activation.countryId || '').trim();
if (countryId) {
return {
id: countryId,
label: normalizeCountryLabel(activation.countryLabel, countryId),
};
}
} else { } else {
const inferredCountry = inferHeroSmsCountryFromPhoneNumber(activation.phoneNumber); const inferredCountry = inferHeroSmsCountryFromPhoneNumber(activation.phoneNumber);
const rawCountryId = normalizeCountryId(activation.countryId, 0); const rawCountryId = normalizeCountryId(activation.countryId, 0);
@@ -4775,7 +4949,9 @@
} }
return candidates[0] || (providerId === PHONE_SMS_PROVIDER_FIVE_SIM return candidates[0] || (providerId === PHONE_SMS_PROVIDER_FIVE_SIM
? { id: 'england', label: 'England' } ? { id: 'england', label: 'England' }
: resolveCountryConfig(fallbackState)); : (providerId === PHONE_SMS_PROVIDER_MADAO
? { id: '', label: '' }
: resolveCountryConfig(fallbackState)));
} }
async function submitPhoneNumber(tabId, phoneNumber, activation = null) { async function submitPhoneNumber(tabId, phoneNumber, activation = null) {
@@ -6509,6 +6685,10 @@
const normalizedCountryId = normalizeNexSmsCountryId(countryId, -1); const normalizedCountryId = normalizeNexSmsCountryId(countryId, -1);
return normalizedCountryId >= 0 ? `${normalizedProvider}:${normalizedCountryId}` : ''; return normalizedCountryId >= 0 ? `${normalizedProvider}:${normalizedCountryId}` : '';
} }
if (normalizedProvider === PHONE_SMS_PROVIDER_MADAO) {
const normalizedCountryId = String(countryId || '').trim();
return normalizedCountryId ? `${normalizedProvider}:${normalizedCountryId}` : '';
}
const normalizedCountryId = normalizeCountryId(countryId, 0); const normalizedCountryId = normalizeCountryId(countryId, 0);
return normalizedCountryId > 0 ? `${normalizedProvider}:${normalizedCountryId}` : ''; return normalizedCountryId > 0 ? `${normalizedProvider}:${normalizedCountryId}` : '';
}; };
@@ -6549,6 +6729,9 @@
.find((entry) => normalizeNexSmsCountryId(entry.id, -1) === normalizedCountryId); .find((entry) => normalizeNexSmsCountryId(entry.id, -1) === normalizedCountryId);
return matched?.label || `Country #${normalizedCountryId}`; return matched?.label || `Country #${normalizedCountryId}`;
} }
if (normalizedProvider === PHONE_SMS_PROVIDER_MADAO) {
return normalizedCountryKey || 'MaDao';
}
const normalizedCountryId = normalizeCountryId(normalizedCountryKey, 0); const normalizedCountryId = normalizeCountryId(normalizedCountryKey, 0);
const matched = resolveCountryCandidates(state) const matched = resolveCountryCandidates(state)
.find((entry) => normalizeCountryId(entry.id, 0) === normalizedCountryId); .find((entry) => normalizeCountryId(entry.id, 0) === normalizedCountryId);
@@ -6651,7 +6834,9 @@
const getCountryFailureKey = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => ( const getCountryFailureKey = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => (
normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_FIVE_SIM normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_FIVE_SIM
? normalizeFiveSimCountryId(countryId, '') ? normalizeFiveSimCountryId(countryId, '')
: String(normalizeCountryId(countryId, 0) || '') : (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_MADAO
? String(countryId || '').trim()
: String(normalizeCountryId(countryId, 0) || ''))
); );
const getCountryFailureCount = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => { const getCountryFailureCount = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => {
@@ -6778,6 +6963,37 @@
); );
}; };
const rotateCurrentActivation = async (reason = '', releaseAction = 'cancel') => {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation || getActivationProviderId(normalizedActivation, state) !== PHONE_SMS_PROVIDER_MADAO) {
return { handled: false, nextActivation: null };
}
const provider = getMaDaoAdapterForState(state);
if (!provider || typeof provider.rotateActivation !== 'function') {
return { handled: false, nextActivation: null };
}
const rotated = await provider.rotateActivation(
scopeStateToActivationProvider(state, normalizedActivation),
normalizedActivation,
{
releaseAction,
reason,
}
);
const nextActivation = normalizeActivation(rotated?.nextActivation);
if (!nextActivation) {
return { handled: true, nextActivation: null };
}
activation = nextActivation;
shouldCancelActivation = true;
await persistCurrentActivation(nextActivation);
await addLog(
`步骤 9:MaDao 已通过路由替换切换到新号码 ${nextActivation.phoneNumber}`,
'info'
);
return { handled: true, nextActivation };
};
const rotateActivationAfterAddPhoneFailure = async (failureReason, failureCode, submitState = {}) => { const rotateActivationAfterAddPhoneFailure = async (failureReason, failureCode, submitState = {}) => {
await markPreferredActivationExhausted(failureCode || failureReason); await markPreferredActivationExhausted(failureCode || failureReason);
usedNumberReplacementAttempts += 1; usedNumberReplacementAttempts += 1;
@@ -6788,6 +7004,37 @@
`步骤 9:添加手机号失败后正在更换号码(${formatStep9Reason(failureReason)}${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, `步骤 9:添加手机号失败后正在更换号码(${formatStep9Reason(failureReason)}${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`,
'warn' 'warn'
); );
const rotated = shouldCancelActivation && activation
? await rotateCurrentActivation(
failureCode || failureReason,
getPhoneReplacementReleaseAction(failureCode || failureReason)
)
: { handled: false, nextActivation: null };
if (rotated.nextActivation) {
preferReuseExistingActivationOnAddPhone = false;
addPhoneReentryWithSameActivation = 0;
pageState = {
...pageState,
...submitState,
addPhonePage: true,
phoneVerificationPage: false,
};
return;
}
if (rotated.handled) {
await clearCurrentActivation();
activation = null;
shouldCancelActivation = false;
preferReuseExistingActivationOnAddPhone = false;
addPhoneReentryWithSameActivation = 0;
pageState = {
...pageState,
...submitState,
addPhonePage: true,
phoneVerificationPage: false,
};
return;
}
if (shouldCancelActivation && activation) { if (shouldCancelActivation && activation) {
await cancelPhoneActivation(state, activation); await cancelPhoneActivation(state, activation);
} }
@@ -6878,12 +7125,17 @@
`自动白嫖复用号码 ${activation.phoneNumber} 反复返回添加手机号页。` `自动白嫖复用号码 ${activation.phoneNumber} 反复返回添加手机号页。`
); );
} }
if (shouldCancelActivation && activation) { const rotated = shouldCancelActivation && activation
await cancelPhoneActivation(state, activation); ? await rotateCurrentActivation('returned_to_add_phone_loop', 'cancel')
: { handled: false, nextActivation: null };
if (!rotated.nextActivation) {
if (!rotated.handled && shouldCancelActivation && activation) {
await cancelPhoneActivation(state, activation);
}
await clearCurrentActivation();
activation = null;
shouldCancelActivation = false;
} }
await clearCurrentActivation();
activation = null;
shouldCancelActivation = false;
preferReuseExistingActivationOnAddPhone = false; preferReuseExistingActivationOnAddPhone = false;
addPhoneReentryWithSameActivation = 0; addPhoneReentryWithSameActivation = 0;
pageState = { pageState = {
@@ -6938,12 +7190,17 @@
`自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。` `自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。`
); );
} }
if (shouldCancelActivation && activation) { const rotated = shouldCancelActivation && activation
await banPhoneActivation(state, activation); ? await rotateCurrentActivation('phone_number_used', 'ban')
: { handled: false, nextActivation: null };
if (!rotated.nextActivation) {
if (!rotated.handled && shouldCancelActivation && activation) {
await banPhoneActivation(state, activation);
}
await clearCurrentActivation();
activation = null;
shouldCancelActivation = false;
} }
await clearCurrentActivation();
activation = null;
shouldCancelActivation = false;
preferReuseExistingActivationOnAddPhone = false; preferReuseExistingActivationOnAddPhone = false;
addPhoneReentryWithSameActivation = 0; addPhoneReentryWithSameActivation = 0;
pageState = { pageState = {
@@ -7079,7 +7336,11 @@
`自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。` `自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。`
); );
} }
if (shouldCancelActivation && activation) { if (
shouldCancelActivation
&& activation
&& getActivationProviderId(activation, state) !== PHONE_SMS_PROVIDER_MADAO
) {
await banPhoneActivation(state, activation); await banPhoneActivation(state, activation);
shouldCancelActivation = false; shouldCancelActivation = false;
} }
@@ -7093,7 +7354,11 @@
if (attempt >= DEFAULT_PHONE_SUBMIT_ATTEMPTS) { if (attempt >= DEFAULT_PHONE_SUBMIT_ATTEMPTS) {
shouldReplaceNumber = true; shouldReplaceNumber = true;
replaceReason = 'code_rejected'; replaceReason = 'code_rejected';
if (shouldCancelActivation && activation) { if (
shouldCancelActivation
&& activation
&& getActivationProviderId(activation, state) !== PHONE_SMS_PROVIDER_MADAO
) {
await banPhoneActivation(state, activation); await banPhoneActivation(state, activation);
shouldCancelActivation = false; shouldCancelActivation = false;
} }
@@ -7201,24 +7466,32 @@
throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, replaceReason || 'unknown'); throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, replaceReason || 'unknown');
} }
if (shouldCancelActivation && activation) { const rotated = shouldCancelActivation && activation
await cancelPhoneActivation(state, activation); ? await rotateCurrentActivation(
} replaceReason || 'replace_number',
getPhoneReplacementReleaseAction(replaceReason || 'replace_number')
)
: { handled: false, nextActivation: null };
if (shouldRetireFreeReusableActivationOnFailure(await getState(), activation)) { if (shouldRetireFreeReusableActivationOnFailure(await getState(), activation)) {
await retireFreeReusableActivation( await retireFreeReusableActivation(
`自动白嫖复用号码 ${activation.phoneNumber} 在失败后被更换。` `自动白嫖复用号码 ${activation.phoneNumber} 在失败后被更换。`
); );
} }
if (isPhoneNumberUsedError(replaceReason)) { if (isPhoneNumberUsedFailureReason(replaceReason)) {
await discardPhoneActivationFromReuse( await discardPhoneActivationFromReuse(
`目标站拒绝该号码(${replaceReason})。`, `目标站拒绝该号码(${replaceReason})。`,
activation, activation,
await getState() await getState()
); );
} }
await clearCurrentActivation(); if (!rotated.nextActivation) {
activation = null; if (!rotated.handled && shouldCancelActivation && activation) {
shouldCancelActivation = false; await cancelPhoneActivation(state, activation);
}
await clearCurrentActivation();
activation = null;
shouldCancelActivation = false;
}
addPhoneReentryWithSameActivation = 0; addPhoneReentryWithSameActivation = 0;
let returnResult = null; let returnResult = null;
@@ -0,0 +1,559 @@
# OpenAI 手机号接码 MaDao 统一接入与侧边栏样式重构开发方案
## 1. 文档定位
本文用于指导后续重新实现 PR #288 相关能力,并同时收敛侧边栏里文本框、按钮、接码 provider 显隐和无用保存按钮问题。目标不是照搬 PR,而是在现有项目真实结构上重新设计一次,保证 OpenAI 手机号注册、后置手机号验证、provider 接码生命周期、设置持久化和侧边栏 UI 一致落地。
本文只是一份开发方案,不直接修改功能代码。后续实现时应按本文的阶段清单逐段开发、逐段自检,发现现有代码与本文冲突时,先修正方案或明确取舍,再改代码。
## 2. 分析基线
已确认的当前工程基线:
- 当前扩展是 Chrome MV3`manifest.json` 的后台入口是 `background.js`,侧边栏入口是 `sidepanel/sidepanel.html`
- OpenAI flow 定义在 `flows/openai/index.js``flows/openai/workflow.js`,并声明 `supportsPhoneSignup: true``supportsPhoneVerificationSettings: true`
- Kiro/Grok 当前不支持手机号注册和接码设置,`flows/kiro/index.js``flows/grok/index.js` 均为 `supportsPhoneSignup: false``supportsPhoneVerificationSettings: false`
- 手机接码目前在架构上仍是 OpenAI 私有能力。`docs/多注册流程架构边界.md` 明确指出 HeroSMS / 5sim / NexSMS、取号、复用、轮询、换号、释放、手机号注册和后置 add-phone 暂不进入 core 通用服务。
- 当前 provider 注册表是 `phone-sms/providers/registry.js`,已有 `hero-sms``5sim``nexsms`
- 当前接码主编排是 `background/phone-verification-flow.js`,实际运行还要同步考虑 `background.js` 内联后的运行入口。
- 当前侧边栏已有统一表单布局基础:`--data-label-width: 76px``--data-field-height: 34px``.data-inline` 作为右侧弹性控制区、`.btn``.data-input` / `.data-select` 共用高度。
- `tests/sidepanel-form-layout.test.js` 已保护“每个 `.data-row` 只有 label + 一个控制区”“按钮/输入框高度统一”“不再出现 `icon-action-btn`”等约束。
PR #288 的已知结论:
- PR 标题为“接入 MaDao 统一接码并收敛 provider 换号链路”。
- PR 增加了 `phone-sms/providers/madao.js`,并在 provider registry 中注册 MaDao。
- PR 的 MaDao 适配层包含 `acquireActivation``pollActivation``releaseActivation``rotateActivation``replaceRoutingActivation`,核心 API 为 `/api/acquire``/api/poll``/api/release``/api/routing/replace`
- PR 中值得保留的是 MaDao provider 模块边界、`routing_plan` / `direct` 两种模式、路由换号 `/api/routing/replace` 的行为。
- PR 中不应照搬的是主流程里 MaDao 特例函数、sidepanel 里散落的 MaDao 专用行和 GitHub 图标按钮,以及任何绕过 provider adapter 契约的判断。
## 3. 需求与硬边界
必须满足的需求:
- 重新实现 MaDao 接码接入,不直接复制 PR #288 的堆叠式 UI 和主流程特例。
- 不留旧的无用代码。被新 provider 契约、统一 UI descriptor、自动保存机制取代的分支、按钮、样式和测试夹具都要清理干净。
- 文本框与按钮高度、边框、宽度体系统一;按钮不需要图标。
- 表单布局采用“固定 label 宽度 + 右侧控制区占剩余宽度 + 可选按钮固定最小宽度”的模型,而不是固定文本框宽度。
- 即使某些文本框右侧新增按钮,也不能挤乱同一组布局;输入框应继续占剩余宽度。
- 项目地址必须正确:CPA 为 `https://github.com/router-for-me/CLIProxyAPI`sub2api 为 `https://github.com/Wei-Shaw/sub2api`
- GitHub 按钮使用普通文字按钮并纳入统一按钮样式,不做图标按钮。
- 自动保存已经存在时,纯“保存设置”按钮应删除;但查询价格、查余额、刷新列表、清空、记录运行时号码等有明确动作语义的按钮可以保留。
- OpenAI 手机号注册模式必须和后置 add-phone 区分,不能混用状态字段。
不能突破的边界:
- MaDao 只接入 OpenAI 手机接码能力,不把接码提升为 core 通用服务。
- Kiro/Grok 不显示 OpenAI 接码、Plus、平台绑定相关配置。
- 主流程不得写 MaDao 专用大分支;新增能力应通过 provider registry 和 provider adapter 暴露。
- 不能因为接入 MaDao 破坏现有 HeroSMS、5sim、NexSMS 行为和存量配置。
- 不能只修改 `background/phone-verification-flow.js` 而忘记实际加载的 `background.js`,也不能只改 `background.js` 导致模块测试失真。
## 4. 当前真实实现现状
### 4.1 OpenAI 手机号注册模式
OpenAI flow 当前不是单一注册路径,而是多个 variant:
- `normal`:邮箱注册 -> 邮箱验证码 -> 登录验证码 -> 后置手机号验证 -> OAuth -> 平台回调。
- `normalPhone`:手机号注册 -> 手机验证码 -> 登录验证码 -> 绑定邮箱 -> 绑定邮箱验证码 -> OAuth -> 平台回调。
- `normalPhoneRelogin`:手机号注册并绑定邮箱后,切入绑定邮箱重登链路。
因此“手机号注册”和“后置手机号验证”虽然都依赖接码 provider,但状态槽位不同:
- 手机号注册使用 `signupPhoneNumber``signupPhoneActivation``signupPhoneCompletedActivation`
- 后置 add-phone 使用 `currentPhoneActivation`
后续 MaDao 接入必须同时覆盖这两条链路,并在日志、换号、释放、成功完成时保持状态字段不串线。
### 4.2 provider 当前模型
当前 provider 顺序为 `hero-sms``5sim``nexsms``DEFAULT_PHONE_SMS_PROVIDER_ORDER` 和相关 normalize 函数会按 provider 数量截断,所以接入 MaDao 时必须同步扩展 provider 常量、顺序、registry、测试 fixture,否则 UI 选中了 MaDao 也可能被 normalize 回退或截断。
当前 `background/phone-verification-flow.js` 仍有较多 provider 内联逻辑。后续应把 MaDao 做成 adapter,并逐步收敛公共生命周期:
- acquire:取号。
- poll:轮询短信。
- releasecancel / ban / finish。
- rotate:失败换号,允许 provider 自己决定是先 release 再 acquire,还是走路由替换。
- normalize:把 provider 返回值转成统一 activation。
### 4.3 侧边栏 UI 当前模型
侧边栏接码区已经存在 HeroSMS、5sim、NexSMS 的配置行,且很多字段复用了 HeroSMS 命名或布局类。后续实现不应继续在 `sidepanel.js` 里追加大量 `if provider === ...` 片段,而应引入 provider UI descriptor 或至少集中式字段矩阵,让“显示哪些行、保存哪些字段、切换 provider 时保留哪些值”有统一来源。
当前布局已有正确方向:
- `.data-row` 负责一行。
- `.data-label` 固定 label 宽度。
- `.data-inline` 作为右侧 control area`flex: 1 1 0`
- `.data-input``.data-select``.btn` 统一高度。
- `.data-inline-btn` 用固定最小宽度承接右侧动作按钮。
需要避免的是:
- 直接在 `.data-row` 里放多个并列控制元素,导致 label 后布局不可预测。
- 单独给某些按钮做无边框、特殊高度、图标按钮或内联 style。
- 为某个 provider 写独立布局,造成切换 provider 后高度、宽度、边框不一致。
## 5. PR #288 取舍
### 5.1 保留的设计
保留 MaDao 作为独立 provider 模块的方向。建议文件为 `phone-sms/providers/madao.js`,暴露 `createProvider()`,并返回统一方法:
- `acquireActivation(state, options, deps)`
- `pollActivation(state, activation, deps)`
- `releaseActivation(state, activation, action, deps)`
- `rotateActivation(state, activation, options, deps)`
- `replaceRoutingActivation(state, activation, options, deps)`
保留 MaDao 默认配置:
- `provider id`: `madao`
- `label`: `MaDao`
- `baseUrl` 默认 `http://127.0.0.1:7822`
- `service` 默认 `openai`
- HTTP Secret 通过 `Authorization: Bearer <secret>` 传递
保留两种模式:
- `routing_plan`:使用 `madaoRoutingPlanId`,取号只提交 `routing_plan_id``service`,失败换号优先走 `/api/routing/replace`
- `direct`:使用 `madaoProviderId``madaoCountry``madaoAutoPickCountry``madaoReusePhone``madaoMinPrice``madaoMaxPrice`
### 5.2 不照搬的内容
不照搬主流程里的 MaDao 专用函数,例如 `requestMaDaoActivation``getMaDaoProviderForState` 这类把 provider 逻辑重新塞回编排层的写法。
不照搬 sidepanel 中大量散落的 MaDao row/handler。MaDao 字段应通过 provider UI descriptor 或集中显隐矩阵接入:
- provider 选择变更只触发一套 `applyPhoneSmsProviderUi(provider, state)`
- 保存 payload 只走一套 `collectPhoneSmsProviderSettings(provider)`
- 恢复 UI 只走一套 `restorePhoneSmsProviderSettings(state)`
不增加 `btn-open-madao-github`。MaDao 本地服务不是当前目标项目地址入口,GitHub 入口只保留已有项目来源按钮,且按钮样式应统一为普通文字按钮。
## 6. 最终设计
### 6.1 provider 契约
新增或收敛 provider adapter 契约:
```js
{
id,
label,
defaultProduct,
acquireActivation(state, options, deps),
pollActivation(state, activation, deps),
releaseActivation(state, activation, action, deps),
rotateActivation(state, activation, options, deps)
}
```
编排层只依赖契约,不依赖 provider 私有字段。activation 可以携带 provider 私有字段,但编排层只把它们原样保留和回传,最多读取统一字段:
- `activationId`
- `phoneNumber`
- `provider`
- `serviceCode`
- `countryId`
- `countryLabel`
MaDao 私有字段例如 `madaoRoutingPlanId``madaoRoutingItemId``madaoAcquirePath``madaoStatus` 只由 MaDao adapter 或展示层使用。
### 6.2 MaDao 设置字段
新增设置字段建议如下:
- `madaoBaseUrl`
- `madaoHttpSecret`
- `madaoMode`: `routing_plan``direct`
- `madaoRoutingPlanId`
- `madaoProviderId`
- `madaoCountry`
- `madaoAutoPickCountry`
- `madaoReusePhone`
- `madaoMinPrice`
- `madaoMaxPrice`
`madaoServiceName` 不建议作为普通可编辑项显示。OpenAI flow 下 service 固定为 `openai` 更符合当前边界,也能减少用户误配。如果确实需要保留兼容字段,可以内部默认和持久化,但 UI 只显示只读说明或不显示。
这些字段必须进入:
- 默认 state。
- 导入导出和最新设置合并 key。
- sidepanel 恢复、保存、消息更新。
- background 模块与实际 `background.js` 同步入口。
- 相关单元测试 fixture。
### 6.3 MaDao 模式显隐
MaDao 选中时显示:
- Base URL。
- HTTP Secret。
- 模式选择。
`routing_plan` 模式显示:
- 路由方案选择或路由方案 ID。
- 刷新路由方案按钮,如果 MaDao 后端提供列表接口;如果没有列表接口,只保留 ID 输入框,不做假刷新按钮。
`routing_plan` 模式隐藏:
- direct provider。
- country。
- auto pick country。
- reuse phone。
- min/max price。
`direct` 模式显示:
- MaDao provider。
- country。
- auto pick country。
- reuse phone。
- min/max price。
`direct` 模式隐藏:
- routing plan 字段。
- routing replace 相关交互。
### 6.4 其他 provider 显隐矩阵
HeroSMS 显示:
- API Key。
- 国家优先级。
- 生效顺序。
- 拿号优先级。
- 运营商。
- 价格区间:min、max、preferred。
- 号码复用。
- 白嫖复用。
- 查询价格。
- 查余额。
5sim 显示:
- API Key。
- 国家优先级。
- 生效顺序。
- 运营商。
- 产品代码。
- 价格区间:min、max。
- 号码复用。
5sim 不显示:
- preferred price。
- HeroSMS 价格档位查询。
- 白嫖复用。
NexSMS 显示:
- API Key。
- 国家优先级。
- 生效顺序。
- 服务代码。
NexSMS 不显示:
- preferred price。
- operator。
- product。
- 号码复用。
- 白嫖复用。
MaDao 按 6.3 显隐,不复用 HeroSMS 的价格预览区,不显示无意义的国家多选和 provider 顺序内的 HeroSMS 专属说明。
### 6.5 换号与释放
步骤 9 和手机号注册验证码链路中的失败换号都应走统一 provider 方法:
- provider 有 `rotateActivation` 时优先调用。
- MaDao `routing_plan` 且 activation 带 `madaoRoutingPlanId` 时,`rotateActivation` 内部走 `/api/routing/replace` 并返回 `nextActivation`
- MaDao `direct` 或没有可替换路由信息时,先 release 当前 ticket,再让编排层按 provider 顺序重新 acquire。
- HeroSMS、5sim、NexSMS 可以先维持现状,但调用入口要向统一契约靠拢。
编排层不应关心 MaDao 的 `/api/routing/replace` endpoint,只应理解 `rotateActivation` 的返回:
```js
{
currentTicketId,
currentTicketRelease,
nextActivation
}
```
### 6.6 OpenAI 手机号注册状态
后续实现必须分别覆盖:
- Step 2 手机号注册取号和填手机号。
- Step 4 手机注册验证码轮询和提交。
- Step 8 登录验证码、绑定邮箱状态分支。
- Step 9 后置 add-phone 取号、轮询、失败换号、成功释放或保留。
关键约束:
- `signupPhoneActivation` 不写入 `currentPhoneActivation`
- `currentPhoneActivation` 不覆盖 `signupPhoneActivation`
- `signupPhoneCompletedActivation` 用于手机号注册完成后的记录,不用于后置 add-phone 的当前订单。
- 页面已经在等手机验证码但 state 没有 activation 时,应给出明确错误,不自动构造虚假订单。
### 6.7 按钮与文本框统一设计
采用固定 label 宽度,不固定文本框宽度:
```txt
| 76px label | flex:1 控制区 |
```
控制区规则:
- 普通单输入:`input.data-input` 直接作为第二个子元素,宽度占满剩余。
- 输入 + 按钮:使用 `div.data-inline` 包裹,输入 `flex: 1 1 0`,按钮使用 `.data-inline-btn`
- 多按钮动作组:仍放入一个 `.data-inline`,按钮高度统一,必要时允许换行,但不破坏 label + control 的两列结构。
- 密码显示按钮属于输入框内部交互,保留 `input-with-icon`,但不使用 GitHub 图标或 provider 图标。
按钮规则:
- 所有普通按钮使用 `.btn` 基础类,必须有 `border: 1px solid var(--border)``min-height: var(--data-field-height)`
- 小按钮可以用 `btn-sm` / `btn-xs` 调整字体和 padding,但高度仍应由统一变量约束。
- 行内动作按钮使用 `.data-inline-btn`,统一最小宽度,不按文案临时写 style。
- 不再新增 `icon-action-btn`,已有图标按钮若不是密码显隐这种输入内部控制,应改成文字按钮。
- GitHub 按钮显示为 `GitHub` 文本,使用 `btn btn-outline btn-sm data-inline-btn`
删除按钮规则:
- 删除纯保存设置按钮,因为当前 `saveSettings({ silent: true })` 和 autosave 已覆盖设置变更。
- 保留“记录”“清除”“查询价格”“查余额”“拉取”“下一条”“检测出口”等命令按钮,因为它们触发的不是单纯保存。
- 删除按钮时必须同步删除 DOM 引用、事件监听、CSS、测试断言和无用 helper。
### 6.8 项目 GitHub 按钮
项目地址入口只保留一套:
- CPA: `https://github.com/router-for-me/CLIProxyAPI`
- sub2api: `https://github.com/Wei-Shaw/sub2api`
按钮不做图标,不使用 SVG,不新增 MaDao GitHub 入口。`tests/sidepanel-flow-source-registry.test.js` 需要继续保护 URL 和按钮 class。
## 7. 方案自检
### 7.1 是否符合需求
符合。方案明确处理了 PR #288 的重新实现、MaDao provider 接入、OpenAI 手机号注册模式、不同 provider 显隐、文本框和按钮统一、GitHub 按钮去图标化、无用保存按钮删除、旧代码清理等要求。
### 7.2 是否完善
基本完善。方案覆盖后台 provider 生命周期、侧边栏显示、设置持久化、自动保存、测试和实际运行入口。需要在实现阶段继续用代码验证的点是 MaDao 后端是否提供路由方案列表接口;如果没有,则 UI 不能设计“刷新路由方案”按钮,只能提供 ID 输入。
### 7.3 是否完整
完整性满足本次开发目标。方案没有只看 sidepanel,也覆盖了 OpenAI workflow、Step 2/4/8/9、provider registry、settings schema、`background.js` 实际入口和测试。未纳入范围的是把整个接码系统抽成 core 服务,因为这与现有架构边界冲突。
### 7.4 是否正确
方向正确。MaDao 被设计为 OpenAI 接码 provider,不污染 Kiro/Grokrouting replace 被封装在 provider adapter 内,不让主流程理解 MaDao endpoint;手机号注册和后置 add-phone 状态分离,避免订单串线。
### 7.5 是否规范一致
一致。UI 沿用当前 `.data-row``.data-label``.data-inline``.btn``.data-inline-btn` 体系;provider 沿用 `phone-sms/providers/registry.js`;测试沿用 `node --test tests/*.test.js`;中文文档风格与 `md` 目录已有方案保持一致。
### 7.6 是否存在设计冲突
已识别并规避以下冲突:
- “接码通用化”与现有架构边界冲突:本方案不通用化到 core。
- “固定文本框宽度”与行内按钮扩展冲突:本方案固定 label 和按钮最小宽度,让输入框占剩余。
- “MaDao routing_plan”与 direct 参数冲突:两种模式显隐互斥,payload 也互斥。
- “自动保存”与保存按钮冲突:纯保存按钮删除,命令按钮保留。
- “PR #288 直接 diff”与当前本地 UI 改动冲突:只吸收 provider 行为,不照搬 sidepanel 和主流程特例。
- “模块测试文件”与 `background.js` 实际入口冲突:实现阶段必须同步两边或使用现有生成/同步方式验证。
### 7.7 方案自身缺陷和风险
仍需在实现阶段验证的风险:
- MaDao 后端接口返回字段可能和 PR 样例不完全一致,需要 provider 单测覆盖异常 payload、空 ticket、空 phone、HTTP 错误和超时。
- 当前 `background/phone-verification-flow.js` provider 内联较多,完全一次性抽象所有 provider 风险较高。建议本次只为 MaDao 建立统一入口,并把会影响 MaDao 的 acquire/poll/release/rotate 入口收敛,不做无关大重构。
- sidepanel 当前文件较大,集中 descriptor 改造要控制范围。优先让接码设置区 descriptor 化,不顺手重构 Plus、代理、邮箱等其他区域。
- 删除按钮时容易误删命令按钮。判断标准必须是“是否只保存当前设置”,不是“按钮文案里是否有保存/记录”。
- provider 顺序扩展到 4 个后,测试 fixture 中硬编码 3 个 provider 的地方较多,必须全局搜索更新。
## 8. 分阶段开发清单
### 阶段 1:基线保护与差异确认
开发内容:
- 确认当前分支状态,记录本地 ahead commit,不回退已有改动。
- 对比 PR #288,只提取 MaDao provider、routing replace 行为和测试意图。
- 运行现有定向测试,建立失败/通过基线。
- 全局搜索旧图标按钮、保存按钮、provider 顺序硬编码、MaDao 残留。
阶段自检:
- 确认没有改动功能代码。
- 确认 CPA / sub2api GitHub URL 未被误改。
- 确认没有把 PR #288 中删除大量无关文件的改动纳入本次范围。
- 确认终端和文档无乱码。
### 阶段 2MaDao provider 模块与 registry
开发内容:
- 新增 `phone-sms/providers/madao.js`
-`phone-sms/providers/registry.js` 注册 `madao`
- 扩展 `DEFAULT_PHONE_SMS_PROVIDER_ORDER` 为 4 个 provider。
- 更新 provider normalize、label、moduleKey、provider ids 测试。
阶段自检:
- MaDao 未加载时错误信息明确。
- provider order 不截断 MaDao。
- HeroSMS、5sim、NexSMS normalize 行为不变。
- 单测覆盖未知 provider 回退默认 provider。
- 检查新文件 UTF-8 中文错误信息无乱码。
### 阶段 3:接码编排层接入统一 rotate
开发内容:
-`background/phone-verification-flow.js` 中接入 MaDao adapter。
- 收敛 acquire/poll/release/rotate 的调用入口,让 MaDao 走 provider 契约。
- Step 9 换号优先调用 `rotateActivation`routing plan 返回 `nextActivation` 时直接继续使用新号。
- direct 模式或没有 `nextActivation` 时走现有释放后重新取号流程。
- 同步实际运行入口 `background.js`
阶段自检:
- 手机号注册 Step 2/4 使用 `signupPhoneActivation`
- 后置 add-phone Step 9 使用 `currentPhoneActivation`
- 失败换号不会把 signup activation 写入 current activation。
- MaDao routing replace 的 cancel/ban action 传参正确。
- HeroSMS、5sim、NexSMS 原有换号、释放、成功完成行为不回退。
### 阶段 4:设置默认值、导入导出与状态同步
开发内容:
- 新增 MaDao 设置默认值。
- 更新 settings schema、最新设置 key、导入导出、消息保存 payload。
- 更新 sidepanel restore 和后台 state merge。
-`madaoMode``madaoBaseUrl``madaoHttpSecret``madaoRoutingPlanId`、direct 字段加 normalize。
阶段自检:
- 空配置下 MaDao 使用默认 `http://127.0.0.1:7822`
- `routing_plan` 不提交 direct 字段。
- `direct` 不提交 routing plan 字段。
- 导入旧配置不会丢 HeroSMS / 5sim / NexSMS 设置。
- 自动保存不会在用户输入数字半成品时强行格式化造成体验问题。
### 阶段 5:侧边栏 provider UI descriptor
开发内容:
- 建立接码 provider 字段矩阵或 descriptor。
- 将 HeroSMS、5sim、NexSMS、MaDao 的行显隐集中到一处。
- provider 切换时保存当前 provider 独立字段,再恢复目标 provider 字段。
- MaDao mode 切换时刷新 routing/direct 显隐。
- 删除散落的 provider 专用重复判断。
阶段自检:
- HeroSMS 只显示 HeroSMS 相关字段。
- 5sim 不显示 preferred price 和白嫖复用。
- NexSMS 不显示 operator/product/复用。
- MaDao routing plan 不显示 direct 字段。
- MaDao direct 不显示 routing plan 字段。
- 切换 provider 不丢失其他 provider 已填 API key 和价格字段。
- Kiro/Grok 下不显示接码设置。
### 阶段 6:文本框和按钮样式统一
开发内容:
- 清理接码区和项目地址区不一致按钮样式。
- 删除无用纯保存按钮及对应 JS/CSS/测试。
- 所有行改为 label + 单一 control area。
- 有右侧按钮的文本框统一使用 `.data-inline` + `.data-inline-btn`
- GitHub 按钮保持文字按钮,不使用图标。
阶段自检:
- `tests/sidepanel-form-layout.test.js` 通过。
- 全局不存在 `icon-action-btn`
- `.data-row` 没有超过两个直接子元素。
- `.btn``.data-input``.data-select` 高度一致。
- 按钮都有边框,除输入框内部密码显隐按钮外不出现无边框图标按钮。
- 文案较长按钮不撑破布局,必要时按统一规则换行。
### 阶段 7:测试完善
开发内容:
- 增加 MaDao provider 单测:acquire、poll、release、routing replace、direct fallback、超时、HTTP 错误。
- 增加 provider registry 测试:MaDao 注册、顺序、label、moduleKey。
- 增加 phone verification flow 测试:Step 9 routing replace 成功、routing replace 返回无效 next ticket、direct 模式释放后重新取号。
- 增加 sidepanel 测试:MaDao 字段显隐、mode 切换、保存 payload、provider 切换字段保留。
- 更新现有 provider order 和 settings schema 测试。
阶段自检:
- 定向测试全部通过。
- 全量 `npm test` 通过。
- 没有只为测试通过而保留旧死代码。
- 测试名称准确描述行为,不写和实现不符的宽泛断言。
### 阶段 8:最终清理与人工核验
开发内容:
- 全局搜索 MaDao 旧函数名、旧按钮 id、旧 icon 类、无用 CSS。
- 全局搜索 provider 顺序硬编码的 3 provider 残留。
- 检查 sidepanel 渲染效果,确认按钮、文本框、select 高度和边框一致。
- 检查 OpenAI / Kiro / Grok 各 flow 显隐。
- 检查 CPA / sub2api 项目地址按钮。
阶段自检:
- `rg -n "requestMaDaoActivation|getMaDaoProviderForState|btn-open-madao-github|icon-action-btn" .` 无结果。
- `rg -n "hero-sms.*5sim.*nexsms" tests sidepanel background phone-sms` 中需要 4 provider 的地方已包含 MaDao。
- 文档、源码、测试无乱码字符;乱码样本不要写进方案正文,避免检查命令命中自身。
- `git diff` 中没有无关删除、无关格式化或用户未要求的重构。
- 最终运行全量测试并记录结果。
## 9. 验证命令
建议按阶段使用以下命令:
```powershell
node --test tests/phone-sms-provider-registry.test.js
node --test tests/phone-verification-flow.test.js
node --test tests/sidepanel-phone-verification-settings.test.js
node --test tests/sidepanel-form-layout.test.js
node --test tests/sidepanel-flow-source-registry.test.js
npm test
```
建议的清理检查:
```powershell
rg -n "requestMaDaoActivation|getMaDaoProviderForState|btn-open-madao-github|icon-action-btn" .
$staleMarkers = @('T' + 'ODO', '待' + '补', '以后' + '再说', '占' + '位') -join '|'
rg -n $staleMarkers md sidepanel background phone-sms tests
git status --short --branch
git diff --check
```
## 10. 最终结论
本次应按“OpenAI 私有接码 provider 扩展 + 侧边栏 provider descriptor + 表单统一布局”三条线并行收敛。MaDao 的价值在于统一接码后端和 routing replace,不应把它变成主流程里的新特例;UI 的价值在于固定 label、固定按钮最小宽度、输入框占剩余,而不是继续给每个 provider 单独堆宽高和按钮风格。
后续实现完成的标准是:MaDao 能在手机号注册和后置 add-phone 两条链路中稳定取号、轮询、释放、换号;不同 provider 只显示自己需要的字段;按钮和文本框视觉一致;自动保存下没有多余保存按钮;旧的 MaDao 特例和图标按钮代码完全清理。
+462
View File
@@ -0,0 +1,462 @@
// phone-sms/providers/madao.js - MaDao unified SMS backend adapter
(function attachMaDaoProvider(root, factory) {
root.PhoneSmsMaDaoProvider = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createMaDaoProviderModule() {
const PROVIDER_ID = 'madao';
const DEFAULT_BASE_URL = 'http://127.0.0.1:7822';
const DEFAULT_SERVICE = 'openai';
const DEFAULT_MODE = 'routing_plan';
const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
function normalizeText(value = '', fallback = '') {
return String(value || '').trim() || fallback;
}
function normalizeBaseUrl(value = '', fallback = DEFAULT_BASE_URL) {
const trimmed = normalizeText(value, fallback);
try {
return new URL(trimmed).toString().replace(/\/+$/, '');
} catch {
return fallback;
}
}
function normalizeMode(value = '') {
return normalizeText(value).toLowerCase() === 'direct' ? 'direct' : DEFAULT_MODE;
}
function normalizeProviderId(value = '') {
return normalizeText(value).toLowerCase().replace(/[^a-z0-9_-]+/g, '');
}
function normalizeCountry(value = '') {
const trimmed = normalizeText(value);
if (!trimmed) {
return '';
}
const lowered = trimmed.toLowerCase();
if (lowered === 'any' || lowered === 'local') {
return lowered;
}
if (/^[a-z]{2}$/i.test(trimmed)) {
return trimmed.toUpperCase();
}
return lowered.replace(/[^a-z0-9_-]+/g, '');
}
function normalizeBoolean(value, fallback = false) {
if (value === undefined || value === null || value === '') {
return Boolean(fallback);
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (/^(false|0|no|off)$/i.test(normalized)) {
return false;
}
if (/^(true|1|yes|on)$/i.test(normalized)) {
return true;
}
}
return Boolean(value);
}
function normalizePrice(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric) || numeric <= 0) {
return null;
}
return Math.round(numeric * 10000) / 10000;
}
function buildHeaders(config = {}, extraHeaders = {}) {
const headers = {
Accept: 'application/json',
...extraHeaders,
};
const secret = normalizeText(config.httpSecret);
if (secret) {
headers.Authorization = `Bearer ${secret}`;
}
return headers;
}
function describePayload(payload) {
if (payload && typeof payload === 'object') {
const direct = normalizeText(payload.message || payload.error || payload.detail || payload.status);
if (direct) {
return direct;
}
try {
return JSON.stringify(payload);
} catch {
return String(payload);
}
}
return normalizeText(payload);
}
async function requestJson(config, path, options = {}) {
const fetchImpl = config.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
if (!fetchImpl) {
throw new Error('MaDao 网络请求实现不可用。');
}
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const timeoutId = controller
? setTimeout(() => controller.abort(), Number(config.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS)
: null;
try {
const url = new URL(path.replace(/^\/+/, ''), `${config.baseUrl.replace(/\/+$/, '')}/`);
const method = normalizeText(options.method, 'GET').toUpperCase();
const init = {
method,
headers: buildHeaders(config, options.headers || {}),
signal: controller?.signal,
};
if (options.body !== undefined) {
init.body = JSON.stringify(options.body);
init.headers['Content-Type'] = 'application/json';
}
const response = await fetchImpl(url.toString(), init);
const rawText = await response.text();
let payload = null;
try {
payload = rawText ? JSON.parse(rawText) : null;
} catch {
payload = rawText;
}
if (!response.ok) {
const error = new Error(`MaDao 请求失败:${describePayload(payload) || response.statusText || response.status}`);
error.status = response.status;
error.payload = payload;
throw error;
}
return payload;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error('MaDao 请求超时。');
}
throw error;
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
function resolveConfig(state = {}, deps = {}) {
return {
baseUrl: normalizeBaseUrl(state?.madaoBaseUrl || DEFAULT_BASE_URL),
httpSecret: normalizeText(state?.madaoHttpSecret),
fetchImpl: deps.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null),
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
};
}
function mapAcquirePath(value = '') {
const normalized = normalizeText(value).toLowerCase();
if (normalized === 'same_activation_retry') {
return 'same_activation_retry';
}
if (normalized === 'exact_reuse') {
return 'exact_reuse';
}
if (normalized === 'intent_reuse') {
return 'intent_reuse';
}
return 'fresh_acquire';
}
function mapTicketStatus(value = '') {
const normalized = normalizeText(value).toLowerCase();
if (normalized === 'waiting_code') {
return 'waiting_code';
}
if (normalized === 'code_received') {
return 'code_received';
}
if (normalized === 'finished') {
return 'finished';
}
if (normalized === 'cancelled' || normalized === 'canceled') {
return 'cancelled';
}
if (normalized === 'failed') {
return 'failed';
}
return 'pending';
}
function buildAcquireRequest(state = {}, options = {}) {
const mode = normalizeMode(options?.mode || state?.madaoMode);
const routingPlanId = normalizeText(options?.routingPlanId || state?.madaoRoutingPlanId);
const directProvider = normalizeProviderId(options?.providerId || state?.madaoProviderId);
const request = {
provider: mode === 'routing_plan' && routingPlanId ? 'auto' : (directProvider || 'auto'),
service: normalizeText(options?.service || state?.madaoServiceName, DEFAULT_SERVICE),
};
const country = normalizeCountry(options?.country || state?.madaoCountry);
const minPrice = normalizePrice(options?.minPrice ?? state?.madaoMinPrice);
const maxPrice = normalizePrice(options?.maxPrice ?? state?.madaoMaxPrice);
if (mode === 'routing_plan' && routingPlanId) {
request.routing_plan_id = routingPlanId;
return request;
}
request.auto_pick_country = normalizeBoolean(options?.autoPickCountry ?? state?.madaoAutoPickCountry, true);
request.reuse_phone = normalizeBoolean(options?.reusePhone ?? state?.madaoReusePhone, true);
if (country) {
request.country = country;
}
if (minPrice !== null) {
request.min_price = minPrice;
}
if (maxPrice !== null) {
request.max_price = maxPrice;
}
return request;
}
function normalizeActivationFromAcquire(payload = {}, fallback = {}) {
const source = payload && typeof payload === 'object' ? payload : {};
const ticketId = normalizeText(source.ticket_id || source.ticketId || source.id);
const phoneNumber = normalizeText(source.phone_number || source.phoneNumber || source.phone);
if (!ticketId || !phoneNumber) {
return null;
}
const price = normalizePrice(source.price);
return {
activationId: ticketId,
phoneNumber,
provider: PROVIDER_ID,
serviceCode: normalizeText(source.service || fallback.service, DEFAULT_SERVICE),
countryId: normalizeCountry(source.country || fallback.country),
countryLabel: normalizeText(source.country_label || source.countryLabel),
maxUses: 1,
successfulUses: 0,
madaoProviderId: normalizeProviderId(source.provider || fallback.provider),
madaoRoutingPlanId: normalizeText(source.routing_plan_id || source.routingPlanId || fallback.routing_plan_id),
madaoRoutingPlanName: normalizeText(source.routing_plan_name || source.routingPlanName || fallback.routing_plan_name),
madaoRoutingItemId: normalizeText(source.routing_item_id || source.routingItemId || fallback.routing_item_id),
madaoAcquirePath: mapAcquirePath(source.acquire_path || source.acquirePath),
madaoStatus: mapTicketStatus(source.status),
...(price !== null ? { madaoPrice: price } : {}),
};
}
function extractVerificationCode(value = '') {
const text = String(value || '').trim();
if (!text) {
return '';
}
return text.match(/\b(\d{4,8})\b/)?.[1] || '';
}
function extractCodeFromPollPayload(payload = {}) {
if (!payload || typeof payload !== 'object') {
return extractVerificationCode(payload);
}
const candidates = [
payload.code,
payload.sms_code,
payload.smsCode,
payload.text,
payload.message,
payload.data?.code,
payload.data?.text,
payload.data?.message,
];
for (const candidate of candidates) {
const code = extractVerificationCode(candidate);
if (code) {
return code;
}
}
const messages = Array.isArray(payload.messages)
? payload.messages
: (Array.isArray(payload.sms) ? payload.sms : []);
for (let index = messages.length - 1; index >= 0; index -= 1) {
const item = messages[index] || {};
const code = extractVerificationCode(item.code || item.text || item.message || item.body);
if (code) {
return code;
}
}
return '';
}
async function acquireActivation(state = {}, options = {}, deps = {}) {
const config = resolveConfig(state, deps);
const requestBody = buildAcquireRequest(state, options);
const payload = await requestJson(config, '/api/acquire', {
method: 'POST',
body: requestBody,
});
const activation = normalizeActivationFromAcquire(payload, requestBody);
if (!activation) {
throw new Error('MaDao 返回的激活记录无效。');
}
return activation;
}
async function pollActivation(state = {}, activation, deps = {}) {
const config = resolveConfig(state, deps);
const ticketId = normalizeText(activation?.activationId || activation?.ticketId);
if (!ticketId) {
throw new Error('MaDao 激活记录缺少 ticket_id。');
}
return requestJson(config, '/api/poll', {
method: 'POST',
body: {
ticket_id: ticketId,
},
});
}
async function pollActivationCode(state = {}, activation, options = {}, deps = {}) {
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 '';
}
async function releaseActivation(state = {}, activation, action = 'cancel', deps = {}) {
const config = resolveConfig(state, deps);
const ticketId = normalizeText(activation?.activationId || activation?.ticketId);
if (!ticketId) {
throw new Error('MaDao 激活记录缺少 ticket_id。');
}
return requestJson(config, '/api/release', {
method: 'POST',
body: {
ticket_id: ticketId,
action: normalizeText(action, 'cancel').toLowerCase(),
},
});
}
async function replaceRoutingActivation(state = {}, activation, options = {}, deps = {}) {
const config = resolveConfig(state, deps);
const ticketId = normalizeText(activation?.activationId || activation?.ticketId);
if (!ticketId) {
throw new Error('MaDao 激活记录缺少 ticket_id。');
}
const releaseAction = normalizeText(options?.releaseAction, 'cancel').toLowerCase() === 'ban'
? 'ban'
: 'cancel';
const payload = await requestJson(config, '/api/routing/replace', {
method: 'POST',
body: {
ticket_id: ticketId,
release_action: releaseAction,
failed_item_id: normalizeText(options?.failedItemId || activation?.madaoRoutingItemId),
reason: normalizeText(options?.reason),
},
});
const nextTicket = normalizeActivationFromAcquire(payload?.next_ticket || payload?.nextTicket, {
routing_plan_id: activation?.madaoRoutingPlanId,
routing_plan_name: activation?.madaoRoutingPlanName,
service: activation?.serviceCode,
country: activation?.countryId,
});
if (!nextTicket) {
throw new Error('MaDao 返回的下一条路由激活记录无效。');
}
return {
currentTicketId: normalizeText(payload?.current_ticket_id || payload?.currentTicketId, ticketId),
currentTicketRelease: payload?.current_ticket_release || payload?.currentTicketRelease || null,
nextActivation: nextTicket,
};
}
async function rotateActivation(state = {}, activation, options = {}, deps = {}) {
const mode = normalizeMode(state?.madaoMode);
const normalizedActivation = activation && typeof activation === 'object' ? activation : null;
const releaseAction = normalizeText(options?.releaseAction, 'cancel').toLowerCase() === 'ban'
? 'ban'
: 'cancel';
if (mode === 'routing_plan' && normalizeText(normalizedActivation?.madaoRoutingPlanId)) {
return replaceRoutingActivation(state, normalizedActivation, {
releaseAction,
failedItemId: options?.failedItemId || normalizedActivation?.madaoRoutingItemId,
reason: options?.reason,
}, deps);
}
const currentTicketRelease = await releaseActivation(state, normalizedActivation, releaseAction, deps);
return {
currentTicketId: normalizeText(normalizedActivation?.activationId || normalizedActivation?.ticketId),
currentTicketRelease,
nextActivation: null,
};
}
function createProvider(deps = {}) {
return {
id: PROVIDER_ID,
label: 'MaDao',
defaultProduct: DEFAULT_SERVICE,
acquireActivation: (state, options = {}, runtimeDeps = {}) => acquireActivation(state, options, {
...deps,
...runtimeDeps,
}),
pollActivation: (state, activation, runtimeDeps = {}) => pollActivation(state, activation, {
...deps,
...runtimeDeps,
}),
pollActivationCode: (state, activation, options = {}, runtimeDeps = {}) => pollActivationCode(state, activation, options, {
...deps,
...runtimeDeps,
}),
releaseActivation: (state, activation, action = 'cancel', runtimeDeps = {}) => releaseActivation(state, activation, action, {
...deps,
...runtimeDeps,
}),
rotateActivation: (state, activation, options = {}, runtimeDeps = {}) => rotateActivation(state, activation, options, {
...deps,
...runtimeDeps,
}),
replaceRoutingActivation: (state, activation, options = {}, runtimeDeps = {}) => replaceRoutingActivation(state, activation, options, {
...deps,
...runtimeDeps,
}),
buildAcquireRequest,
extractCodeFromPollPayload,
mapAcquirePath,
mapTicketStatus,
normalizeActivationFromAcquire,
resolveConfig: (state = {}, runtimeDeps = {}) => resolveConfig(state, {
...deps,
...runtimeDeps,
}),
};
}
return {
PROVIDER_ID,
DEFAULT_BASE_URL,
DEFAULT_MODE,
DEFAULT_SERVICE,
acquireActivation,
buildAcquireRequest,
createProvider,
extractCodeFromPollPayload,
mapAcquirePath,
mapTicketStatus,
normalizeActivationFromAcquire,
pollActivation,
pollActivationCode,
releaseActivation,
replaceRoutingActivation,
resolveConfig,
rotateActivation,
};
});
+8
View File
@@ -5,11 +5,13 @@
const PROVIDER_HERO_SMS = 'hero-sms'; const PROVIDER_HERO_SMS = 'hero-sms';
const PROVIDER_FIVE_SIM = '5sim'; const PROVIDER_FIVE_SIM = '5sim';
const PROVIDER_NEXSMS = 'nexsms'; const PROVIDER_NEXSMS = 'nexsms';
const PROVIDER_MADAO = 'madao';
const DEFAULT_PROVIDER = PROVIDER_HERO_SMS; const DEFAULT_PROVIDER = PROVIDER_HERO_SMS;
const DEFAULT_PROVIDER_ORDER = Object.freeze([ const DEFAULT_PROVIDER_ORDER = Object.freeze([
PROVIDER_HERO_SMS, PROVIDER_HERO_SMS,
PROVIDER_FIVE_SIM, PROVIDER_FIVE_SIM,
PROVIDER_NEXSMS, PROVIDER_NEXSMS,
PROVIDER_MADAO,
]); ]);
const PROVIDER_DEFINITIONS = Object.freeze({ const PROVIDER_DEFINITIONS = Object.freeze({
[PROVIDER_HERO_SMS]: Object.freeze({ [PROVIDER_HERO_SMS]: Object.freeze({
@@ -27,6 +29,11 @@
label: 'NexSMS', label: 'NexSMS',
moduleKey: 'PhoneSmsNexSmsProvider', moduleKey: 'PhoneSmsNexSmsProvider',
}), }),
[PROVIDER_MADAO]: Object.freeze({
id: PROVIDER_MADAO,
label: 'MaDao',
moduleKey: 'PhoneSmsMaDaoProvider',
}),
}); });
function resolveProviderKey(value = '') { function resolveProviderKey(value = '') {
@@ -125,6 +132,7 @@
PROVIDER_HERO_SMS, PROVIDER_HERO_SMS,
PROVIDER_FIVE_SIM, PROVIDER_FIVE_SIM,
PROVIDER_NEXSMS, PROVIDER_NEXSMS,
PROVIDER_MADAO,
DEFAULT_PROVIDER, DEFAULT_PROVIDER,
DEFAULT_PROVIDER_ORDER, DEFAULT_PROVIDER_ORDER,
PROVIDER_DEFINITIONS, PROVIDER_DEFINITIONS,
+25 -47
View File
@@ -128,26 +128,10 @@ header {
flex: 1; flex: 1;
} }
.header-left svg { color: var(--blue); } .header-repo-btn {
.header-icon-link {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border: none;
background: transparent;
color: inherit;
cursor: pointer;
flex-shrink: 0; flex-shrink: 0;
appearance: none;
} }
.header-icon-link:hover svg {
color: var(--text-primary);
}
.header-icon-link:focus-visible,
.header-version-link:focus-visible { .header-version-link:focus-visible {
outline: 2px solid color-mix(in srgb, var(--blue) 28%, transparent); outline: 2px solid color-mix(in srgb, var(--blue) 28%, transparent);
outline-offset: 2px; outline-offset: 2px;
@@ -321,29 +305,6 @@ header {
outline-offset: 1px; outline-offset: 1px;
} }
/* ============================================================
Theme Toggle
============================================================ */
.theme-toggle {
width: 30px;
height: 30px;
border: none;
background: transparent;
color: var(--text-muted);
border-radius: var(--radius-sm);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition);
}
.theme-toggle:hover { background: var(--bg-hover); color: var(--text-primary); }
.theme-toggle .icon-moon { display: block; }
.theme-toggle .icon-sun { display: none; }
[data-theme="dark"] .theme-toggle .icon-moon { display: none; }
[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
.header-menu { .header-menu {
position: relative; position: relative;
display: flex; display: flex;
@@ -422,6 +383,8 @@ header {
line-height: 1; line-height: 1;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: var(--bg-field);
color: var(--text-secondary);
cursor: pointer; cursor: pointer;
transition: all var(--transition); transition: all var(--transition);
white-space: nowrap; white-space: nowrap;
@@ -486,8 +449,17 @@ header {
} }
.btn-outline:hover { border-color: var(--blue); color: var(--blue); background: var(--blue-soft); } .btn-outline:hover { border-color: var(--blue); color: var(--blue); background: var(--blue-soft); }
.btn-sm { padding-inline: 12px; font-size: 12px; } .btn-sm {
.btn-xs { padding-inline: 10px; font-size: 11px; } min-height: var(--data-field-height);
padding-inline: 12px;
font-size: 12px;
}
.btn-xs {
min-height: var(--data-field-height);
padding-inline: 10px;
font-size: 11px;
}
/* ============================================================ /* ============================================================
Extension Updates Extension Updates
@@ -1556,6 +1528,7 @@ header {
min-width: var(--data-inline-action-min-width); min-width: var(--data-inline-action-min-width);
min-height: var(--data-field-height); min-height: var(--data-field-height);
justify-content: center; justify-content: center;
white-space: nowrap;
} }
.hotmail-card { .hotmail-card {
@@ -2922,22 +2895,27 @@ header {
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 6px; gap: 6px;
width: 48px; width: 76px;
flex-shrink: 0; flex-shrink: 0;
} }
.step-manual-btn { .step-manual-btn {
width: 20px; min-width: 48px;
height: 20px; min-height: 26px;
padding: 0; padding: 0 8px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 999px; border-radius: var(--radius-sm);
background: var(--bg-surface); background: var(--bg-surface);
color: var(--text-muted); color: var(--text-muted);
cursor: pointer; cursor: pointer;
font-family: inherit;
font-size: 11px;
font-weight: 700;
line-height: 1;
white-space: nowrap;
transition: all var(--transition); transition: all var(--transition);
} }
.step-manual-btn:hover:not(:disabled) { .step-manual-btn:hover:not(:disabled) {
+92 -41
View File
@@ -16,12 +16,8 @@
<body> <body>
<header> <header>
<div class="header-left"> <div class="header-left">
<button id="btn-repo-home" class="header-icon-link" type="button" aria-label="打开 GitHub 仓库" title="打开 GitHub 仓库"> <button id="btn-repo-home" class="btn btn-outline btn-sm header-repo-btn" type="button"
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-label="打开 GitHub 仓库" title="打开 GitHub 仓库">仓库</button>
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
</svg>
</button>
<div class="header-version-block"> <div class="header-version-block">
<div class="header-version-main"> <div class="header-version-main">
<button id="extension-update-status" class="header-version-title header-version-link" type="button" <button id="extension-update-status" class="header-version-title header-version-link" type="button"
@@ -36,39 +32,11 @@
<button id="btn-contribution-mode" class="btn btn-outline btn-sm btn-contribution-mode" type="button" <button id="btn-contribution-mode" class="btn btn-outline btn-sm btn-contribution-mode" type="button"
aria-pressed="false" title="进入贡献模式并打开官网页">贡献/使用教程</button> aria-pressed="false" title="进入贡献模式并打开官网页">贡献/使用教程</button>
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" title="运行次数" /> <input type="number" id="input-run-count" class="run-count-input" value="1" min="1" title="运行次数" />
<button id="btn-auto-run" class="btn btn-success" title="自动执行全部步骤"> <button id="btn-auto-run" class="btn btn-success" title="自动执行全部步骤">自动</button>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
自动
</button>
<button id="btn-stop" class="btn btn-danger" title="停止当前流程" disabled>停止</button> <button id="btn-stop" class="btn btn-danger" title="停止当前流程" disabled>停止</button>
</div> </div>
<button id="btn-reset" class="btn btn-ghost" title="重置全部步骤"> <button id="btn-reset" class="btn btn-ghost btn-sm" title="重置全部步骤">重置</button>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" <button id="btn-theme" class="btn btn-ghost btn-sm header-theme-btn" title="切换主题">主题</button>
stroke-linecap="round" stroke-linejoin="round">
<polyline points="1 4 1 10 7 10" />
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
</svg>
</button>
<button id="btn-theme" class="theme-toggle" title="切换主题">
<svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
<svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
</button>
<div id="config-menu-shell" class="header-menu"> <div id="config-menu-shell" class="header-menu">
<button id="btn-config-menu" class="btn btn-ghost btn-sm header-config-btn" type="button" aria-haspopup="menu" <button id="btn-config-menu" class="btn btn-ghost btn-sm header-config-btn" type="button" aria-haspopup="menu"
aria-expanded="false">配置</button> aria-expanded="false">配置</button>
@@ -1470,20 +1438,22 @@
<option value="hero-sms">HeroSMS</option> <option value="hero-sms">HeroSMS</option>
<option value="5sim">5sim</option> <option value="5sim">5sim</option>
<option value="nexsms">NexSMS</option> <option value="nexsms">NexSMS</option>
<option value="madao">MaDao</option>
</select> </select>
</div> </div>
<div class="data-row" id="row-phone-sms-provider-order" style="display:none;"> <div class="data-row" id="row-phone-sms-provider-order" style="display:none;">
<span class="data-label">服务商顺序</span> <span class="data-label">服务商顺序</span>
<div class="data-inline hero-sms-country-stack"> <div class="data-inline hero-sms-country-stack">
<select id="select-phone-sms-provider-order" class="data-input mono" multiple size="3" style="display:none;"> <select id="select-phone-sms-provider-order" class="data-input mono" multiple size="4" style="display:none;">
<option value="hero-sms" selected>HeroSMS</option> <option value="hero-sms" selected>HeroSMS</option>
<option value="5sim" selected>5sim</option> <option value="5sim" selected>5sim</option>
<option value="nexsms" selected>NexSMS</option> <option value="nexsms" selected>NexSMS</option>
<option value="madao" selected>MaDao</option>
</select> </select>
<div class="hero-sms-country-mainline"> <div class="hero-sms-country-mainline">
<div id="phone-sms-provider-order-menu-shell" class="hero-sms-country-menu"> <div id="phone-sms-provider-order-menu-shell" class="hero-sms-country-menu">
<button id="btn-phone-sms-provider-order-menu" class="btn btn-outline btn-sm hero-sms-country-menu-btn" type="button" aria-haspopup="listbox" aria-expanded="false"> <button id="btn-phone-sms-provider-order-menu" class="btn btn-outline btn-sm hero-sms-country-menu-btn" type="button" aria-haspopup="listbox" aria-expanded="false">
HeroSMS / 5sim / NexSMS (3/3) HeroSMS / 5sim / NexSMS / MaDao (4/4)
</button> </button>
<div id="phone-sms-provider-order-menu" class="hero-sms-country-menu-dropdown" role="listbox" aria-multiselectable="true" hidden></div> <div id="phone-sms-provider-order-menu" class="hero-sms-country-menu-dropdown" role="listbox" aria-multiselectable="true" hidden></div>
</div> </div>
@@ -1636,6 +1606,86 @@
<input type="text" id="input-nex-sms-service-code" class="data-input mono" <input type="text" id="input-nex-sms-service-code" class="data-input mono"
placeholder="ot" /> placeholder="ot" />
</div> </div>
<div class="data-row" id="row-madao-base-url" style="display:none;">
<span class="data-label">MaDao 地址</span>
<input type="text" id="input-madao-base-url" class="data-input mono"
placeholder="http://127.0.0.1:7822" />
</div>
<div class="data-row" id="row-madao-http-secret" style="display:none;">
<span class="data-label">MaDao 密钥</span>
<div class="input-with-icon">
<input type="password" id="input-madao-http-secret" class="data-input data-input-with-icon mono"
placeholder="可空;MaDao HTTP Secret" />
<button id="btn-toggle-madao-http-secret" class="input-icon-btn" type="button"
data-password-toggle="input-madao-http-secret" data-show-label="显示 MaDao HTTP Secret"
data-hide-label="隐藏 MaDao HTTP Secret" aria-label="显示 MaDao HTTP Secret" title="显示 MaDao HTTP Secret"></button>
</div>
</div>
<div class="data-row" id="row-madao-mode" style="display:none;">
<span class="data-label">接入模式</span>
<select id="select-madao-mode" class="data-select mono">
<option value="routing_plan">路由计划</option>
<option value="direct">直连参数</option>
</select>
</div>
<div class="data-row" id="row-madao-routing-plan-id" style="display:none;">
<span class="data-label">路由计划</span>
<input type="text" id="input-madao-routing-plan-id" class="data-input mono"
placeholder="routing_plan_id" />
</div>
<div class="data-row" id="row-madao-provider-id" style="display:none;">
<span class="data-label">直连平台</span>
<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 class="data-row" id="row-madao-reuse-phone" style="display:none;">
<span class="data-label">MaDao 复用</span>
<div class="data-inline">
<label class="toggle-switch" for="input-madao-reuse-phone">
<input type="checkbox" id="input-madao-reuse-phone" />
<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 class="data-row" id="row-madao-price-range" style="display:none;">
<span class="data-label">MaDao 价格</span>
<div class="data-inline hero-sms-price-preview-stack">
<div class="hero-sms-price-controls-grid">
<div class="hero-sms-price-control">
<span class="hero-sms-settings-caption">最低购买价</span>
<div class="setting-controls">
<input type="number" id="input-madao-min-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="可空" min="0" step="0.0001" />
</div>
</div>
<div class="hero-sms-price-control">
<span class="hero-sms-settings-caption">价格上限</span>
<div class="setting-controls">
<input type="number" id="input-madao-max-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="可空" min="0" step="0.0001" />
</div>
</div>
</div>
</div>
</div>
<div class="data-row" id="row-hero-sms-max-price" style="display:none;"> <div class="data-row" id="row-hero-sms-max-price" style="display:none;">
<span class="data-label">价格区间</span> <span class="data-label">价格区间</span>
<div class="data-inline hero-sms-price-preview-stack"> <div class="data-inline hero-sms-price-preview-stack">
@@ -1660,13 +1710,13 @@
<input type="number" id="input-hero-sms-max-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="0.12" min="0" step="0.0001" title="接码价格上限;可空(空=不设上限)" /> <input type="number" id="input-hero-sms-max-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="0.12" min="0" step="0.0001" title="接码价格上限;可空(空=不设上限)" />
</div> </div>
</div> </div>
<div class="hero-sms-price-control"> <div id="row-phone-sms-preferred-price-control" class="hero-sms-price-control">
<span class="hero-sms-settings-caption">指定档位</span> <span class="hero-sms-settings-caption">指定档位</span>
<div class="setting-controls"> <div class="setting-controls">
<input type="number" id="input-hero-sms-preferred-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="例如 0.0512(可空)" min="0" step="0.0001" title="优先尝试该价格档位;可空(空=按优先级策略)" /> <input type="number" id="input-hero-sms-preferred-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="例如 0.0512(可空)" min="0" step="0.0001" title="优先尝试该价格档位;可空(空=按优先级策略)" />
</div> </div>
</div> </div>
<div class="hero-sms-price-control hero-sms-price-control-reuse"> <div id="row-phone-sms-reuse-control" class="hero-sms-price-control hero-sms-price-control-reuse">
<span class="hero-sms-settings-caption">号码复用</span> <span class="hero-sms-settings-caption">号码复用</span>
<div class="setting-controls hero-sms-toggle-controls"> <div class="setting-controls hero-sms-toggle-controls">
<label class="toggle-switch hero-sms-price-reuse-toggle" for="input-hero-sms-reuse-enabled" title="开启后会优先复用未超次数的可用号码"> <label class="toggle-switch hero-sms-price-reuse-toggle" for="input-hero-sms-reuse-enabled" title="开启后会优先复用未超次数的可用号码">
@@ -1914,6 +1964,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/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>
<script src="../mail-provider-utils.js"></script> <script src="../mail-provider-utils.js"></script>
+731 -184
View File
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,8 @@ const assert = require('node:assert/strict');
const fs = require('node:fs'); const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8'); const source = fs.readFileSync('background.js', 'utf8');
const DEFAULT_MADAO_BASE_URL_FOR_TEST = 'http://127.0.0.1:7822';
const DEFAULT_MADAO_MODE_FOR_TEST = 'routing_plan';
function extractFunction(name) { function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`]; const markers = [`async function ${name}(`, `function ${name}(`];
@@ -65,6 +67,12 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeNexSmsCountryId'), extractFunction('normalizeNexSmsCountryId'),
extractFunction('normalizeNexSmsCountryOrder'), extractFunction('normalizeNexSmsCountryOrder'),
extractFunction('normalizeNexSmsServiceCode'), extractFunction('normalizeNexSmsServiceCode'),
extractFunction('normalizeMaDaoBaseUrl'),
extractFunction('normalizeMaDaoMode'),
extractFunction('normalizeMaDaoIdentifier'),
extractFunction('normalizeMaDaoProviderId'),
extractFunction('normalizeMaDaoCountry'),
extractFunction('normalizeMaDaoPrice'),
extractFunction('normalizePhonePreferredActivation'), extractFunction('normalizePhonePreferredActivation'),
extractFunction('normalizePhoneVerificationReplacementLimit'), extractFunction('normalizePhoneVerificationReplacementLimit'),
extractFunction('normalizePhoneCodeWaitSeconds'), extractFunction('normalizePhoneCodeWaitSeconds'),
@@ -119,8 +127,11 @@ const DEFAULT_HERO_SMS_OPERATOR = 'any';
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms'; const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim'; const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms'; const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms']; const PHONE_SMS_PROVIDER_MADAO = 'madao';
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms', 'madao'];
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS; const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
const DEFAULT_MADAO_MODE = 'routing_plan';
const SIGNUP_METHOD_EMAIL = 'email'; const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone'; const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL; const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
@@ -293,8 +304,12 @@ return {
assert.equal(api.normalizePersistentSettingValue('kiroRsKey', ' key-1 '), 'key-1'); assert.equal(api.normalizePersistentSettingValue('kiroRsKey', ' key-1 '), 'key-1');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim'); assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'NEXSMS'), 'nexsms'); assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'NEXSMS'), 'nexsms');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'MaDao'), 'madao');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms'); assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms');
assert.deepStrictEqual(api.normalizePersistentSettingValue('phoneSmsProviderOrder', ['nexsms', '5sim', 'nexsms']), ['nexsms', '5sim']); assert.deepStrictEqual(
api.normalizePersistentSettingValue('phoneSmsProviderOrder', ['madao', 'nexsms', '5sim', 'nexsms']),
['madao', 'nexsms', '5sim']
);
assert.equal(api.normalizePersistentSettingValue('phoneSmsReuseEnabled', false), false); assert.equal(api.normalizePersistentSettingValue('phoneSmsReuseEnabled', false), false);
assert.equal(api.normalizePersistentSettingValue('phoneSmsReuseEnabled', true), true); assert.equal(api.normalizePersistentSettingValue('phoneSmsReuseEnabled', true), true);
assert.equal(api.normalizePersistentSettingValue('fiveSimApiKey', ' demo-five '), ' demo-five '); assert.equal(api.normalizePersistentSettingValue('fiveSimApiKey', ' demo-five '), ' demo-five ');
@@ -373,6 +388,22 @@ return {
[1, 6] [1, 6]
); );
assert.equal(api.normalizePersistentSettingValue('nexSmsServiceCode', ' OT! '), 'ot'); assert.equal(api.normalizePersistentSettingValue('nexSmsServiceCode', ' OT! '), 'ot');
assert.equal(
api.normalizePersistentSettingValue('madaoBaseUrl', 'http://127.0.0.1:7822/api/acquire?x=1'),
DEFAULT_MADAO_BASE_URL_FOR_TEST
);
assert.equal(api.normalizePersistentSettingValue('madaoBaseUrl', 'ftp://invalid'), DEFAULT_MADAO_BASE_URL_FOR_TEST);
assert.equal(api.normalizePersistentSettingValue('madaoHttpSecret', ' secret-token '), ' secret-token ');
assert.equal(api.normalizePersistentSettingValue('madaoMode', 'direct'), 'direct');
assert.equal(api.normalizePersistentSettingValue('madaoMode', 'unknown'), DEFAULT_MADAO_MODE_FOR_TEST);
assert.equal(api.normalizePersistentSettingValue('madaoRoutingPlanId', ' rp/openai! '), 'rp/openai!');
assert.equal(api.normalizePersistentSettingValue('madaoProviderId', ' Upstream A! '), 'upstreama');
assert.equal(api.normalizePersistentSettingValue('madaoCountry', ' gb '), 'GB');
assert.equal(api.normalizePersistentSettingValue('madaoCountry', 'ANY'), 'any');
assert.equal(api.normalizePersistentSettingValue('madaoAutoPickCountry', 1), true);
assert.equal(api.normalizePersistentSettingValue('madaoReusePhone', 0), false);
assert.equal(api.normalizePersistentSettingValue('madaoMinPrice', '0.123456'), '0.1235');
assert.equal(api.normalizePersistentSettingValue('madaoMaxPrice', '-1'), '');
const rangePayload = api.buildPersistentSettingsPayload({ const rangePayload = api.buildPersistentSettingsPayload({
heroSmsMinPrice: '0.023456', heroSmsMinPrice: '0.023456',
fiveSimMinPrice: '0.0789', fiveSimMinPrice: '0.0789',
@@ -412,6 +412,17 @@ return {
cloudMailToken: 'cloud-token', cloudMailToken: 'cloud-token',
gopayHelperApiKey: 'gpc-key', gopayHelperApiKey: 'gpc-key',
heroSmsApiKey: 'hero-key', heroSmsApiKey: 'hero-key',
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822',
madaoHttpSecret: 'madao-secret',
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'rp-openai',
madaoProviderId: 'upstream-a',
madaoCountry: 'GB',
madaoAutoPickCountry: false,
madaoReusePhone: true,
madaoMinPrice: '0.12',
madaoMaxPrice: '0.45',
hotmailAccounts: [ hotmailAccounts: [
{ id: 'hotmail-1', email: 'owner@example.com', password: 'mail-pass' }, { id: 'hotmail-1', email: 'owner@example.com', password: 'mail-pass' },
], ],
@@ -434,6 +445,17 @@ return {
assert.equal(api.getPayloadInput().cloudMailToken, 'cloud-token'); assert.equal(api.getPayloadInput().cloudMailToken, 'cloud-token');
assert.equal(api.getPayloadInput().gopayHelperApiKey, 'gpc-key'); assert.equal(api.getPayloadInput().gopayHelperApiKey, 'gpc-key');
assert.equal(api.getPayloadInput().heroSmsApiKey, 'hero-key'); assert.equal(api.getPayloadInput().heroSmsApiKey, 'hero-key');
assert.equal(api.getPayloadInput().phoneSmsProvider, 'madao');
assert.equal(api.getPayloadInput().madaoBaseUrl, 'http://127.0.0.1:7822');
assert.equal(api.getPayloadInput().madaoHttpSecret, 'madao-secret');
assert.equal(api.getPayloadInput().madaoMode, 'routing_plan');
assert.equal(api.getPayloadInput().madaoRoutingPlanId, 'rp-openai');
assert.equal(api.getPayloadInput().madaoProviderId, 'upstream-a');
assert.equal(api.getPayloadInput().madaoCountry, 'GB');
assert.equal(api.getPayloadInput().madaoAutoPickCountry, false);
assert.equal(api.getPayloadInput().madaoReusePhone, true);
assert.equal(api.getPayloadInput().madaoMinPrice, '0.12');
assert.equal(api.getPayloadInput().madaoMaxPrice, '0.45');
assert.deepEqual(api.getPayloadInput().hotmailAccounts, [ assert.deepEqual(api.getPayloadInput().hotmailAccounts, [
{ id: 'hotmail-1', email: 'owner@example.com', password: 'mail-pass' }, { id: 'hotmail-1', email: 'owner@example.com', password: 'mail-pass' },
]); ]);
@@ -441,5 +463,6 @@ return {
assert.equal(api.getPayloadInput().paypalAccounts[0].email, 'paypal@example.com'); assert.equal(api.getPayloadInput().paypalAccounts[0].email, 'paypal@example.com');
assert.equal(api.getPersistedUpdates().settingsState.flows.kiro.targets['kiro-rs'].apiKey, 'schema-key'); assert.equal(api.getPersistedUpdates().settingsState.flows.kiro.targets['kiro-rs'].apiKey, 'schema-key');
assert.equal(api.getPersistedUpdates().cloudflareTempEmailAdminAuth, 'admin-secret'); assert.equal(api.getPersistedUpdates().cloudflareTempEmailAdminAuth, 'admin-secret');
assert.equal(api.getPersistedUpdates().madaoRoutingPlanId, 'rp-openai');
assert.deepEqual(api.getPersistOptions(), { replaceExisting: true }); assert.deepEqual(api.getPersistOptions(), { replaceExisting: true });
}); });
@@ -6,6 +6,8 @@ const { readFlowRegistryBundle } = require('./helpers/script-bundles.js');
const flowRegistrySource = readFlowRegistryBundle(); const flowRegistrySource = readFlowRegistryBundle();
const settingsSchemaSource = fs.readFileSync('core/flow-kernel/settings-schema.js', 'utf8'); const settingsSchemaSource = fs.readFileSync('core/flow-kernel/settings-schema.js', 'utf8');
const backgroundSource = fs.readFileSync('background.js', 'utf8'); const backgroundSource = fs.readFileSync('background.js', 'utf8');
const DEFAULT_MADAO_BASE_URL_FOR_TEST = 'http://127.0.0.1:7822';
const DEFAULT_MADAO_MODE_FOR_TEST = 'routing_plan';
function extractFunction(name) { function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`]; const markers = [`async function ${name}(`, `function ${name}(`];
@@ -91,6 +93,8 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
'stepExecutionRangeByFlow', 'stepExecutionRangeByFlow',
]); ]);
const SETTINGS_SCHEMA_VIEW_KEY_SET = new Set(SETTINGS_SCHEMA_VIEW_KEYS); const SETTINGS_SCHEMA_VIEW_KEY_SET = new Set(SETTINGS_SCHEMA_VIEW_KEYS);
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
const DEFAULT_MADAO_MODE = 'routing_plan';
const PERSISTED_SETTING_DEFAULTS = { const PERSISTED_SETTING_DEFAULTS = {
activeFlowId: DEFAULT_ACTIVE_FLOW_ID, activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
targetId: 'cpa', targetId: 'cpa',
@@ -107,6 +111,17 @@ const PERSISTED_SETTING_DEFAULTS = {
ipProxyMode: 'account', ipProxyMode: 'account',
kiroRsUrl: '', kiroRsUrl: '',
kiroRsKey: '', kiroRsKey: '',
phoneSmsProvider: 'hero-sms',
madaoBaseUrl: DEFAULT_MADAO_BASE_URL,
madaoHttpSecret: '',
madaoMode: DEFAULT_MADAO_MODE,
madaoRoutingPlanId: '',
madaoProviderId: '',
madaoCountry: '',
madaoAutoPickCountry: true,
madaoReusePhone: true,
madaoMinPrice: '',
madaoMaxPrice: '',
stepExecutionRangeByFlow: {}, stepExecutionRangeByFlow: {},
}; };
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS); const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
@@ -158,6 +173,68 @@ function normalizeCustomMailHelperBaseUrl(value = '') {
} }
} }
function normalizeStepExecutionRangeByFlow(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; } function normalizeStepExecutionRangeByFlow(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
function normalizePhoneSmsProvider(value = '') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === '5sim' || normalized === 'nexsms' || normalized === 'madao') {
return normalized;
}
return 'hero-sms';
}
function normalizePhoneSmsProviderOrder(value = []) {
const source = Array.isArray(value) ? value : String(value || '').split(/[\\r\\n,]+/);
const seen = new Set();
return source
.map((entry) => normalizePhoneSmsProvider(entry))
.filter((provider) => {
if (!provider || seen.has(provider)) return false;
seen.add(provider);
return true;
})
.slice(0, 4);
}
function normalizeLocalHttpBaseUrl(value = '', fallback = DEFAULT_MADAO_BASE_URL) {
const rawValue = String(value || fallback).trim();
try {
const parsed = new URL(rawValue);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return fallback;
}
return parsed.toString().replace(/\\/$/, '');
} catch {
return fallback;
}
}
function normalizeMaDaoBaseUrl(value = '') {
const normalized = normalizeLocalHttpBaseUrl(value, DEFAULT_MADAO_BASE_URL);
try {
const parsed = new URL(normalized);
parsed.pathname = parsed.pathname.replace(/\\/api\\/(?:acquire|poll|release|routing\\/replace)$/i, '');
parsed.search = '';
parsed.hash = '';
return parsed.toString().replace(/\\/$/, '');
} catch {
return DEFAULT_MADAO_BASE_URL;
}
}
function normalizeMaDaoMode(value = '') { return String(value || '').trim().toLowerCase() === 'direct' ? 'direct' : DEFAULT_MADAO_MODE; }
function normalizeMaDaoIdentifier(value = '') { return String(value || '').trim(); }
function normalizeMaDaoProviderId(value = '') { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); }
function normalizeMaDaoCountry(value = '') {
const trimmed = String(value || '').trim();
if (!trimmed) return '';
const lowered = trimmed.toLowerCase();
if (lowered === 'any' || lowered === 'local') return lowered;
if (/^[a-z]{2}$/i.test(trimmed)) return trimmed.toUpperCase();
return lowered.replace(/[^a-z0-9_-]+/g, '');
}
function normalizeHeroSmsMaxPrice(value = '') {
const rawValue = String(value ?? '').trim();
if (!rawValue) return '';
const numeric = Number(rawValue);
if (!Number.isFinite(numeric) || numeric <= 0) return '';
return String(Math.round(numeric * 10000) / 10000);
}
function normalizeMaDaoPrice(value = '') { return normalizeHeroSmsMaxPrice(value); }
function normalizeIpProxyProviderValue(value) { return String(value || '711proxy').trim() || '711proxy'; } function normalizeIpProxyProviderValue(value) { return String(value || '711proxy').trim() || '711proxy'; }
function normalizeIpProxyMode(value) { return String(value || 'account').trim() || 'account'; } function normalizeIpProxyMode(value) { return String(value || 'account').trim() || 'account'; }
function normalizeIpProxyServiceProfiles(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; } function normalizeIpProxyServiceProfiles(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
@@ -226,6 +303,11 @@ test('buildPersistentSettingsPayload writes canonical settings schema into persi
assert.equal(payload.targetId, 'kiro-rs'); assert.equal(payload.targetId, 'kiro-rs');
assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin'); assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(payload.kiroRsKey, 'secret-key'); assert.equal(payload.kiroRsKey, 'secret-key');
assert.equal(payload.phoneSmsProvider, 'hero-sms');
assert.equal(payload.madaoBaseUrl, DEFAULT_MADAO_BASE_URL_FOR_TEST);
assert.equal(payload.madaoMode, DEFAULT_MADAO_MODE_FOR_TEST);
assert.equal(payload.madaoAutoPickCountry, true);
assert.equal(payload.madaoReusePhone, true);
assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false); assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false);
assert.equal(payload.settingsSchemaVersion, 5); assert.equal(payload.settingsSchemaVersion, 5);
assert.equal(payload.settingsState.activeFlowId, 'kiro'); assert.equal(payload.settingsState.activeFlowId, 'kiro');
@@ -335,6 +417,8 @@ function getRequestedKeys() {
assert.ok(api.getRequestedKeys().includes('plusAccountAccessStrategy')); assert.ok(api.getRequestedKeys().includes('plusAccountAccessStrategy'));
assert.equal(state.settingsSchemaVersion, 5); assert.equal(state.settingsSchemaVersion, 5);
assert.equal(state.settingsState.activeFlowId, 'openai'); assert.equal(state.settingsState.activeFlowId, 'openai');
assert.ok(api.getRequestedKeys().includes('madaoBaseUrl'));
assert.ok(api.getRequestedKeys().includes('madaoMode'));
}); });
test('getPersistedSettings can project schema-only storage back into legacy flat settings', async () => { test('getPersistedSettings can project schema-only storage back into legacy flat settings', async () => {
@@ -629,6 +713,37 @@ function getRemovedKeys() {
assert.equal(Object.prototype.hasOwnProperty.call(write, 'customMailReceiveMode'), false); assert.equal(Object.prototype.hasOwnProperty.call(write, 'customMailReceiveMode'), false);
}); });
test('buildPersistentSettingsPayload persists normalized MaDao flat settings outside canonical settingsState', () => {
const api = buildHarness();
const payload = api.buildPersistentSettingsPayload({
phoneSmsProvider: 'MaDao',
madaoBaseUrl: 'http://127.0.0.1:7822/api/acquire?x=1',
madaoHttpSecret: ' secret-token ',
madaoMode: 'direct',
madaoRoutingPlanId: ' rp-openai ',
madaoProviderId: ' Upstream A! ',
madaoCountry: ' gb ',
madaoAutoPickCountry: 0,
madaoReusePhone: 1,
madaoMinPrice: '0.123456',
madaoMaxPrice: '-1',
});
assert.equal(payload.phoneSmsProvider, 'madao');
assert.equal(payload.madaoBaseUrl, DEFAULT_MADAO_BASE_URL_FOR_TEST);
assert.equal(payload.madaoHttpSecret, ' secret-token ');
assert.equal(payload.madaoMode, 'direct');
assert.equal(payload.madaoRoutingPlanId, 'rp-openai');
assert.equal(payload.madaoProviderId, 'upstreama');
assert.equal(payload.madaoCountry, 'GB');
assert.equal(payload.madaoAutoPickCountry, false);
assert.equal(payload.madaoReusePhone, true);
assert.equal(payload.madaoMinPrice, '0.1235');
assert.equal(payload.madaoMaxPrice, '');
assert.equal(Object.prototype.hasOwnProperty.call(payload.settingsState || {}, 'madaoBaseUrl'), false);
});
test('setPersistentSettings mirrors flat schema updates without resetting other canonical settings', async () => { test('setPersistentSettings mirrors flat schema updates without resetting other canonical settings', async () => {
const api = buildHarness(` const api = buildHarness(`
const persistedWrites = []; const persistedWrites = [];
+212
View File
@@ -0,0 +1,212 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('phone-sms/providers/madao.js', 'utf8');
const api = new Function('self', `${source}; return self.PhoneSmsMaDaoProvider;`)({});
function createJsonResponse(payload, ok = true, status = ok ? 200 : 400, statusText = ok ? 'OK' : 'Bad Request') {
return {
ok,
status,
statusText,
text: async () => (typeof payload === 'string' ? payload : JSON.stringify(payload)),
};
}
test('MaDao direct acquire sends only direct fields and normalizes activation data', async () => {
const requests = [];
const provider = api.createProvider({
fetchImpl: async (url, options = {}) => {
requests.push({ url: new URL(url), options, body: JSON.parse(options.body) });
return createJsonResponse({
ticket_id: 'ticket-1',
phone_number: '+441111111111',
service: 'openai',
country: 'gb',
provider: 'Upstream A!',
price: 0.123456,
status: 'code_received',
});
},
});
const activation = await provider.acquireActivation({
madaoBaseUrl: 'http://127.0.0.1:7822/',
madaoHttpSecret: 'token-1',
madaoMode: 'direct',
madaoRoutingPlanId: 'route-plan-should-not-be-sent',
madaoProviderId: 'Upstream A!',
madaoCountry: 'gb',
madaoAutoPickCountry: 'false',
madaoReusePhone: '1',
madaoMinPrice: '0.01',
madaoMaxPrice: '0.20',
});
assert.equal(requests.length, 1);
assert.equal(requests[0].url.toString(), 'http://127.0.0.1:7822/api/acquire');
assert.equal(requests[0].options.method, 'POST');
assert.equal(requests[0].options.headers.Authorization, 'Bearer token-1');
assert.deepEqual(requests[0].body, {
provider: 'upstreama',
service: 'openai',
auto_pick_country: false,
reuse_phone: true,
country: 'GB',
min_price: 0.01,
max_price: 0.2,
});
assert.equal(activation.provider, 'madao');
assert.equal(activation.activationId, 'ticket-1');
assert.equal(activation.phoneNumber, '+441111111111');
assert.equal(activation.countryId, 'GB');
assert.equal(activation.madaoProviderId, 'upstreama');
assert.equal(activation.madaoPrice, 0.1235);
assert.equal(activation.madaoStatus, 'code_received');
});
test('MaDao routing acquire sends routing plan only', async () => {
const requests = [];
const provider = api.createProvider({
fetchImpl: async (url, options = {}) => {
requests.push({ url: new URL(url), body: JSON.parse(options.body) });
return createJsonResponse({
ticket_id: 'ticket-2',
phone_number: '+442222222222',
routing_plan_id: 'rp-openai',
routing_item_id: 'route-1',
});
},
});
const activation = await provider.acquireActivation({
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'rp-openai',
madaoProviderId: 'direct-provider',
madaoCountry: 'TH',
madaoAutoPickCountry: false,
madaoReusePhone: false,
madaoMinPrice: '0.01',
madaoMaxPrice: '0.20',
});
assert.deepEqual(requests[0].body, {
provider: 'auto',
service: 'openai',
routing_plan_id: 'rp-openai',
});
assert.equal(activation.madaoRoutingPlanId, 'rp-openai');
assert.equal(activation.madaoRoutingItemId, 'route-1');
});
test('MaDao poll extracts codes from nested messages and reports pending status', async () => {
const statusEvents = [];
const provider = api.createProvider({
fetchImpl: async (_url, options = {}) => {
const body = JSON.parse(options.body);
if (body.ticket_id === 'pending-ticket') {
return createJsonResponse({ status: 'waiting_code', message: 'still waiting' });
}
return createJsonResponse({
messages: [
{ text: 'old text without code' },
{ body: 'OpenAI verification code: 765432' },
],
});
},
});
const pending = await provider.pollActivationCode({}, { activationId: 'pending-ticket' }, {
onStatus: async (event) => statusEvents.push(event),
});
const code = await provider.pollActivationCode({}, { activationId: 'ready-ticket' });
assert.equal(pending, '');
assert.equal(statusEvents.length, 1);
assert.equal(statusEvents[0].statusText, 'still waiting');
assert.equal(code, '765432');
});
test('MaDao direct rotate releases the current ticket without acquiring a new one', async () => {
const requests = [];
const provider = api.createProvider({
fetchImpl: async (url, options = {}) => {
requests.push({ url: new URL(url), body: JSON.parse(options.body) });
return createJsonResponse({ released: true });
},
});
const result = await provider.rotateActivation(
{ madaoMode: 'direct' },
{ activationId: 'ticket-old', phoneNumber: '+441111111111' },
{ releaseAction: 'ban' }
);
assert.deepEqual(
requests.map((request) => request.url.pathname),
['/api/release']
);
assert.deepEqual(requests[0].body, { ticket_id: 'ticket-old', action: 'ban' });
assert.equal(result.currentTicketId, 'ticket-old');
assert.equal(result.nextActivation, null);
});
test('MaDao routing rotate rejects invalid replacement payloads', async () => {
const provider = api.createProvider({
fetchImpl: async () => createJsonResponse({
current_ticket_id: 'ticket-old',
next_ticket: {
ticket_id: 'ticket-new',
},
}),
});
await assert.rejects(
() => provider.rotateActivation(
{ madaoMode: 'routing_plan' },
{
activationId: 'ticket-old',
phoneNumber: '+441111111111',
madaoRoutingPlanId: 'rp-openai',
madaoRoutingItemId: 'route-old',
},
{ releaseAction: 'ban', reason: 'phone_number_used' }
),
/MaDao/
);
});
test('MaDao provider surfaces HTTP errors and request timeouts', async () => {
const httpProvider = api.createProvider({
fetchImpl: async () => createJsonResponse({ error: 'upstream unavailable' }, false, 503, 'Service Unavailable'),
});
await assert.rejects(
() => httpProvider.acquireActivation({}),
(error) => {
assert.equal(error.status, 503);
assert.equal(error.payload.error, 'upstream unavailable');
assert.match(error.message, /upstream unavailable/);
return true;
}
);
const timeoutProvider = api.createProvider({
fetchImpl: async (_url, options = {}) => {
await new Promise((resolve, reject) => {
options.signal.addEventListener('abort', () => {
const error = new Error('aborted');
error.name = 'AbortError';
reject(error);
});
});
},
requestTimeoutMs: 1,
});
await assert.rejects(
() => timeoutProvider.acquireActivation({}),
/MaDao/
);
});
+16 -4
View File
@@ -16,29 +16,41 @@ test('phone sms provider registry normalizes ids, order and labels consistently'
PhoneSmsFiveSimProvider: { PhoneSmsFiveSimProvider: {
createProvider: (deps = {}) => ({ provider: '5sim', deps }), createProvider: (deps = {}) => ({ provider: '5sim', deps }),
}, },
PhoneSmsMaDaoProvider: {
createProvider: (deps = {}) => ({ provider: 'madao', deps }),
},
}); });
assert.deepStrictEqual(registry.getProviderIds(), ['hero-sms', '5sim', 'nexsms']); assert.deepStrictEqual(registry.getProviderIds(), ['hero-sms', '5sim', 'nexsms', 'madao']);
assert.equal(registry.normalizeProviderId(' NEXSMS '), 'nexsms'); assert.equal(registry.normalizeProviderId(' NEXSMS '), 'nexsms');
assert.equal(registry.normalizeProviderId(' MaDao '), 'madao');
assert.equal(registry.normalizeProviderId('unknown-provider'), 'hero-sms'); assert.equal(registry.normalizeProviderId('unknown-provider'), 'hero-sms');
assert.equal(registry.getProviderLabel('nexsms'), 'NexSMS'); assert.equal(registry.getProviderLabel('nexsms'), 'NexSMS');
assert.equal(registry.getProviderLabel('madao'), 'MaDao');
assert.equal(registry.getProviderDefinition('nexsms').moduleKey, 'PhoneSmsNexSmsProvider'); assert.equal(registry.getProviderDefinition('nexsms').moduleKey, 'PhoneSmsNexSmsProvider');
assert.equal(registry.getProviderDefinition('madao').moduleKey, 'PhoneSmsMaDaoProvider');
assert.deepStrictEqual( assert.deepStrictEqual(
registry.normalizeProviderOrder([ registry.normalizeProviderOrder([
{ provider: 'madao' },
{ provider: 'nexsms' }, { provider: 'nexsms' },
{ id: '5sim' }, { id: '5sim' },
{ value: 'hero-sms' }, { value: 'hero-sms' },
'MADAO',
'NEXSMS', 'NEXSMS',
]), ]),
['nexsms', '5sim', 'hero-sms'] ['madao', 'nexsms', '5sim', 'hero-sms']
); );
assert.deepStrictEqual( assert.deepStrictEqual(
registry.normalizeProviderOrder([], ['nexsms', '5sim', 'nexsms']), registry.normalizeProviderOrder([], ['madao', 'nexsms', '5sim', 'nexsms']),
['nexsms', '5sim'] ['madao', 'nexsms', '5sim']
); );
assert.deepStrictEqual( assert.deepStrictEqual(
registry.createProvider('5sim', { foo: 1 }), registry.createProvider('5sim', { foo: 1 }),
{ provider: '5sim', deps: { foo: 1 } } { provider: '5sim', deps: { foo: 1 } }
); );
assert.deepStrictEqual(
registry.createProvider('madao', { foo: 2 }),
{ provider: 'madao', deps: { foo: 2 } }
);
assert.throws(() => registry.createProvider('nexsms'), /接码平台模块未加载:nexsms/); assert.throws(() => registry.createProvider('nexsms'), /接码平台模块未加载:nexsms/);
}); });
+364
View File
@@ -5,6 +5,8 @@ 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 globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(globalScope); const api = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(globalScope);
const maDaoSource = fs.readFileSync('phone-sms/providers/madao.js', 'utf8');
const maDaoModule = new Function('self', `${maDaoSource}; return self.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({
@@ -155,6 +157,101 @@ 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 acquires, polls and releases MaDao activation through provider adapter', async () => {
const requests = [];
let currentState = {
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822/',
madaoHttpSecret: 'secret-token',
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'rp-openai',
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 2,
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
createMaDaoProvider: maDaoModule.createProvider,
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-1',
phone_number: '+441111111111',
service: 'openai',
country: 'GB',
provider: 'upstream-a',
routing_plan_id: 'rp-openai',
routing_item_id: 'route-1',
status: 'waiting_code',
}),
};
}
if (parsedUrl.pathname === '/api/poll') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: 'madao-1',
status: 'code_received',
code: '123456',
}),
};
}
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 () => ({}),
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const activation = await helpers.requestPhoneActivation(currentState);
const code = await helpers.pollPhoneActivationCode(currentState, activation, {
timeoutMs: 15000,
intervalMs: 1000,
maxRounds: 1,
});
await helpers.finalizeSignupPhoneActivationAfterSuccess(currentState, activation);
assert.equal(activation.provider, 'madao');
assert.equal(activation.activationId, 'madao-1');
assert.equal(activation.phoneNumber, '+441111111111');
assert.equal(activation.countryId, 'GB');
assert.equal(activation.madaoRoutingPlanId, 'rp-openai');
assert.equal(activation.madaoRoutingItemId, 'route-1');
assert.equal(code, '123456');
assert.deepStrictEqual(
requests.map((entry) => entry.url.pathname),
['/api/acquire', '/api/poll', '/api/release']
);
assert.deepStrictEqual(requests[0].body, {
provider: 'auto',
service: 'openai',
routing_plan_id: 'rp-openai',
});
assert.equal(requests[0].options.headers.Authorization, 'Bearer secret-token');
assert.deepStrictEqual(requests[1].body, { ticket_id: 'madao-1' });
assert.deepStrictEqual(requests[2].body, { ticket_id: 'madao-1', action: 'finish' });
});
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 = {
@@ -8722,6 +8819,273 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
); );
}); });
test('phone verification helper uses MaDao routing replace when add-phone rejects current number', async () => {
const requests = [];
const submittedPhones = [];
let currentState = {
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822',
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'rp-openai',
verificationResendCount: 0,
phoneVerificationReplacementLimit: 2,
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
createMaDaoProvider: maDaoModule.createProvider,
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url);
requests.push({ url: parsedUrl, 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-old',
phone_number: '+441111111111',
service: 'openai',
country: 'GB',
routing_plan_id: 'rp-openai',
routing_item_id: 'route-old',
status: 'waiting_code',
}),
};
}
if (parsedUrl.pathname === '/api/routing/replace') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
current_ticket_id: 'madao-old',
next_ticket: {
ticket_id: 'madao-new',
phone_number: '+442222222222',
service: 'openai',
country: 'GB',
routing_plan_id: 'rp-openai',
routing_item_id: 'route-new',
status: 'waiting_code',
},
}),
};
}
if (parsedUrl.pathname === '/api/poll') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: 'madao-new',
status: 'code_received',
code: '654321',
}),
};
}
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) => {
if (message.type === 'SUBMIT_PHONE_NUMBER') {
submittedPhones.push(message.payload.phoneNumber);
if (message.payload.phoneNumber === '+441111111111') {
return {
addPhoneRejected: true,
errorText: 'This phone number is already linked to another account',
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
};
}
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
assert.equal(message.payload.code, '654321');
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
}
if (message.type === 'RETURN_TO_ADD_PHONE' || message.type === 'STEP8_GET_STATE') {
return { addPhonePage: true, phoneVerificationPage: false, url: 'https://auth.openai.com/add-phone' };
}
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.deepStrictEqual(submittedPhones, ['+441111111111', '+442222222222']);
assert.deepStrictEqual(
requests.map((entry) => entry.url.pathname),
['/api/acquire', '/api/routing/replace', '/api/poll', '/api/release']
);
assert.deepStrictEqual(requests[1].body, {
ticket_id: 'madao-old',
release_action: 'ban',
failed_item_id: 'route-old',
reason: 'phone_number_used',
});
assert.deepStrictEqual(requests[2].body, { ticket_id: 'madao-new' });
assert.deepStrictEqual(requests[3].body, { ticket_id: 'madao-new', action: 'finish' });
assert.equal(currentState.currentPhoneActivation, null);
});
test('phone verification helper reacquires MaDao direct numbers after releasing rejected number', async () => {
const requests = [];
const submittedPhones = [];
let acquireCount = 0;
let currentState = {
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822',
madaoMode: 'direct',
madaoRoutingPlanId: 'rp-stale',
madaoProviderId: 'upstream-a',
madaoCountry: 'gb',
madaoAutoPickCountry: false,
madaoReusePhone: true,
madaoMinPrice: '0.01',
madaoMaxPrice: '0.2',
verificationResendCount: 0,
phoneVerificationReplacementLimit: 2,
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
createMaDaoProvider: maDaoModule.createProvider,
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url);
requests.push({ url: parsedUrl, body: options.body ? JSON.parse(options.body) : null });
if (parsedUrl.pathname === '/api/acquire') {
acquireCount += 1;
const isFirst = acquireCount === 1;
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: isFirst ? 'madao-direct-old' : 'madao-direct-new',
phone_number: isFirst ? '+441111111111' : '+442222222222',
service: 'openai',
country: 'GB',
provider: 'upstream-a',
status: 'waiting_code',
}),
};
}
if (parsedUrl.pathname === '/api/release') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({ ok: true }),
};
}
if (parsedUrl.pathname === '/api/poll') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: 'madao-direct-new',
status: 'code_received',
code: '246810',
}),
};
}
throw new Error(`Unexpected MaDao path: ${parsedUrl.pathname}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'SUBMIT_PHONE_NUMBER') {
submittedPhones.push(message.payload.phoneNumber);
if (message.payload.phoneNumber === '+441111111111') {
return {
addPhoneRejected: true,
errorText: 'This phone number is already linked to another account',
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
};
}
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
assert.equal(message.payload.code, '246810');
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
}
if (message.type === 'RETURN_TO_ADD_PHONE' || message.type === 'STEP8_GET_STATE') {
return { addPhonePage: true, phoneVerificationPage: false, url: 'https://auth.openai.com/add-phone' };
}
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.deepStrictEqual(submittedPhones, ['+441111111111', '+442222222222']);
assert.deepStrictEqual(
requests.map((entry) => entry.url.pathname),
['/api/acquire', '/api/release', '/api/acquire', '/api/poll', '/api/release']
);
assert.deepStrictEqual(requests[0].body, {
provider: 'upstream-a',
service: 'openai',
auto_pick_country: false,
reuse_phone: true,
country: 'GB',
min_price: 0.01,
max_price: 0.2,
});
assert.equal(Object.prototype.hasOwnProperty.call(requests[0].body, 'routing_plan_id'), false);
assert.deepStrictEqual(requests[1].body, { ticket_id: 'madao-direct-old', action: 'ban' });
assert.deepStrictEqual(requests[3].body, { ticket_id: 'madao-direct-new' });
assert.deepStrictEqual(requests[4].body, { ticket_id: 'madao-direct-new', action: 'finish' });
assert.equal(currentState.currentPhoneActivation, null);
});
test('phone verification helper keeps 5sim reusable activation on the original order', async () => { test('phone verification helper keeps 5sim reusable activation on the original order', async () => {
const requests = []; const requests = [];
let checkCount = 0; let checkCount = 0;
@@ -88,9 +88,11 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
assert.match(html, /id="select-phone-sms-provider"/); assert.match(html, /id="select-phone-sms-provider"/);
assert.match(html, /\.\.\/phone-sms\/providers\/hero-sms\.js/); assert.match(html, /\.\.\/phone-sms\/providers\/hero-sms\.js/);
assert.match(html, /\.\.\/phone-sms\/providers\/five-sim\.js/); assert.match(html, /\.\.\/phone-sms\/providers\/five-sim\.js/);
assert.match(html, /\.\.\/phone-sms\/providers\/madao\.js/);
assert.match(html, /\.\.\/phone-sms\/providers\/registry\.js/); assert.match(html, /\.\.\/phone-sms\/providers\/registry\.js/);
assert.match(html, /<option value="hero-sms">HeroSMS<\/option>/); assert.match(html, /<option value="hero-sms">HeroSMS<\/option>/);
assert.match(html, /<option value="5sim">5sim<\/option>/); assert.match(html, /<option value="5sim">5sim<\/option>/);
assert.match(html, /<option value="madao">MaDao<\/option>/);
assert.match(html, /id="row-hero-sms-country"/); assert.match(html, /id="row-hero-sms-country"/);
assert.match(html, /id="row-hero-sms-country-fallback"/); assert.match(html, /id="row-hero-sms-country-fallback"/);
assert.match(html, /id="row-hero-sms-acquire-priority"/); assert.match(html, /id="row-hero-sms-acquire-priority"/);
@@ -147,6 +149,26 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
assert.match(html, /id="row-nex-sms-country-fallback"/); assert.match(html, /id="row-nex-sms-country-fallback"/);
assert.match(html, /id="row-nex-sms-service-code"/); assert.match(html, /id="row-nex-sms-service-code"/);
assert.match(html, /id="input-nex-sms-service-code"/); assert.match(html, /id="input-nex-sms-service-code"/);
assert.match(html, /id="row-madao-base-url"/);
assert.match(html, /id="input-madao-base-url"/);
assert.match(html, /id="row-madao-http-secret"/);
assert.match(html, /id="input-madao-http-secret"/);
assert.match(html, /id="row-madao-mode"/);
assert.match(html, /id="select-madao-mode"/);
assert.match(html, /id="row-madao-routing-plan-id"/);
assert.match(html, /id="input-madao-routing-plan-id"/);
assert.match(html, /id="row-madao-provider-id"/);
assert.match(html, /id="input-madao-provider-id"/);
assert.match(html, /id="row-madao-country"/);
assert.match(html, /id="input-madao-country"/);
assert.match(html, /id="row-madao-auto-pick-country"/);
assert.match(html, /id="input-madao-auto-pick-country"/);
assert.match(html, /id="row-madao-reuse-phone"/);
assert.match(html, /id="input-madao-reuse-phone"/);
assert.match(html, /id="row-madao-price-range"/);
assert.match(html, /id="input-madao-min-price"/);
assert.match(html, /id="input-madao-max-price"/);
assert.doesNotMatch(html, /id="btn-open-madao-github"/);
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/); assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
}); });
@@ -623,19 +645,21 @@ const rowPhoneSmsProvider = { style: { display: 'none' } };
const rowPhoneSmsProviderOrder = { style: { display: 'none' } }; const rowPhoneSmsProviderOrder = { style: { display: 'none' } };
const rowPhoneSmsProviderOrderActions = { style: { display: 'none' } }; const rowPhoneSmsProviderOrderActions = { style: { display: 'none' } };
const selectPhoneSmsProvider = { value: 'hero-sms' }; const selectPhoneSmsProvider = { value: 'hero-sms' };
const selectMaDaoMode = { value: 'routing_plan' };
const btnTogglePhoneVerificationSection = { const btnTogglePhoneVerificationSection = {
disabled: false, disabled: false,
textContent: '', textContent: '',
title: '', title: '',
setAttribute: () => {}, setAttribute: () => {},
}; };
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms']; const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms', 'madao'];
const phoneSmsProviderOrderSelection = []; const phoneSmsProviderOrderSelection = [];
function getPhoneSmsProviderCount() { return DEFAULT_PHONE_SMS_PROVIDER_ORDER.length; }
function normalizePhoneSmsProviderOrderValue(value = [], fallbackOrder = DEFAULT_PHONE_SMS_PROVIDER_ORDER) { function normalizePhoneSmsProviderOrderValue(value = [], fallbackOrder = DEFAULT_PHONE_SMS_PROVIDER_ORDER) {
const source = Array.isArray(value) ? value : []; const source = Array.isArray(value) ? value : [];
const normalized = [...source]; const normalized = [...source];
if (normalized.length) { if (normalized.length) {
return normalized.slice(0, 3); return normalized.slice(0, getPhoneSmsProviderCount());
} }
if (!Array.isArray(fallbackOrder) || !fallbackOrder.length) { if (!Array.isArray(fallbackOrder) || !fallbackOrder.length) {
return []; return [];
@@ -646,7 +670,7 @@ const btnTogglePhoneVerificationSection = {
fallbackNormalized.push(provider); fallbackNormalized.push(provider);
} }
} }
return fallbackNormalized.slice(0, 3); return fallbackNormalized.slice(0, getPhoneSmsProviderCount());
} }
function resolveNormalizedProviderOrderForRuntime(state = {}) { function resolveNormalizedProviderOrderForRuntime(state = {}) {
const rawOrder = Array.isArray(state?.phoneSmsProviderOrder) ? state.phoneSmsProviderOrder : []; const rawOrder = Array.isArray(state?.phoneSmsProviderOrder) ? state.phoneSmsProviderOrder : [];
@@ -674,6 +698,15 @@ const rowNexSmsApiKey = { style: { display: 'none' } };
const rowNexSmsCountry = { style: { display: 'none' } }; const rowNexSmsCountry = { style: { display: 'none' } };
const rowNexSmsCountryFallback = { style: { display: 'none' } }; const rowNexSmsCountryFallback = { style: { display: 'none' } };
const rowNexSmsServiceCode = { style: { display: 'none' } }; const rowNexSmsServiceCode = { style: { display: 'none' } };
const rowMaDaoBaseUrl = { style: { display: 'none' } };
const rowMaDaoHttpSecret = { style: { display: 'none' } };
const rowMaDaoMode = { style: { display: 'none' } };
const rowMaDaoRoutingPlanId = { style: { display: 'none' } };
const rowMaDaoProviderId = { style: { display: 'none' } };
const rowMaDaoCountry = { style: { display: 'none' } };
const rowMaDaoAutoPickCountry = { style: { display: 'none' } };
const rowMaDaoReusePhone = { style: { display: 'none' } };
const rowMaDaoPriceRange = { style: { display: 'none' } };
const rowHeroSmsRuntimePair = { style: { display: 'none' } }; const rowHeroSmsRuntimePair = { style: { display: 'none' } };
const rowHeroSmsCurrentNumber = { style: { display: 'none' } }; const rowHeroSmsCurrentNumber = { style: { display: 'none' } };
const rowHeroSmsCurrentCountdown = { style: { display: 'none' } }; const rowHeroSmsCurrentCountdown = { style: { display: 'none' } };
@@ -689,6 +722,8 @@ const rowPhoneCodePollMaxRounds = { style: { display: 'none' } };
const rowFreePhoneReuseEnabled = createMockRow(); const rowFreePhoneReuseEnabled = createMockRow();
const rowFreePhoneReuseAutoEnabled = createMockRow(); const rowFreePhoneReuseAutoEnabled = createMockRow();
const rowFreeReusablePhone = createMockRow(); const rowFreeReusablePhone = createMockRow();
const rowPhoneSmsPreferredPriceControl = { style: { display: 'none' } };
const rowPhoneSmsReuseControl = { style: { display: 'none' } };
const heroSmsReuseRow = createMockRow(); const heroSmsReuseRow = createMockRow();
const inputHeroSmsReuseEnabled = { checked: true, disabled: false, closest: () => heroSmsReuseRow }; const inputHeroSmsReuseEnabled = { checked: true, disabled: false, closest: () => heroSmsReuseRow };
const inputFreePhoneReuseEnabled = { checked: true, disabled: false }; const inputFreePhoneReuseEnabled = { checked: true, disabled: false };
@@ -698,10 +733,75 @@ const inputFreeReusablePhone = { disabled: false };
const btnSaveFreeReusablePhone = { disabled: false }; const btnSaveFreeReusablePhone = { disabled: false };
const btnClearFreeReusablePhone = { disabled: false }; const btnClearFreeReusablePhone = { disabled: false };
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms'; 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_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms'; const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const PHONE_SMS_PROVIDER_MADAO = 'madao';
const MADAO_MODE_ROUTING_PLAN = 'routing_plan';
const MADAO_MODE_DIRECT = 'direct';
const DEFAULT_MADAO_MODE = MADAO_MODE_ROUTING_PLAN;
const PHONE_SMS_PROVIDER_UI_DESCRIPTORS = ${JSON.stringify({
'hero-sms': {
rowKeys: [
'rowHeroSmsCountry',
'rowHeroSmsCountryFallback',
'rowHeroSmsAcquirePriority',
'rowHeroSmsOperator',
'rowHeroSmsApiKey',
'rowHeroSmsMaxPrice',
],
priceControlKeys: [
'rowPhoneSmsPreferredPriceControl',
'rowPhoneSmsReuseControl',
],
},
'5sim': {
rowKeys: [
'rowFiveSimApiKey',
'rowFiveSimCountry',
'rowFiveSimCountryFallback',
'rowFiveSimOperator',
'rowFiveSimProduct',
'rowHeroSmsMaxPrice',
],
},
nexsms: {
rowKeys: [
'rowNexSmsApiKey',
'rowNexSmsCountry',
'rowNexSmsCountryFallback',
'rowNexSmsServiceCode',
],
},
madao: {
rowKeys: [
'rowMaDaoBaseUrl',
'rowMaDaoHttpSecret',
'rowMaDaoMode',
],
routingRowKeys: [
'rowMaDaoRoutingPlanId',
],
directRowKeys: [
'rowMaDaoProviderId',
'rowMaDaoCountry',
'rowMaDaoAutoPickCountry',
'rowMaDaoReusePhone',
'rowMaDaoPriceRange',
],
},
})};
function getSelectedPhoneSmsProvider() { return selectPhoneSmsProvider.value; } function getSelectedPhoneSmsProvider() { return selectPhoneSmsProvider.value; }
function isFiveSimProviderSelected() { return getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM; } function isFiveSimProviderSelected() { return getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM; }
function normalizePhoneSmsProviderValue(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return DEFAULT_PHONE_SMS_PROVIDER_ORDER.includes(normalized) ? normalized : PHONE_SMS_PROVIDER_HERO_SMS;
}
function normalizeMaDaoModeValue(value = '') { return String(value || '').trim().toLowerCase() === MADAO_MODE_DIRECT ? MADAO_MODE_DIRECT : DEFAULT_MADAO_MODE; }
${extractFunction('getPhoneSmsProviderUiRowMap')}
${extractFunction('getProviderUiRows')}
${extractFunction('getAllProviderUiRows')}
${extractFunction('updateProviderPriceControls')}
function updateHeroSmsPlatformDisplay() {} function updateHeroSmsPlatformDisplay() {}
function updateSignupMethodUI() { function updateSignupMethodUI() {
rowSignupMethod.style.display = inputPhoneVerificationEnabled.checked ? '' : 'none'; rowSignupMethod.style.display = inputPhoneVerificationEnabled.checked ? '' : 'none';
@@ -756,6 +856,18 @@ return {
rowNexSmsCountry, rowNexSmsCountry,
rowNexSmsCountryFallback, rowNexSmsCountryFallback,
rowNexSmsServiceCode, rowNexSmsServiceCode,
rowMaDaoBaseUrl,
rowMaDaoHttpSecret,
rowMaDaoMode,
rowMaDaoRoutingPlanId,
rowMaDaoProviderId,
rowMaDaoCountry,
rowMaDaoAutoPickCountry,
rowMaDaoReusePhone,
rowMaDaoPriceRange,
rowPhoneSmsPreferredPriceControl,
rowPhoneSmsReuseControl,
selectMaDaoMode,
rowHeroSmsRuntimePair, rowHeroSmsRuntimePair,
rowHeroSmsCurrentNumber, rowHeroSmsCurrentNumber,
rowHeroSmsCurrentCountdown, rowHeroSmsCurrentCountdown,
@@ -823,6 +935,15 @@ return {
assert.equal(api.rowNexSmsCountry.style.display, 'none'); assert.equal(api.rowNexSmsCountry.style.display, 'none');
assert.equal(api.rowNexSmsCountryFallback.style.display, 'none'); assert.equal(api.rowNexSmsCountryFallback.style.display, 'none');
assert.equal(api.rowNexSmsServiceCode.style.display, 'none'); assert.equal(api.rowNexSmsServiceCode.style.display, 'none');
assert.equal(api.rowMaDaoBaseUrl.style.display, 'none');
assert.equal(api.rowMaDaoHttpSecret.style.display, 'none');
assert.equal(api.rowMaDaoMode.style.display, 'none');
assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none');
assert.equal(api.rowMaDaoProviderId.style.display, 'none');
assert.equal(api.rowMaDaoCountry.style.display, 'none');
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
assert.equal(api.rowMaDaoPriceRange.style.display, 'none');
api.inputPhoneVerificationEnabled.checked = true; api.inputPhoneVerificationEnabled.checked = true;
api.setLatestState({ signupPhoneNumber: '66959916439' }); api.setLatestState({ signupPhoneNumber: '66959916439' });
@@ -853,6 +974,8 @@ return {
assert.equal(api.rowHeroSmsOperator.style.display, ''); assert.equal(api.rowHeroSmsOperator.style.display, '');
assert.equal(api.rowHeroSmsApiKey.style.display, ''); assert.equal(api.rowHeroSmsApiKey.style.display, '');
assert.equal(api.rowHeroSmsMaxPrice.style.display, ''); assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
assert.equal(api.rowPhoneSmsPreferredPriceControl.style.display, '');
assert.equal(api.rowPhoneSmsReuseControl.style.display, '');
assert.equal(api.rowFiveSimOperator.style.display, 'none'); assert.equal(api.rowFiveSimOperator.style.display, 'none');
assert.equal(api.rowHeroSmsCurrentNumber.style.display, ''); assert.equal(api.rowHeroSmsCurrentNumber.style.display, '');
assert.equal(api.rowHeroSmsCurrentCountdown.style.display, ''); assert.equal(api.rowHeroSmsCurrentCountdown.style.display, '');
@@ -913,6 +1036,8 @@ return {
assert.equal(api.rowFiveSimCountryFallback.style.display, ''); assert.equal(api.rowFiveSimCountryFallback.style.display, '');
assert.equal(api.rowFiveSimOperator.style.display, ''); assert.equal(api.rowFiveSimOperator.style.display, '');
assert.equal(api.rowFiveSimProduct.style.display, ''); assert.equal(api.rowFiveSimProduct.style.display, '');
assert.equal(api.rowPhoneSmsPreferredPriceControl.style.display, 'none');
assert.equal(api.rowPhoneSmsReuseControl.style.display, 'none');
api.setSelectedPhoneSmsProvider('nexsms'); api.setSelectedPhoneSmsProvider('nexsms');
api.updatePhoneVerificationSettingsUI(); api.updatePhoneVerificationSettingsUI();
@@ -920,6 +1045,33 @@ return {
assert.equal(api.rowNexSmsCountry.style.display, ''); assert.equal(api.rowNexSmsCountry.style.display, '');
assert.equal(api.rowNexSmsCountryFallback.style.display, ''); assert.equal(api.rowNexSmsCountryFallback.style.display, '');
assert.equal(api.rowNexSmsServiceCode.style.display, ''); assert.equal(api.rowNexSmsServiceCode.style.display, '');
assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
assert.equal(api.rowFiveSimOperator.style.display, 'none');
api.setSelectedPhoneSmsProvider('madao');
api.selectMaDaoMode.value = 'routing_plan';
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowMaDaoBaseUrl.style.display, '');
assert.equal(api.rowMaDaoHttpSecret.style.display, '');
assert.equal(api.rowMaDaoMode.style.display, '');
assert.equal(api.rowMaDaoRoutingPlanId.style.display, '');
assert.equal(api.rowMaDaoProviderId.style.display, 'none');
assert.equal(api.rowMaDaoCountry.style.display, 'none');
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
assert.equal(api.rowMaDaoPriceRange.style.display, 'none');
api.selectMaDaoMode.value = 'direct';
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowMaDaoBaseUrl.style.display, '');
assert.equal(api.rowMaDaoHttpSecret.style.display, '');
assert.equal(api.rowMaDaoMode.style.display, '');
assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none');
assert.equal(api.rowMaDaoProviderId.style.display, '');
assert.equal(api.rowMaDaoCountry.style.display, '');
assert.equal(api.rowMaDaoAutoPickCountry.style.display, '');
assert.equal(api.rowMaDaoReusePhone.style.display, '');
assert.equal(api.rowMaDaoPriceRange.style.display, '');
}); });
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => { test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
@@ -1005,6 +1157,16 @@ const inputFiveSimOperator = { value: 'any' };
const inputFiveSimProduct = { value: 'openai' }; const inputFiveSimProduct = { value: 'openai' };
const inputNexSmsApiKey = { value: 'nex-key' }; const inputNexSmsApiKey = { value: 'nex-key' };
const inputNexSmsServiceCode = { value: 'ot' }; const inputNexSmsServiceCode = { value: 'ot' };
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/acquire' };
const inputMaDaoHttpSecret = { value: 'madao-secret' };
const selectMaDaoMode = { value: 'direct' };
const inputMaDaoRoutingPlanId = { value: 'plan-1' };
const inputMaDaoProviderId = { value: 'Local Provider!!' };
const inputMaDaoCountry = { value: 'th' };
const inputMaDaoAutoPickCountry = { checked: false };
const inputMaDaoReusePhone = { checked: true };
const inputMaDaoMinPrice = { value: '0.02' };
const inputMaDaoMaxPrice = { value: '0.2' };
const inputHeroSmsReuseEnabled = { checked: true }; const inputHeroSmsReuseEnabled = { checked: true };
const selectHeroSmsAcquirePriority = { value: 'price' }; const selectHeroSmsAcquirePriority = { value: 'price' };
const selectHeroSmsOperator = { value: 'AIS!!' }; const selectHeroSmsOperator = { value: 'AIS!!' };
@@ -1052,9 +1214,15 @@ const PHONE_REPLACEMENT_LIMIT_MAX = 20;
const DEFAULT_HERO_SMS_COUNTRY_ID = 52; const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand'; const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms'; 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_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms'; const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const PHONE_SMS_PROVIDER_MADAO = 'madao';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS; const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
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 SIGNUP_METHOD_EMAIL = 'email'; const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone'; const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL; const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
@@ -1093,6 +1261,12 @@ function normalizePlusAccountAccessStrategy(value = '') { return String(value ||
function resolvePlusAccountAccessStrategyForTarget(value = '') { return normalizePlusAccountAccessStrategy(value); } function resolvePlusAccountAccessStrategyForTarget(value = '') { return normalizePlusAccountAccessStrategy(value); }
${extractFunction('normalizePhoneSmsProvider')} ${extractFunction('normalizePhoneSmsProvider')}
${extractFunction('normalizePhoneSmsProviderValue')} ${extractFunction('normalizePhoneSmsProviderValue')}
${extractFunction('normalizeMaDaoBaseUrlValue')}
${extractFunction('normalizeMaDaoModeValue')}
${extractFunction('normalizeMaDaoIdentifierValue')}
${extractFunction('normalizeMaDaoProviderIdValue')}
${extractFunction('normalizeMaDaoCountry')}
${extractFunction('normalizeMaDaoPriceValue')}
${extractFunction('normalizeFiveSimCountryCode')} ${extractFunction('normalizeFiveSimCountryCode')}
${extractFunction('normalizeFiveSimCountryOrderValue')} ${extractFunction('normalizeFiveSimCountryOrderValue')}
${extractFunction('normalizeFiveSimProductValue')} ${extractFunction('normalizeFiveSimProductValue')}
@@ -1157,6 +1331,16 @@ return { collectSettingsPayload };
assert.equal(payload.nexSmsApiKey, 'nex-key'); assert.equal(payload.nexSmsApiKey, 'nex-key');
assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]); assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]);
assert.equal(payload.nexSmsServiceCode, 'ot'); assert.equal(payload.nexSmsServiceCode, 'ot');
assert.equal(payload.madaoBaseUrl, 'http://127.0.0.1:7822');
assert.equal(payload.madaoHttpSecret, 'madao-secret');
assert.equal(payload.madaoMode, 'direct');
assert.equal(payload.madaoRoutingPlanId, 'plan-1');
assert.equal(payload.madaoProviderId, 'localprovider');
assert.equal(payload.madaoCountry, 'TH');
assert.equal(payload.madaoAutoPickCountry, false);
assert.equal(payload.madaoReusePhone, true);
assert.equal(payload.madaoMinPrice, '0.02');
assert.equal(payload.madaoMaxPrice, '0.2');
assert.equal(payload.phoneSmsReuseEnabled, false); assert.equal(payload.phoneSmsReuseEnabled, false);
assert.equal(payload.heroSmsReuseEnabled, false); assert.equal(payload.heroSmsReuseEnabled, false);
assert.equal(payload.freePhoneReuseEnabled, false); assert.equal(payload.freePhoneReuseEnabled, false);
@@ -1192,8 +1376,22 @@ test('switchPhoneSmsProvider saves API keys independently when the select value
const api = new Function(` const api = new Function(`
let latestState = { let latestState = {
phoneSmsProvider: 'hero-sms', phoneSmsProvider: 'hero-sms',
phoneSmsProviderOrder: ['hero-sms', '5sim'],
heroSmsApiKey: 'hero-old', heroSmsApiKey: 'hero-old',
fiveSimApiKey: 'five-old', fiveSimApiKey: 'five-old',
nexSmsApiKey: 'nex-old',
nexSmsCountryOrder: [1],
nexSmsServiceCode: 'ot',
madaoBaseUrl: 'http://127.0.0.1:7822',
madaoHttpSecret: 'madao-old-secret',
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'plan-old',
madaoProviderId: 'provider-old',
madaoCountry: 'TH',
madaoAutoPickCountry: true,
madaoReusePhone: true,
madaoMinPrice: '0.01',
madaoMaxPrice: '0.09',
heroSmsMinPrice: '0.04', heroSmsMinPrice: '0.04',
heroSmsMaxPrice: '0.11', heroSmsMaxPrice: '0.11',
fiveSimMinPrice: '0.88', fiveSimMinPrice: '0.88',
@@ -1207,10 +1405,21 @@ let latestState = {
fiveSimOperator: 'any', fiveSimOperator: 'any',
}; };
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms'; 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_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_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms', 'madao'];
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam'; const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)'; const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any'; 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 DEFAULT_HERO_SMS_OPERATOR = 'any'; const DEFAULT_HERO_SMS_OPERATOR = 'any';
const DEFAULT_HERO_SMS_COUNTRY_ID = 52; const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand'; const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
@@ -1218,25 +1427,64 @@ const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vie
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']); const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const selectPhoneSmsProvider = { value: 'hero-sms', dataset: { activeProvider: 'hero-sms' } }; const selectPhoneSmsProvider = { value: 'hero-sms', dataset: { activeProvider: 'hero-sms' } };
const inputHeroSmsApiKey = { value: 'hero-live' }; const inputHeroSmsApiKey = { value: 'hero-live' };
const inputFiveSimApiKey = { value: 'five-old' };
const inputNexSmsApiKey = { value: 'nex-live' };
const inputNexSmsServiceCode = { value: 'ot' };
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/poll' };
const inputMaDaoHttpSecret = { value: 'madao-live-secret' };
const selectMaDaoMode = { value: 'direct' };
const inputMaDaoRoutingPlanId = { value: 'plan-live' };
const inputMaDaoProviderId = { value: 'Provider Live!' };
const inputMaDaoCountry = { value: 'local' };
const inputMaDaoAutoPickCountry = { checked: false };
const inputMaDaoReusePhone = { checked: false };
const inputMaDaoMinPrice = { value: '0.02' };
const inputMaDaoMaxPrice = { value: '0.2' };
const inputHeroSmsMinPrice = { value: '0.03' }; const inputHeroSmsMinPrice = { value: '0.03' };
const inputHeroSmsMaxPrice = { value: '0.22' }; const inputHeroSmsMaxPrice = { value: '0.22' };
const inputFiveSimOperator = { value: 'any' }; const inputFiveSimOperator = { value: 'any' };
const inputFiveSimProduct = { value: 'openai' };
const selectHeroSmsOperator = { value: 'ais', options: [{ value: 'any' }, { value: 'ais' }] }; const selectHeroSmsOperator = { value: 'ais', options: [{ value: 'any' }, { value: 'ais' }] };
const inputHeroSmsPreferredPrice = { value: '' };
const displayHeroSmsPriceTiers = { textContent: '' }; const displayHeroSmsPriceTiers = { textContent: '' };
const displayPhoneSmsBalance = { textContent: '' }; const displayPhoneSmsBalance = { textContent: '' };
const rowHeroSmsPriceTiers = { style: { display: '' } }; const rowHeroSmsPriceTiers = { style: { display: '' } };
let heroSmsCountrySelectionOrder = []; let heroSmsCountrySelectionOrder = [];
let phoneSmsProviderOrderSelection = ['hero-sms', '5sim'];
let lastPhoneSmsProviderBeforeChange = null;
let savedPayload = null; let savedPayload = null;
${extractFunction('normalizePhoneSmsProvider')} ${extractFunction('normalizePhoneSmsProvider')}
${extractFunction('normalizePhoneSmsProviderValue')}
function getPhoneSmsProviderCount() { return DEFAULT_PHONE_SMS_PROVIDER_ORDER.length; }
${extractFunction('normalizePhoneSmsProviderOrderValue')}
${extractFunction('setPhoneSmsProviderSelectValue')} ${extractFunction('setPhoneSmsProviderSelectValue')}
${extractFunction('getLastAppliedPhoneSmsProvider')} ${extractFunction('getLastAppliedPhoneSmsProvider')}
function getSelectedPhoneSmsProvider() { return normalizePhoneSmsProvider(selectPhoneSmsProvider?.value || latestState?.phoneSmsProvider); } function getSelectedPhoneSmsProvider() { return normalizePhoneSmsProvider(selectPhoneSmsProvider?.value || latestState?.phoneSmsProvider); }
function applyPhoneSmsProviderOrderSelection(order = [], options = {}) {
phoneSmsProviderOrderSelection = normalizePhoneSmsProviderOrderValue(order, []);
if (options.syncProvider && phoneSmsProviderOrderSelection.length) {
selectPhoneSmsProvider.value = phoneSmsProviderOrderSelection[0];
}
return [...phoneSmsProviderOrderSelection];
}
${extractFunction('setPhoneSmsProviderOrderPrimary')}
${extractFunction('normalizeFiveSimCountryId')} ${extractFunction('normalizeFiveSimCountryId')}
${extractFunction('normalizeFiveSimCountryLabel')} ${extractFunction('normalizeFiveSimCountryLabel')}
${extractFunction('normalizeFiveSimCountryCode')}
${extractFunction('normalizeFiveSimOperator')} ${extractFunction('normalizeFiveSimOperator')}
${extractFunction('normalizeFiveSimProductValue')}
${extractFunction('normalizeFiveSimMaxPriceValue')} ${extractFunction('normalizeFiveSimMaxPriceValue')}
${extractFunction('normalizeHeroSmsMaxPriceValue')} ${extractFunction('normalizeHeroSmsMaxPriceValue')}
${extractFunction('normalizeNexSmsCountryIdValue')}
${extractFunction('normalizeNexSmsCountryOrderValue')}
${extractFunction('normalizeNexSmsServiceCodeValue')}
${extractFunction('normalizeMaDaoBaseUrlValue')}
${extractFunction('normalizeMaDaoModeValue')}
${extractFunction('normalizeMaDaoIdentifierValue')}
${extractFunction('normalizeMaDaoProviderIdValue')}
${extractFunction('normalizeMaDaoCountry')}
${extractFunction('normalizeMaDaoPriceValue')}
${extractFunction('normalizePhoneSmsMinPriceValue')} ${extractFunction('normalizePhoneSmsMinPriceValue')}
${extractFunction('normalizePhoneSmsMaxPriceValue')} ${extractFunction('normalizePhoneSmsMaxPriceValue')}
${extractFunction('normalizeHeroSmsCountryId')} ${extractFunction('normalizeHeroSmsCountryId')}
@@ -1254,11 +1502,18 @@ function syncHeroSmsFallbackSelectionOrderFromSelect() {
? [{ id: 'vietnam', label: '越南 (Vietnam)' }] ? [{ id: 'vietnam', label: '越南 (Vietnam)' }]
: [{ id: 52, label: 'Thailand' }]; : [{ id: 52, label: 'Thailand' }];
} }
function getSelectedNexSmsCountries() { return [{ id: 1, label: 'Country #1' }]; }
function syncLatestState(patch) { latestState = { ...latestState, ...patch }; } function syncLatestState(patch) { latestState = { ...latestState, ...patch }; }
function loadHeroSmsCountries() { return Promise.resolve(); } function loadHeroSmsCountries() { return Promise.resolve(); }
function loadFiveSimCountries() { return Promise.resolve(); }
function loadNexSmsCountries() { return Promise.resolve(); }
function applyHeroSmsFallbackSelection() {} function applyHeroSmsFallbackSelection() {}
function applyFiveSimCountrySelection() {}
function applyNexSmsCountrySelection() {}
function setHeroSmsOperatorSelectValue(operator = latestState?.heroSmsOperator) { selectHeroSmsOperator.value = normalizeHeroSmsOperatorValue(operator); } function setHeroSmsOperatorSelectValue(operator = latestState?.heroSmsOperator) { selectHeroSmsOperator.value = normalizeHeroSmsOperatorValue(operator); }
function refreshHeroSmsOperatorOptions() { return Promise.resolve(); } function refreshHeroSmsOperatorOptions() { return Promise.resolve(); }
${extractFunction('buildPhoneSmsProviderStatePatch')}
${extractFunction('applyPhoneSmsProviderFieldsToInputs')}
function updatePhoneVerificationSettingsUI() {} function updatePhoneVerificationSettingsUI() {}
function markSettingsDirty() {} function markSettingsDirty() {}
function saveSettings() { savedPayload = { ...latestState }; return Promise.resolve(); } function saveSettings() { savedPayload = { ...latestState }; return Promise.resolve(); }
@@ -1268,6 +1523,7 @@ ${extractFunction('switchPhoneSmsProvider')}
return { return {
selectPhoneSmsProvider, selectPhoneSmsProvider,
inputHeroSmsApiKey, inputHeroSmsApiKey,
inputFiveSimApiKey,
inputHeroSmsMinPrice, inputHeroSmsMinPrice,
get latestState() { return latestState; }, get latestState() { return latestState; },
get savedPayload() { return savedPayload; }, get savedPayload() { return savedPayload; },
@@ -1284,11 +1540,12 @@ return {
assert.equal(api.latestState.fiveSimApiKey, 'five-old'); assert.equal(api.latestState.fiveSimApiKey, 'five-old');
assert.equal(api.latestState.heroSmsMinPrice, '0.03'); assert.equal(api.latestState.heroSmsMinPrice, '0.03');
assert.equal(api.latestState.fiveSimMinPrice, '0.88'); assert.equal(api.latestState.fiveSimMinPrice, '0.88');
assert.equal(api.inputHeroSmsApiKey.value, 'five-old'); assert.equal(api.inputHeroSmsApiKey.value, 'hero-live');
assert.equal(api.inputFiveSimApiKey.value, 'five-old');
assert.equal(api.inputHeroSmsMinPrice.value, '0.88'); assert.equal(api.inputHeroSmsMinPrice.value, '0.88');
assert.equal(api.selectPhoneSmsProvider.dataset.activeProvider, '5sim'); assert.equal(api.selectPhoneSmsProvider.dataset.activeProvider, '5sim');
api.inputHeroSmsApiKey.value = 'five-live'; api.inputFiveSimApiKey.value = 'five-live';
api.selectPhoneSmsProvider.value = 'hero-sms'; api.selectPhoneSmsProvider.value = 'hero-sms';
await api.switchPhoneSmsProvider(api.selectPhoneSmsProvider.value); await api.switchPhoneSmsProvider(api.selectPhoneSmsProvider.value);
@@ -1355,6 +1612,7 @@ test('previewHeroSmsPriceTiers prefers 5sim products price for buy-compatible an
let latestState = { phoneSmsProvider: '5sim', fiveSimOperator: 'any' }; let latestState = { phoneSmsProvider: '5sim', fiveSimOperator: 'any' };
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms'; const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim'; const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_MADAO = 'madao';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam'; const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)'; const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any'; const DEFAULT_FIVE_SIM_OPERATOR = 'any';