feat: add MaDao phone SMS provider
This commit is contained in:
@@ -21,6 +21,7 @@ importScripts(
|
||||
'gopay-utils.js',
|
||||
'phone-sms/providers/hero-sms.js',
|
||||
'phone-sms/providers/five-sim.js',
|
||||
'phone-sms/providers/madao.js',
|
||||
'phone-sms/providers/registry.js',
|
||||
'background/phone-verification-flow.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_FIVE_SIM = PHONE_SMS_PROVIDER_5SIM;
|
||||
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_ORDER = Object.freeze([
|
||||
PHONE_SMS_PROVIDER_HERO,
|
||||
PHONE_SMS_PROVIDER_5SIM,
|
||||
PHONE_SMS_PROVIDER_NEXSMS,
|
||||
PHONE_SMS_PROVIDER_MADAO,
|
||||
]);
|
||||
const DEFAULT_FIVE_SIM_BASE_URL = 'https://5sim.net/v1';
|
||||
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_SERVICE_CODE = 'ot';
|
||||
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 HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
@@ -1477,6 +1482,16 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
nexSmsApiKey: '',
|
||||
nexSmsCountryOrder: [...DEFAULT_NEX_SMS_COUNTRY_ORDER],
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -1864,6 +1879,9 @@ function normalizePhoneSmsProvider(value = '') {
|
||||
if (normalized === 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;
|
||||
}
|
||||
function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) {
|
||||
@@ -2212,6 +2230,56 @@ function normalizeNexSmsServiceCode(value = '', fallback = DEFAULT_NEX_SMS_SERVI
|
||||
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) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null;
|
||||
@@ -3606,6 +3674,24 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return normalizeNexSmsCountryOrder(value);
|
||||
case 'nexSmsServiceCode':
|
||||
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':
|
||||
return normalizePhonePreferredActivation(value);
|
||||
default:
|
||||
@@ -13878,6 +13964,7 @@ const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.crea
|
||||
sleepWithStop,
|
||||
throwIfStopped,
|
||||
createFiveSimProvider: self.PhoneSmsFiveSimProvider?.createProvider,
|
||||
createMaDaoProvider: self.PhoneSmsMaDaoProvider?.createProvider,
|
||||
});
|
||||
const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({
|
||||
addLog,
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
DEFAULT_NEX_SMS_SERVICE_CODE = 'ot',
|
||||
DEFAULT_HERO_SMS_REUSE_ENABLED = true,
|
||||
createFiveSimProvider = null,
|
||||
createMaDaoProvider = null,
|
||||
HERO_SMS_COUNTRY_ID = 52,
|
||||
HERO_SMS_COUNTRY_LABEL = 'Thailand',
|
||||
HERO_SMS_SERVICE_CODE = 'dr',
|
||||
@@ -84,11 +85,13 @@
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = PHONE_SMS_PROVIDER_HERO;
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = PHONE_SMS_PROVIDER_5SIM;
|
||||
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_ORDER = Object.freeze([
|
||||
PHONE_SMS_PROVIDER_HERO,
|
||||
PHONE_SMS_PROVIDER_5SIM,
|
||||
PHONE_SMS_PROVIDER_NEXSMS,
|
||||
PHONE_SMS_PROVIDER_MADAO,
|
||||
]);
|
||||
const MAX_PHONE_REUSABLE_POOL = 12;
|
||||
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
|
||||
@@ -192,6 +195,9 @@
|
||||
if (normalized === 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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) {
|
||||
@@ -680,7 +705,7 @@
|
||||
});
|
||||
|
||||
if (normalized.length) {
|
||||
return normalized.slice(0, 3);
|
||||
return normalized.slice(0, DEFAULT_PHONE_SMS_PROVIDER_ORDER.length);
|
||||
}
|
||||
|
||||
const fallback = Array.isArray(fallbackOrder) ? fallbackOrder : [];
|
||||
@@ -696,7 +721,7 @@
|
||||
fallbackNormalized.push(provider);
|
||||
});
|
||||
|
||||
return fallbackNormalized.slice(0, 3);
|
||||
return fallbackNormalized.slice(0, DEFAULT_PHONE_SMS_PROVIDER_ORDER.length);
|
||||
}
|
||||
function resolvePhoneProviderOrder(state = {}, preferredProvider = '') {
|
||||
const currentProvider = normalizePhoneSmsProvider(
|
||||
@@ -723,7 +748,7 @@
|
||||
return fallbackOrder;
|
||||
}
|
||||
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) {
|
||||
@@ -962,6 +987,9 @@
|
||||
if (provider === PHONE_SMS_PROVIDER_NEXSMS) {
|
||||
return 'NexSMS';
|
||||
}
|
||||
if (provider === PHONE_SMS_PROVIDER_MADAO) {
|
||||
return 'MaDao';
|
||||
}
|
||||
return 'HeroSMS';
|
||||
}
|
||||
|
||||
@@ -1200,6 +1228,25 @@
|
||||
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') {
|
||||
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
return normalized || fallback;
|
||||
@@ -1362,14 +1409,18 @@
|
||||
const rawProvider = String(record.provider || '').trim();
|
||||
const provider = normalizePhoneSmsProvider(rawProvider);
|
||||
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 serviceCode = String(
|
||||
record.serviceCode
|
||||
|| (
|
||||
provider === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? 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();
|
||||
const countryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
@@ -1377,7 +1428,11 @@
|
||||
: (
|
||||
provider === PHONE_SMS_PROVIDER_NEXSMS
|
||||
? normalizeNexSmsCountryId(rawCountryId, 0)
|
||||
: normalizeCountryId(rawCountryId, fallbackCountryId)
|
||||
: (
|
||||
provider === PHONE_SMS_PROVIDER_MADAO
|
||||
? String(rawCountryId || '').trim()
|
||||
: normalizeCountryId(rawCountryId, fallbackCountryId)
|
||||
)
|
||||
);
|
||||
const ignoredPhoneCodeKeys = normalizeStringList(record.ignoredPhoneCodeKeys);
|
||||
return {
|
||||
@@ -1396,6 +1451,15 @@
|
||||
...(record.phoneCodeReceived ? { phoneCodeReceived: true } : {}),
|
||||
...(record.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(record.phoneCodeReceivedAt) || 0) } : {}),
|
||||
...(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 countryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? 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 statusAction = String(record.statusAction || '').trim();
|
||||
|
||||
@@ -1584,6 +1648,10 @@
|
||||
if (countryId) {
|
||||
fallback.countryId = countryId;
|
||||
}
|
||||
} else if (provider === PHONE_SMS_PROVIDER_MADAO) {
|
||||
if (countryId) {
|
||||
fallback.countryId = countryId;
|
||||
}
|
||||
} else if (Number.isFinite(countryId) && countryId > 0) {
|
||||
fallback.countryId = countryId;
|
||||
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);
|
||||
if (!apiKey) {
|
||||
throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。');
|
||||
@@ -2150,6 +2225,16 @@
|
||||
'phoneSmsProvider',
|
||||
'phoneSmsProviderOrder',
|
||||
'phoneSmsReuseEnabled',
|
||||
'madaoBaseUrl',
|
||||
'madaoHttpSecret',
|
||||
'madaoMode',
|
||||
'madaoRoutingPlanId',
|
||||
'madaoProviderId',
|
||||
'madaoCountry',
|
||||
'madaoAutoPickCountry',
|
||||
'madaoReusePhone',
|
||||
'madaoMinPrice',
|
||||
'madaoMaxPrice',
|
||||
'heroSmsApiKey',
|
||||
'heroSmsBaseUrl',
|
||||
'heroSmsCountryId',
|
||||
@@ -3691,6 +3776,13 @@
|
||||
state = await mergeLatestPhoneSettingsState(state, {
|
||||
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) {
|
||||
const provider = getFiveSimProviderForState(state);
|
||||
if (provider) {
|
||||
@@ -4041,6 +4133,9 @@
|
||||
return provider.reuseActivation(scopeStateToActivationProvider(state, normalizedActivation), normalizedActivation);
|
||||
}
|
||||
}
|
||||
if (getActivationProviderId(normalizedActivation, state) === PHONE_SMS_PROVIDER_MADAO) {
|
||||
return normalizedActivation;
|
||||
}
|
||||
|
||||
const scopedState = scopeStateToActivationProvider(state, normalizedActivation);
|
||||
const config = resolvePhoneConfig(scopedState);
|
||||
@@ -4141,6 +4236,13 @@
|
||||
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)');
|
||||
}
|
||||
|
||||
@@ -4162,6 +4264,13 @@
|
||||
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)');
|
||||
} catch (_) {
|
||||
// Best-effort cleanup.
|
||||
@@ -4263,6 +4372,13 @@
|
||||
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)');
|
||||
} catch (_) {
|
||||
// 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) {
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (maxRounds > 0 && pollCount >= maxRounds) {
|
||||
@@ -4733,6 +4896,9 @@
|
||||
if (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_NEXSMS) {
|
||||
return resolveNexSmsCountryCandidates(state);
|
||||
}
|
||||
if (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_MADAO) {
|
||||
return [];
|
||||
}
|
||||
return resolveCountryCandidates(state);
|
||||
}
|
||||
|
||||
@@ -4750,6 +4916,14 @@
|
||||
label: normalizeFiveSimCountryLabel(activation.countryLabel, countryId),
|
||||
};
|
||||
}
|
||||
} else if (providerId === PHONE_SMS_PROVIDER_MADAO) {
|
||||
const countryId = String(activation.countryId || '').trim();
|
||||
if (countryId) {
|
||||
return {
|
||||
id: countryId,
|
||||
label: normalizeCountryLabel(activation.countryLabel, countryId),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const inferredCountry = inferHeroSmsCountryFromPhoneNumber(activation.phoneNumber);
|
||||
const rawCountryId = normalizeCountryId(activation.countryId, 0);
|
||||
@@ -4775,7 +4949,9 @@
|
||||
}
|
||||
return candidates[0] || (providerId === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? { id: 'england', label: 'England' }
|
||||
: resolveCountryConfig(fallbackState));
|
||||
: (providerId === PHONE_SMS_PROVIDER_MADAO
|
||||
? { id: '', label: '' }
|
||||
: resolveCountryConfig(fallbackState)));
|
||||
}
|
||||
|
||||
async function submitPhoneNumber(tabId, phoneNumber, activation = null) {
|
||||
@@ -6509,6 +6685,10 @@
|
||||
const normalizedCountryId = normalizeNexSmsCountryId(countryId, -1);
|
||||
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);
|
||||
return normalizedCountryId > 0 ? `${normalizedProvider}:${normalizedCountryId}` : '';
|
||||
};
|
||||
@@ -6549,6 +6729,9 @@
|
||||
.find((entry) => normalizeNexSmsCountryId(entry.id, -1) === normalizedCountryId);
|
||||
return matched?.label || `Country #${normalizedCountryId}`;
|
||||
}
|
||||
if (normalizedProvider === PHONE_SMS_PROVIDER_MADAO) {
|
||||
return normalizedCountryKey || 'MaDao';
|
||||
}
|
||||
const normalizedCountryId = normalizeCountryId(normalizedCountryKey, 0);
|
||||
const matched = resolveCountryCandidates(state)
|
||||
.find((entry) => normalizeCountryId(entry.id, 0) === normalizedCountryId);
|
||||
@@ -6651,7 +6834,9 @@
|
||||
const getCountryFailureKey = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => (
|
||||
normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? 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)) => {
|
||||
@@ -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 = {}) => {
|
||||
await markPreferredActivationExhausted(failureCode || failureReason);
|
||||
usedNumberReplacementAttempts += 1;
|
||||
@@ -6788,6 +7004,37 @@
|
||||
`步骤 9:添加手机号失败后正在更换号码(${formatStep9Reason(failureReason)},${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`,
|
||||
'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) {
|
||||
await cancelPhoneActivation(state, activation);
|
||||
}
|
||||
@@ -6878,12 +7125,17 @@
|
||||
`自动白嫖复用号码 ${activation.phoneNumber} 反复返回添加手机号页。`
|
||||
);
|
||||
}
|
||||
if (shouldCancelActivation && activation) {
|
||||
await cancelPhoneActivation(state, activation);
|
||||
const rotated = shouldCancelActivation && 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;
|
||||
addPhoneReentryWithSameActivation = 0;
|
||||
pageState = {
|
||||
@@ -6938,12 +7190,17 @@
|
||||
`自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。`
|
||||
);
|
||||
}
|
||||
if (shouldCancelActivation && activation) {
|
||||
await banPhoneActivation(state, activation);
|
||||
const rotated = shouldCancelActivation && 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;
|
||||
addPhoneReentryWithSameActivation = 0;
|
||||
pageState = {
|
||||
@@ -7079,7 +7336,11 @@
|
||||
`自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。`
|
||||
);
|
||||
}
|
||||
if (shouldCancelActivation && activation) {
|
||||
if (
|
||||
shouldCancelActivation
|
||||
&& activation
|
||||
&& getActivationProviderId(activation, state) !== PHONE_SMS_PROVIDER_MADAO
|
||||
) {
|
||||
await banPhoneActivation(state, activation);
|
||||
shouldCancelActivation = false;
|
||||
}
|
||||
@@ -7093,7 +7354,11 @@
|
||||
if (attempt >= DEFAULT_PHONE_SUBMIT_ATTEMPTS) {
|
||||
shouldReplaceNumber = true;
|
||||
replaceReason = 'code_rejected';
|
||||
if (shouldCancelActivation && activation) {
|
||||
if (
|
||||
shouldCancelActivation
|
||||
&& activation
|
||||
&& getActivationProviderId(activation, state) !== PHONE_SMS_PROVIDER_MADAO
|
||||
) {
|
||||
await banPhoneActivation(state, activation);
|
||||
shouldCancelActivation = false;
|
||||
}
|
||||
@@ -7201,24 +7466,32 @@
|
||||
throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, replaceReason || 'unknown');
|
||||
}
|
||||
|
||||
if (shouldCancelActivation && activation) {
|
||||
await cancelPhoneActivation(state, activation);
|
||||
}
|
||||
const rotated = shouldCancelActivation && activation
|
||||
? await rotateCurrentActivation(
|
||||
replaceReason || 'replace_number',
|
||||
getPhoneReplacementReleaseAction(replaceReason || 'replace_number')
|
||||
)
|
||||
: { handled: false, nextActivation: null };
|
||||
if (shouldRetireFreeReusableActivationOnFailure(await getState(), activation)) {
|
||||
await retireFreeReusableActivation(
|
||||
`自动白嫖复用号码 ${activation.phoneNumber} 在失败后被更换。`
|
||||
);
|
||||
}
|
||||
if (isPhoneNumberUsedError(replaceReason)) {
|
||||
if (isPhoneNumberUsedFailureReason(replaceReason)) {
|
||||
await discardPhoneActivationFromReuse(
|
||||
`目标站拒绝该号码(${replaceReason})。`,
|
||||
activation,
|
||||
await getState()
|
||||
);
|
||||
}
|
||||
await clearCurrentActivation();
|
||||
activation = null;
|
||||
shouldCancelActivation = false;
|
||||
if (!rotated.nextActivation) {
|
||||
if (!rotated.handled && shouldCancelActivation && activation) {
|
||||
await cancelPhoneActivation(state, activation);
|
||||
}
|
||||
await clearCurrentActivation();
|
||||
activation = null;
|
||||
shouldCancelActivation = false;
|
||||
}
|
||||
addPhoneReentryWithSameActivation = 0;
|
||||
|
||||
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:轮询短信。
|
||||
- release:cancel / 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/Grok;routing 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 中删除大量无关文件的改动纳入本次范围。
|
||||
- 确认终端和文档无乱码。
|
||||
|
||||
### 阶段 2:MaDao 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 特例和图标按钮代码完全清理。
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -5,11 +5,13 @@
|
||||
const PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PROVIDER_FIVE_SIM = '5sim';
|
||||
const PROVIDER_NEXSMS = 'nexsms';
|
||||
const PROVIDER_MADAO = 'madao';
|
||||
const DEFAULT_PROVIDER = PROVIDER_HERO_SMS;
|
||||
const DEFAULT_PROVIDER_ORDER = Object.freeze([
|
||||
PROVIDER_HERO_SMS,
|
||||
PROVIDER_FIVE_SIM,
|
||||
PROVIDER_NEXSMS,
|
||||
PROVIDER_MADAO,
|
||||
]);
|
||||
const PROVIDER_DEFINITIONS = Object.freeze({
|
||||
[PROVIDER_HERO_SMS]: Object.freeze({
|
||||
@@ -27,6 +29,11 @@
|
||||
label: 'NexSMS',
|
||||
moduleKey: 'PhoneSmsNexSmsProvider',
|
||||
}),
|
||||
[PROVIDER_MADAO]: Object.freeze({
|
||||
id: PROVIDER_MADAO,
|
||||
label: 'MaDao',
|
||||
moduleKey: 'PhoneSmsMaDaoProvider',
|
||||
}),
|
||||
});
|
||||
|
||||
function resolveProviderKey(value = '') {
|
||||
@@ -125,6 +132,7 @@
|
||||
PROVIDER_HERO_SMS,
|
||||
PROVIDER_FIVE_SIM,
|
||||
PROVIDER_NEXSMS,
|
||||
PROVIDER_MADAO,
|
||||
DEFAULT_PROVIDER,
|
||||
DEFAULT_PROVIDER_ORDER,
|
||||
PROVIDER_DEFINITIONS,
|
||||
|
||||
+25
-47
@@ -128,26 +128,10 @@ header {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-left svg { color: var(--blue); }
|
||||
|
||||
.header-icon-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
.header-repo-btn {
|
||||
flex-shrink: 0;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.header-icon-link:hover svg {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.header-icon-link:focus-visible,
|
||||
.header-version-link:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--blue) 28%, transparent);
|
||||
outline-offset: 2px;
|
||||
@@ -321,29 +305,6 @@ header {
|
||||
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 {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -422,6 +383,8 @@ header {
|
||||
line-height: 1;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-field);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
white-space: nowrap;
|
||||
@@ -486,8 +449,17 @@ header {
|
||||
}
|
||||
.btn-outline:hover { border-color: var(--blue); color: var(--blue); background: var(--blue-soft); }
|
||||
|
||||
.btn-sm { padding-inline: 12px; font-size: 12px; }
|
||||
.btn-xs { padding-inline: 10px; font-size: 11px; }
|
||||
.btn-sm {
|
||||
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
|
||||
@@ -1556,6 +1528,7 @@ header {
|
||||
min-width: var(--data-inline-action-min-width);
|
||||
min-height: var(--data-field-height);
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hotmail-card {
|
||||
@@ -2922,22 +2895,27 @@ header {
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
width: 48px;
|
||||
width: 76px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-manual-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
min-width: 48px;
|
||||
min-height: 26px;
|
||||
padding: 0 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.step-manual-btn:hover:not(:disabled) {
|
||||
|
||||
+92
-41
@@ -16,12 +16,8 @@
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-left">
|
||||
<button id="btn-repo-home" class="header-icon-link" type="button" aria-label="打开 GitHub 仓库" title="打开 GitHub 仓库">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id="btn-repo-home" class="btn btn-outline btn-sm header-repo-btn" type="button"
|
||||
aria-label="打开 GitHub 仓库" title="打开 GitHub 仓库">仓库</button>
|
||||
<div class="header-version-block">
|
||||
<div class="header-version-main">
|
||||
<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"
|
||||
aria-pressed="false" title="进入贡献模式并打开官网页">贡献/使用教程</button>
|
||||
<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="自动执行全部步骤">
|
||||
<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-auto-run" class="btn btn-success" title="自动执行全部步骤">自动</button>
|
||||
<button id="btn-stop" class="btn btn-danger" title="停止当前流程" disabled>停止</button>
|
||||
</div>
|
||||
<button id="btn-reset" class="btn btn-ghost" title="重置全部步骤">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
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>
|
||||
<button id="btn-reset" class="btn btn-ghost btn-sm" title="重置全部步骤">重置</button>
|
||||
<button id="btn-theme" class="btn btn-ghost btn-sm header-theme-btn" title="切换主题">主题</button>
|
||||
<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"
|
||||
aria-expanded="false">配置</button>
|
||||
@@ -1470,20 +1438,22 @@
|
||||
<option value="hero-sms">HeroSMS</option>
|
||||
<option value="5sim">5sim</option>
|
||||
<option value="nexsms">NexSMS</option>
|
||||
<option value="madao">MaDao</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row" id="row-phone-sms-provider-order" style="display:none;">
|
||||
<span class="data-label">服务商顺序</span>
|
||||
<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="5sim" selected>5sim</option>
|
||||
<option value="nexsms" selected>NexSMS</option>
|
||||
<option value="madao" selected>MaDao</option>
|
||||
</select>
|
||||
<div class="hero-sms-country-mainline">
|
||||
<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">
|
||||
HeroSMS / 5sim / NexSMS (3/3)
|
||||
HeroSMS / 5sim / NexSMS / MaDao (4/4)
|
||||
</button>
|
||||
<div id="phone-sms-provider-order-menu" class="hero-sms-country-menu-dropdown" role="listbox" aria-multiselectable="true" hidden></div>
|
||||
</div>
|
||||
@@ -1636,6 +1606,86 @@
|
||||
<input type="text" id="input-nex-sms-service-code" class="data-input mono"
|
||||
placeholder="ot" />
|
||||
</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;">
|
||||
<span class="data-label">价格区间</span>
|
||||
<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="接码价格上限;可空(空=不设上限)" />
|
||||
</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>
|
||||
<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="优先尝试该价格档位;可空(空=按优先级策略)" />
|
||||
</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>
|
||||
<div class="setting-controls hero-sms-toggle-controls">
|
||||
<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="../phone-sms/providers/hero-sms.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="../icloud-utils.js"></script>
|
||||
<script src="../mail-provider-utils.js"></script>
|
||||
|
||||
+731
-184
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 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) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
@@ -65,6 +67,12 @@ test('background account history settings are normalized independently from hotm
|
||||
extractFunction('normalizeNexSmsCountryId'),
|
||||
extractFunction('normalizeNexSmsCountryOrder'),
|
||||
extractFunction('normalizeNexSmsServiceCode'),
|
||||
extractFunction('normalizeMaDaoBaseUrl'),
|
||||
extractFunction('normalizeMaDaoMode'),
|
||||
extractFunction('normalizeMaDaoIdentifier'),
|
||||
extractFunction('normalizeMaDaoProviderId'),
|
||||
extractFunction('normalizeMaDaoCountry'),
|
||||
extractFunction('normalizeMaDaoPrice'),
|
||||
extractFunction('normalizePhonePreferredActivation'),
|
||||
extractFunction('normalizePhoneVerificationReplacementLimit'),
|
||||
extractFunction('normalizePhoneCodeWaitSeconds'),
|
||||
@@ -119,8 +127,11 @@ const DEFAULT_HERO_SMS_OPERATOR = 'any';
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
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_MADAO_BASE_URL = 'http://127.0.0.1:7822';
|
||||
const DEFAULT_MADAO_MODE = 'routing_plan';
|
||||
const SIGNUP_METHOD_EMAIL = 'email';
|
||||
const SIGNUP_METHOD_PHONE = 'phone';
|
||||
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('phoneSmsProvider', '5SIM'), '5sim');
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'NEXSMS'), 'nexsms');
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'MaDao'), 'madao');
|
||||
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', true), true);
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimApiKey', ' demo-five '), ' demo-five ');
|
||||
@@ -373,6 +388,22 @@ return {
|
||||
[1, 6]
|
||||
);
|
||||
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({
|
||||
heroSmsMinPrice: '0.023456',
|
||||
fiveSimMinPrice: '0.0789',
|
||||
|
||||
@@ -412,6 +412,17 @@ return {
|
||||
cloudMailToken: 'cloud-token',
|
||||
gopayHelperApiKey: 'gpc-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: [
|
||||
{ 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().gopayHelperApiKey, 'gpc-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, [
|
||||
{ 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.getPersistedUpdates().settingsState.flows.kiro.targets['kiro-rs'].apiKey, 'schema-key');
|
||||
assert.equal(api.getPersistedUpdates().cloudflareTempEmailAdminAuth, 'admin-secret');
|
||||
assert.equal(api.getPersistedUpdates().madaoRoutingPlanId, 'rp-openai');
|
||||
assert.deepEqual(api.getPersistOptions(), { replaceExisting: true });
|
||||
});
|
||||
|
||||
@@ -6,6 +6,8 @@ const { readFlowRegistryBundle } = require('./helpers/script-bundles.js');
|
||||
const flowRegistrySource = readFlowRegistryBundle();
|
||||
const settingsSchemaSource = fs.readFileSync('core/flow-kernel/settings-schema.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) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
@@ -91,6 +93,8 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
|
||||
'stepExecutionRangeByFlow',
|
||||
]);
|
||||
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 = {
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
targetId: 'cpa',
|
||||
@@ -107,6 +111,17 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
ipProxyMode: 'account',
|
||||
kiroRsUrl: '',
|
||||
kiroRsKey: '',
|
||||
phoneSmsProvider: 'hero-sms',
|
||||
madaoBaseUrl: DEFAULT_MADAO_BASE_URL,
|
||||
madaoHttpSecret: '',
|
||||
madaoMode: DEFAULT_MADAO_MODE,
|
||||
madaoRoutingPlanId: '',
|
||||
madaoProviderId: '',
|
||||
madaoCountry: '',
|
||||
madaoAutoPickCountry: true,
|
||||
madaoReusePhone: true,
|
||||
madaoMinPrice: '',
|
||||
madaoMaxPrice: '',
|
||||
stepExecutionRangeByFlow: {},
|
||||
};
|
||||
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 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 normalizeIpProxyMode(value) { return String(value || 'account').trim() || 'account'; }
|
||||
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.kiroRsUrl, 'https://kiro.example.com/admin');
|
||||
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(payload.settingsSchemaVersion, 5);
|
||||
assert.equal(payload.settingsState.activeFlowId, 'kiro');
|
||||
@@ -335,6 +417,8 @@ function getRequestedKeys() {
|
||||
assert.ok(api.getRequestedKeys().includes('plusAccountAccessStrategy'));
|
||||
assert.equal(state.settingsSchemaVersion, 5);
|
||||
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 () => {
|
||||
@@ -629,6 +713,37 @@ function getRemovedKeys() {
|
||||
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 () => {
|
||||
const api = buildHarness(`
|
||||
const persistedWrites = [];
|
||||
|
||||
@@ -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,29 +16,41 @@ test('phone sms provider registry normalizes ids, order and labels consistently'
|
||||
PhoneSmsFiveSimProvider: {
|
||||
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(' MaDao '), 'madao');
|
||||
assert.equal(registry.normalizeProviderId('unknown-provider'), 'hero-sms');
|
||||
assert.equal(registry.getProviderLabel('nexsms'), 'NexSMS');
|
||||
assert.equal(registry.getProviderLabel('madao'), 'MaDao');
|
||||
assert.equal(registry.getProviderDefinition('nexsms').moduleKey, 'PhoneSmsNexSmsProvider');
|
||||
assert.equal(registry.getProviderDefinition('madao').moduleKey, 'PhoneSmsMaDaoProvider');
|
||||
assert.deepStrictEqual(
|
||||
registry.normalizeProviderOrder([
|
||||
{ provider: 'madao' },
|
||||
{ provider: 'nexsms' },
|
||||
{ id: '5sim' },
|
||||
{ value: 'hero-sms' },
|
||||
'MADAO',
|
||||
'NEXSMS',
|
||||
]),
|
||||
['nexsms', '5sim', 'hero-sms']
|
||||
['madao', 'nexsms', '5sim', 'hero-sms']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
registry.normalizeProviderOrder([], ['nexsms', '5sim', 'nexsms']),
|
||||
['nexsms', '5sim']
|
||||
registry.normalizeProviderOrder([], ['madao', 'nexsms', '5sim', 'nexsms']),
|
||||
['madao', 'nexsms', '5sim']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
registry.createProvider('5sim', { 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/);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ const fs = require('node:fs');
|
||||
const source = fs.readFileSync('background/phone-verification-flow.js', 'utf8');
|
||||
const 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 } = {}) {
|
||||
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');
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const setStateCalls = [];
|
||||
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 () => {
|
||||
const requests = [];
|
||||
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, /\.\.\/phone-sms\/providers\/hero-sms\.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, /<option value="hero-sms">HeroSMS<\/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-fallback"/);
|
||||
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-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"/);
|
||||
});
|
||||
|
||||
@@ -623,19 +645,21 @@ const rowPhoneSmsProvider = { style: { display: 'none' } };
|
||||
const rowPhoneSmsProviderOrder = { style: { display: 'none' } };
|
||||
const rowPhoneSmsProviderOrderActions = { style: { display: 'none' } };
|
||||
const selectPhoneSmsProvider = { value: 'hero-sms' };
|
||||
const selectMaDaoMode = { value: 'routing_plan' };
|
||||
const btnTogglePhoneVerificationSection = {
|
||||
disabled: false,
|
||||
textContent: '',
|
||||
title: '',
|
||||
setAttribute: () => {},
|
||||
};
|
||||
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms'];
|
||||
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms', 'madao'];
|
||||
const phoneSmsProviderOrderSelection = [];
|
||||
function getPhoneSmsProviderCount() { return DEFAULT_PHONE_SMS_PROVIDER_ORDER.length; }
|
||||
function normalizePhoneSmsProviderOrderValue(value = [], fallbackOrder = DEFAULT_PHONE_SMS_PROVIDER_ORDER) {
|
||||
const source = Array.isArray(value) ? value : [];
|
||||
const normalized = [...source];
|
||||
if (normalized.length) {
|
||||
return normalized.slice(0, 3);
|
||||
return normalized.slice(0, getPhoneSmsProviderCount());
|
||||
}
|
||||
if (!Array.isArray(fallbackOrder) || !fallbackOrder.length) {
|
||||
return [];
|
||||
@@ -646,7 +670,7 @@ const btnTogglePhoneVerificationSection = {
|
||||
fallbackNormalized.push(provider);
|
||||
}
|
||||
}
|
||||
return fallbackNormalized.slice(0, 3);
|
||||
return fallbackNormalized.slice(0, getPhoneSmsProviderCount());
|
||||
}
|
||||
function resolveNormalizedProviderOrderForRuntime(state = {}) {
|
||||
const rawOrder = Array.isArray(state?.phoneSmsProviderOrder) ? state.phoneSmsProviderOrder : [];
|
||||
@@ -674,6 +698,15 @@ const rowNexSmsApiKey = { style: { display: 'none' } };
|
||||
const rowNexSmsCountry = { style: { display: 'none' } };
|
||||
const rowNexSmsCountryFallback = { 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 rowHeroSmsCurrentNumber = { style: { display: 'none' } };
|
||||
const rowHeroSmsCurrentCountdown = { style: { display: 'none' } };
|
||||
@@ -689,6 +722,8 @@ const rowPhoneCodePollMaxRounds = { style: { display: 'none' } };
|
||||
const rowFreePhoneReuseEnabled = createMockRow();
|
||||
const rowFreePhoneReuseAutoEnabled = createMockRow();
|
||||
const rowFreeReusablePhone = createMockRow();
|
||||
const rowPhoneSmsPreferredPriceControl = { style: { display: 'none' } };
|
||||
const rowPhoneSmsReuseControl = { style: { display: 'none' } };
|
||||
const heroSmsReuseRow = createMockRow();
|
||||
const inputHeroSmsReuseEnabled = { checked: true, disabled: false, closest: () => heroSmsReuseRow };
|
||||
const inputFreePhoneReuseEnabled = { checked: true, disabled: false };
|
||||
@@ -698,10 +733,75 @@ const inputFreeReusablePhone = { disabled: false };
|
||||
const btnSaveFreeReusablePhone = { disabled: false };
|
||||
const btnClearFreeReusablePhone = { disabled: false };
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_HERO = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
|
||||
const PHONE_SMS_PROVIDER_MADAO = 'madao';
|
||||
const 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 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 updateSignupMethodUI() {
|
||||
rowSignupMethod.style.display = inputPhoneVerificationEnabled.checked ? '' : 'none';
|
||||
@@ -756,6 +856,18 @@ return {
|
||||
rowNexSmsCountry,
|
||||
rowNexSmsCountryFallback,
|
||||
rowNexSmsServiceCode,
|
||||
rowMaDaoBaseUrl,
|
||||
rowMaDaoHttpSecret,
|
||||
rowMaDaoMode,
|
||||
rowMaDaoRoutingPlanId,
|
||||
rowMaDaoProviderId,
|
||||
rowMaDaoCountry,
|
||||
rowMaDaoAutoPickCountry,
|
||||
rowMaDaoReusePhone,
|
||||
rowMaDaoPriceRange,
|
||||
rowPhoneSmsPreferredPriceControl,
|
||||
rowPhoneSmsReuseControl,
|
||||
selectMaDaoMode,
|
||||
rowHeroSmsRuntimePair,
|
||||
rowHeroSmsCurrentNumber,
|
||||
rowHeroSmsCurrentCountdown,
|
||||
@@ -823,6 +935,15 @@ return {
|
||||
assert.equal(api.rowNexSmsCountry.style.display, 'none');
|
||||
assert.equal(api.rowNexSmsCountryFallback.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.setLatestState({ signupPhoneNumber: '66959916439' });
|
||||
@@ -853,6 +974,8 @@ return {
|
||||
assert.equal(api.rowHeroSmsOperator.style.display, '');
|
||||
assert.equal(api.rowHeroSmsApiKey.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.rowHeroSmsCurrentNumber.style.display, '');
|
||||
assert.equal(api.rowHeroSmsCurrentCountdown.style.display, '');
|
||||
@@ -913,6 +1036,8 @@ return {
|
||||
assert.equal(api.rowFiveSimCountryFallback.style.display, '');
|
||||
assert.equal(api.rowFiveSimOperator.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.updatePhoneVerificationSettingsUI();
|
||||
@@ -920,6 +1045,33 @@ return {
|
||||
assert.equal(api.rowNexSmsCountry.style.display, '');
|
||||
assert.equal(api.rowNexSmsCountryFallback.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', () => {
|
||||
@@ -1005,6 +1157,16 @@ const inputFiveSimOperator = { value: 'any' };
|
||||
const inputFiveSimProduct = { value: 'openai' };
|
||||
const inputNexSmsApiKey = { value: 'nex-key' };
|
||||
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 selectHeroSmsAcquirePriority = { value: 'price' };
|
||||
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_LABEL = 'Thailand';
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_HERO = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
|
||||
const PHONE_SMS_PROVIDER_MADAO = 'madao';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_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_PHONE = 'phone';
|
||||
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
|
||||
@@ -1093,6 +1261,12 @@ function normalizePlusAccountAccessStrategy(value = '') { return String(value ||
|
||||
function resolvePlusAccountAccessStrategyForTarget(value = '') { return normalizePlusAccountAccessStrategy(value); }
|
||||
${extractFunction('normalizePhoneSmsProvider')}
|
||||
${extractFunction('normalizePhoneSmsProviderValue')}
|
||||
${extractFunction('normalizeMaDaoBaseUrlValue')}
|
||||
${extractFunction('normalizeMaDaoModeValue')}
|
||||
${extractFunction('normalizeMaDaoIdentifierValue')}
|
||||
${extractFunction('normalizeMaDaoProviderIdValue')}
|
||||
${extractFunction('normalizeMaDaoCountry')}
|
||||
${extractFunction('normalizeMaDaoPriceValue')}
|
||||
${extractFunction('normalizeFiveSimCountryCode')}
|
||||
${extractFunction('normalizeFiveSimCountryOrderValue')}
|
||||
${extractFunction('normalizeFiveSimProductValue')}
|
||||
@@ -1157,6 +1331,16 @@ return { collectSettingsPayload };
|
||||
assert.equal(payload.nexSmsApiKey, 'nex-key');
|
||||
assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]);
|
||||
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.heroSmsReuseEnabled, false);
|
||||
assert.equal(payload.freePhoneReuseEnabled, false);
|
||||
@@ -1192,8 +1376,22 @@ test('switchPhoneSmsProvider saves API keys independently when the select value
|
||||
const api = new Function(`
|
||||
let latestState = {
|
||||
phoneSmsProvider: 'hero-sms',
|
||||
phoneSmsProviderOrder: ['hero-sms', '5sim'],
|
||||
heroSmsApiKey: 'hero-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',
|
||||
heroSmsMaxPrice: '0.11',
|
||||
fiveSimMinPrice: '0.88',
|
||||
@@ -1207,10 +1405,21 @@ let latestState = {
|
||||
fiveSimOperator: 'any',
|
||||
};
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_HERO = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
|
||||
const PHONE_SMS_PROVIDER_MADAO = 'madao';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms', 'madao'];
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
|
||||
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
|
||||
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
|
||||
const MADAO_MODE_ROUTING_PLAN = 'routing_plan';
|
||||
const MADAO_MODE_DIRECT = 'direct';
|
||||
const DEFAULT_MADAO_MODE = MADAO_MODE_ROUTING_PLAN;
|
||||
const DEFAULT_HERO_SMS_OPERATOR = 'any';
|
||||
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||
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 selectPhoneSmsProvider = { value: 'hero-sms', dataset: { activeProvider: 'hero-sms' } };
|
||||
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 inputHeroSmsMaxPrice = { value: '0.22' };
|
||||
const inputFiveSimOperator = { value: 'any' };
|
||||
const inputFiveSimProduct = { value: 'openai' };
|
||||
const selectHeroSmsOperator = { value: 'ais', options: [{ value: 'any' }, { value: 'ais' }] };
|
||||
const inputHeroSmsPreferredPrice = { value: '' };
|
||||
const displayHeroSmsPriceTiers = { textContent: '' };
|
||||
const displayPhoneSmsBalance = { textContent: '' };
|
||||
const rowHeroSmsPriceTiers = { style: { display: '' } };
|
||||
let heroSmsCountrySelectionOrder = [];
|
||||
let phoneSmsProviderOrderSelection = ['hero-sms', '5sim'];
|
||||
let lastPhoneSmsProviderBeforeChange = null;
|
||||
let savedPayload = null;
|
||||
|
||||
${extractFunction('normalizePhoneSmsProvider')}
|
||||
${extractFunction('normalizePhoneSmsProviderValue')}
|
||||
function getPhoneSmsProviderCount() { return DEFAULT_PHONE_SMS_PROVIDER_ORDER.length; }
|
||||
${extractFunction('normalizePhoneSmsProviderOrderValue')}
|
||||
${extractFunction('setPhoneSmsProviderSelectValue')}
|
||||
${extractFunction('getLastAppliedPhoneSmsProvider')}
|
||||
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('normalizeFiveSimCountryLabel')}
|
||||
${extractFunction('normalizeFiveSimCountryCode')}
|
||||
${extractFunction('normalizeFiveSimOperator')}
|
||||
${extractFunction('normalizeFiveSimProductValue')}
|
||||
${extractFunction('normalizeFiveSimMaxPriceValue')}
|
||||
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
||||
${extractFunction('normalizeNexSmsCountryIdValue')}
|
||||
${extractFunction('normalizeNexSmsCountryOrderValue')}
|
||||
${extractFunction('normalizeNexSmsServiceCodeValue')}
|
||||
${extractFunction('normalizeMaDaoBaseUrlValue')}
|
||||
${extractFunction('normalizeMaDaoModeValue')}
|
||||
${extractFunction('normalizeMaDaoIdentifierValue')}
|
||||
${extractFunction('normalizeMaDaoProviderIdValue')}
|
||||
${extractFunction('normalizeMaDaoCountry')}
|
||||
${extractFunction('normalizeMaDaoPriceValue')}
|
||||
${extractFunction('normalizePhoneSmsMinPriceValue')}
|
||||
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
||||
${extractFunction('normalizeHeroSmsCountryId')}
|
||||
@@ -1254,11 +1502,18 @@ function syncHeroSmsFallbackSelectionOrderFromSelect() {
|
||||
? [{ id: 'vietnam', label: '越南 (Vietnam)' }]
|
||||
: [{ id: 52, label: 'Thailand' }];
|
||||
}
|
||||
function getSelectedNexSmsCountries() { return [{ id: 1, label: 'Country #1' }]; }
|
||||
function syncLatestState(patch) { latestState = { ...latestState, ...patch }; }
|
||||
function loadHeroSmsCountries() { return Promise.resolve(); }
|
||||
function loadFiveSimCountries() { return Promise.resolve(); }
|
||||
function loadNexSmsCountries() { return Promise.resolve(); }
|
||||
function applyHeroSmsFallbackSelection() {}
|
||||
function applyFiveSimCountrySelection() {}
|
||||
function applyNexSmsCountrySelection() {}
|
||||
function setHeroSmsOperatorSelectValue(operator = latestState?.heroSmsOperator) { selectHeroSmsOperator.value = normalizeHeroSmsOperatorValue(operator); }
|
||||
function refreshHeroSmsOperatorOptions() { return Promise.resolve(); }
|
||||
${extractFunction('buildPhoneSmsProviderStatePatch')}
|
||||
${extractFunction('applyPhoneSmsProviderFieldsToInputs')}
|
||||
function updatePhoneVerificationSettingsUI() {}
|
||||
function markSettingsDirty() {}
|
||||
function saveSettings() { savedPayload = { ...latestState }; return Promise.resolve(); }
|
||||
@@ -1268,6 +1523,7 @@ ${extractFunction('switchPhoneSmsProvider')}
|
||||
return {
|
||||
selectPhoneSmsProvider,
|
||||
inputHeroSmsApiKey,
|
||||
inputFiveSimApiKey,
|
||||
inputHeroSmsMinPrice,
|
||||
get latestState() { return latestState; },
|
||||
get savedPayload() { return savedPayload; },
|
||||
@@ -1284,11 +1540,12 @@ return {
|
||||
assert.equal(api.latestState.fiveSimApiKey, 'five-old');
|
||||
assert.equal(api.latestState.heroSmsMinPrice, '0.03');
|
||||
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.selectPhoneSmsProvider.dataset.activeProvider, '5sim');
|
||||
|
||||
api.inputHeroSmsApiKey.value = 'five-live';
|
||||
api.inputFiveSimApiKey.value = 'five-live';
|
||||
api.selectPhoneSmsProvider.value = 'hero-sms';
|
||||
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' };
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
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_LABEL = '越南 (Vietnam)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
|
||||
Reference in New Issue
Block a user