diff --git a/background.js b/background.js index 02ef4c82..da21087e 100644 --- a/background.js +++ b/background.js @@ -21,6 +21,8 @@ importScripts( 'gopay-utils.js', 'phone-sms/providers/hero-sms.js', 'phone-sms/providers/five-sim.js', + 'phone-sms/providers/nexsms.js', + 'phone-sms/providers/madao.js', 'phone-sms/providers/registry.js', 'background/phone-verification-flow.js', 'background/account-run-history.js', @@ -691,11 +693,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'; @@ -1464,6 +1468,17 @@ const PERSISTED_SETTING_DEFAULTS = { nexSmsApiKey: '', nexSmsCountryOrder: [...DEFAULT_NEX_SMS_COUNTRY_ORDER], nexSmsServiceCode: DEFAULT_NEX_SMS_SERVICE_CODE, + madaoMode: 'routing_plan', + madaoBaseUrl: 'http://127.0.0.1:7822', + madaoHttpSecret: '', + madaoProviderId: '', + madaoRoutingPlanId: '', + madaoServiceName: 'openai', + madaoCountry: '', + madaoAutoPickCountry: true, + madaoReusePhone: true, + madaoMinPrice: '', + madaoMaxPrice: '', phonePreferredActivation: null, }; @@ -1827,6 +1842,9 @@ function normalizePhoneSmsProvider(value = '') { if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderId) { return rootScope.PhoneSmsProviderRegistry.normalizeProviderId(value); } + const madaoProvider = typeof PHONE_SMS_PROVIDER_MADAO !== 'undefined' + ? PHONE_SMS_PROVIDER_MADAO + : 'madao'; const normalized = String(value || '').trim().toLowerCase(); if (normalized === PHONE_SMS_PROVIDER_FIVE_SIM) { return PHONE_SMS_PROVIDER_FIVE_SIM; @@ -1834,6 +1852,9 @@ function normalizePhoneSmsProvider(value = '') { if (normalized === PHONE_SMS_PROVIDER_NEXSMS) { return PHONE_SMS_PROVIDER_NEXSMS; } + if (normalized === madaoProvider) { + return madaoProvider; + } return PHONE_SMS_PROVIDER_HERO_SMS; } function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) { @@ -3529,6 +3550,39 @@ function normalizePersistentSettingValue(key, value) { return normalizeNexSmsCountryOrder(value); case 'nexSmsServiceCode': return normalizeNexSmsServiceCode(value); + case 'madaoBaseUrl': + return String(value || '').trim() || 'http://127.0.0.1:7822'; + case 'madaoMode': + return String(value || '').trim().toLowerCase() === 'direct' ? 'direct' : 'routing_plan'; + case 'madaoHttpSecret': + return String(value || '').trim(); + case 'madaoProviderId': + return String(value || '').trim().toLowerCase(); + case 'madaoRoutingPlanId': + return String(value || '').trim(); + case 'madaoServiceName': + return String(value || '').trim().toLowerCase() || 'openai'; + case 'madaoCountry': + { + 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; + } + case 'madaoAutoPickCountry': + case 'madaoReusePhone': + return Boolean(value); + case 'madaoMinPrice': + case 'madaoMaxPrice': + return normalizeHeroSmsMaxPrice(value); case 'phonePreferredActivation': return normalizePhonePreferredActivation(value); default: diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index 08141efd..7c653b79 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -83,11 +83,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::'; @@ -191,6 +193,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 = {}) { @@ -935,9 +940,71 @@ if (provider === PHONE_SMS_PROVIDER_NEXSMS) { return 'NexSMS'; } + if (provider === PHONE_SMS_PROVIDER_MADAO) { + return 'MaDao'; + } return 'HeroSMS'; } + function getMaDaoProviderForState() { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + return rootScope.PhoneSmsMaDaoProvider || null; + } + + function getPhoneSmsProviderAdapterForState(state = {}) { + const providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER); + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.PhoneSmsProviderRegistry?.createProvider) { + return rootScope.PhoneSmsProviderRegistry.createProvider(providerId, { + addLog, + fetchImpl, + requestTimeoutMs: DEFAULT_PHONE_REQUEST_TIMEOUT_MS, + sleepWithStop, + throwIfStopped, + }); + } + return null; + } + + function resolveMaDaoPriceRange(state = {}) { + const minPrice = normalizeHeroSmsPriceLimit(state?.madaoMinPrice); + const maxPrice = normalizeHeroSmsPriceLimit(state?.madaoMaxPrice); + return { + minPrice, + maxPrice, + invalidRange: minPrice !== null && maxPrice !== null && minPrice > maxPrice, + }; + } + + async function requestMaDaoActivation(state = {}, options = {}) { + const provider = getMaDaoProviderForState(); + if (!provider?.acquireActivation) { + throw new Error('MaDao 接码模块未加载。'); + } + const madaoMode = String(state?.madaoMode || '').trim().toLowerCase() === 'direct' + ? 'direct' + : 'routing_plan'; + const priceRange = resolveMaDaoPriceRange(state); + if (priceRange.invalidRange) { + throw new Error( + `MaDao 价格区间无效:最低购买价 ${priceRange.minPrice} 高于价格上限 ${priceRange.maxPrice}。` + ); + } + return provider.acquireActivation(state, { + providerId: madaoMode === 'direct' ? state?.madaoProviderId : '', + routingPlanId: madaoMode === 'routing_plan' ? state?.madaoRoutingPlanId : '', + service: state?.madaoServiceName || DEFAULT_FIVE_SIM_PRODUCT, + country: madaoMode === 'direct' ? (state?.madaoCountry || '') : '', + autoPickCountry: madaoMode === 'direct' ? state?.madaoAutoPickCountry : true, + reusePhone: madaoMode === 'direct' ? state?.madaoReusePhone : true, + minPrice: madaoMode === 'direct' ? priceRange.minPrice : null, + maxPrice: madaoMode === 'direct' ? priceRange.maxPrice : null, + }, { + fetchImpl, + requestTimeoutMs: DEFAULT_PHONE_REQUEST_TIMEOUT_MS, + }); + } + function formatStep9Reason(reason = '') { const text = String(reason || '').trim(); if (!text) { @@ -1368,6 +1435,13 @@ ...(record.source ? { source: String(record.source || '').trim() } : {}), ...(record.phoneCodeReceived ? { phoneCodeReceived: true } : {}), ...(record.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(record.phoneCodeReceivedAt) || 0) } : {}), + ...(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) } : {}), ...(ignoredPhoneCodeKeys.length ? { ignoredPhoneCodeKeys } : {}), }; } @@ -2056,6 +2130,13 @@ function resolvePhoneConfig(state = {}) { const provider = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER); + if (provider === PHONE_SMS_PROVIDER_MADAO) { + return { + provider, + baseUrl: String(state?.madaoBaseUrl || 'http://127.0.0.1:7822').trim() || 'http://127.0.0.1:7822', + countryCandidates: [], + }; + } if (provider === PHONE_SMS_PROVIDER_5SIM) { const apiKey = normalizeApiKey(state.fiveSimApiKey || state.heroSmsApiKey); if (!apiKey) { @@ -3584,6 +3665,9 @@ } async function requestPhoneActivation(state = {}, options = {}) { + if (normalizePhoneSmsProvider(state?.phoneSmsProvider) === PHONE_SMS_PROVIDER_MADAO) { + return requestMaDaoActivation(state, options); + } if (normalizePhoneSmsProvider(state?.phoneSmsProvider) === PHONE_SMS_PROVIDER_FIVE_SIM) { const provider = getFiveSimProviderForState(state); if (provider) { @@ -4008,6 +4092,18 @@ } return describeNexSmsPayload(payload); } + if (config.provider === PHONE_SMS_PROVIDER_MADAO) { + const provider = getMaDaoProviderForState(); + if (!provider?.releaseActivation) { + throw new Error('MaDao 接码模块未加载。'); + } + const action = normalizedStatus === 6 ? 'finish' : 'cancel'; + const payload = await provider.releaseActivation(state, normalizedActivation, action, { + fetchImpl, + requestTimeoutMs: DEFAULT_PHONE_REQUEST_TIMEOUT_MS, + }); + return String(payload?.message || action).trim() || action; + } const payload = await fetchHeroSmsPayload(config, { action: 'setStatus', id: normalizedActivation.activationId, @@ -4161,6 +4257,26 @@ } } + async function rotateCurrentPhoneActivation(state = {}, activation, options = {}) { + const adapter = getPhoneSmsProviderAdapterForState(state); + if (adapter?.rotateActivation) { + return adapter.rotateActivation(state, activation, options, { + fetchImpl, + requestTimeoutMs: DEFAULT_PHONE_REQUEST_TIMEOUT_MS, + }); + } + if (options?.releaseAction === 'ban') { + await banPhoneActivation(state, activation); + } else { + await cancelPhoneActivation(state, activation); + } + return { + currentTicketId: normalizeActivation(activation)?.activationId || '', + currentTicketRelease: null, + nextActivation: null, + }; + } + async function requestAdditionalPhoneSms(state = {}, activation) { const config = resolvePhoneConfig(state); if (config.provider !== PHONE_SMS_PROVIDER_HERO) { @@ -4500,6 +4616,60 @@ throw buildPhoneCodeTimeoutError(lastResponse); } + if (config.provider === PHONE_SMS_PROVIDER_MADAO) { + const provider = getMaDaoProviderForState(); + if (!provider?.pollActivation) { + throw new Error('MaDao 接码模块未加载。'); + } + while (Date.now() - start < timeoutMs) { + if (maxRounds > 0 && pollCount >= maxRounds) { + break; + } + throwIfStopped(); + const payload = await provider.pollActivation(state, normalizedActivation, { + fetchImpl, + requestTimeoutMs: DEFAULT_PHONE_REQUEST_TIMEOUT_MS, + }); + const statusText = String(payload?.status || '').trim(); + const mappedStatus = provider.mapTicketStatus + ? provider.mapTicketStatus(statusText) + : String(statusText || '').trim().toLowerCase(); + lastResponse = String(payload?.message || statusText || '').trim(); + pollCount += 1; + + if (mappedStatus === 'code_received') { + const directCode = extractVerificationCode(payload?.code || payload?.message || ''); + if (directCode) { + return directCode; + } + throw new Error('MaDao 返回 code_received,但未提供验证码。'); + } + + if (mappedStatus === 'waiting_code' || mappedStatus === 'pending') { + if (typeof options.onStatus === 'function') { + await options.onStatus({ + activation: normalizedActivation, + elapsedMs: Date.now() - start, + pollCount, + statusText: statusText || 'WAITING_CODE', + timeoutMs, + }); + } + await emitWaitingForCode(statusText || 'WAITING_CODE'); + await sleepWithStop(intervalMs); + continue; + } + + if (mappedStatus === 'cancelled' || mappedStatus === 'failed' || mappedStatus === 'finished') { + throw new Error(`MaDao 订单在收到短信前已结束:${statusText || mappedStatus}`); + } + + throw new Error(`MaDao 返回未知状态:${statusText || mappedStatus || 'unknown'}`); + } + + throw buildPhoneCodeTimeoutError(lastResponse); + } + while (Date.now() - start < timeoutMs) { if (maxRounds > 0 && pollCount >= maxRounds) { break; @@ -6678,11 +6848,18 @@ 'warn' ); if (shouldCancelActivation && activation) { - await cancelPhoneActivation(state, activation); + const rotation = await rotateCurrentPhoneActivation(state, activation, { + releaseAction: 'cancel', + reason: String(failureCode || failureReason || '').trim(), + }); + activation = normalizeActivation(rotation?.nextActivation) || null; } await clearCurrentActivation(); - activation = null; - shouldCancelActivation = false; + if (activation) { + await persistCurrentActivation(activation); + } else { + shouldCancelActivation = false; + } preferReuseExistingActivationOnAddPhone = false; addPhoneReentryWithSameActivation = 0; let addPhoneSnapshot = { @@ -6768,11 +6945,18 @@ ); } if (shouldCancelActivation && activation) { - await cancelPhoneActivation(state, activation); + const rotation = await rotateCurrentPhoneActivation(state, activation, { + releaseAction: 'cancel', + reason: 'returned_to_add_phone_loop', + }); + activation = normalizeActivation(rotation?.nextActivation) || null; } await clearCurrentActivation(); - activation = null; - shouldCancelActivation = false; + if (activation) { + await persistCurrentActivation(activation); + } else { + shouldCancelActivation = false; + } preferReuseExistingActivationOnAddPhone = false; addPhoneReentryWithSameActivation = 0; pageState = { @@ -6828,11 +7012,18 @@ ); } if (shouldCancelActivation && activation) { - await banPhoneActivation(state, activation); + const rotation = await rotateCurrentPhoneActivation(state, activation, { + releaseAction: 'ban', + reason: 'phone_number_used', + }); + activation = normalizeActivation(rotation?.nextActivation) || null; } await clearCurrentActivation(); - activation = null; - shouldCancelActivation = false; + if (activation) { + await persistCurrentActivation(activation); + } else { + shouldCancelActivation = false; + } preferReuseExistingActivationOnAddPhone = false; addPhoneReentryWithSameActivation = 0; pageState = { @@ -6914,6 +7105,8 @@ let shouldReplaceNumber = false; let replaceReason = ''; + let failedActivationForReplacement = null; + let replacementPreparedInAdvance = false; for (let attempt = 1; attempt <= DEFAULT_PHONE_SUBMIT_ATTEMPTS; attempt += 1) { throwIfStopped(); @@ -6923,6 +7116,7 @@ await markPreferredActivationExhausted(codeResult.reason || 'sms_timeout'); shouldReplaceNumber = true; replaceReason = codeResult.reason || 'sms_not_received'; + failedActivationForReplacement = activation; break; } @@ -6958,6 +7152,7 @@ if (isPhoneNumberUsedError(invalidErrorText)) { shouldReplaceNumber = true; replaceReason = 'phone_number_used'; + failedActivationForReplacement = activation; await discardPhoneActivationFromReuse( `目标站拒绝该号码(${invalidErrorText})。`, activation, @@ -6969,8 +7164,13 @@ ); } if (shouldCancelActivation && activation) { - await banPhoneActivation(state, activation); - shouldCancelActivation = false; + const rotation = await rotateCurrentPhoneActivation(state, activation, { + releaseAction: 'ban', + reason: 'phone_number_used', + }); + activation = normalizeActivation(rotation?.nextActivation) || null; + shouldCancelActivation = Boolean(activation); + replacementPreparedInAdvance = Boolean(activation); } await addLog( `步骤 9:手机号被提示已使用(${invalidErrorText}),立即更换新号码。`, @@ -6982,9 +7182,15 @@ if (attempt >= DEFAULT_PHONE_SUBMIT_ATTEMPTS) { shouldReplaceNumber = true; replaceReason = 'code_rejected'; + failedActivationForReplacement = activation; if (shouldCancelActivation && activation) { - await banPhoneActivation(state, activation); - shouldCancelActivation = false; + const rotation = await rotateCurrentPhoneActivation(state, activation, { + releaseAction: 'ban', + reason: 'code_rejected', + }); + activation = normalizeActivation(rotation?.nextActivation) || null; + shouldCancelActivation = Boolean(activation); + replacementPreparedInAdvance = Boolean(activation); } await addLog( `步骤 9:手机验证码连续 ${DEFAULT_PHONE_SUBMIT_ATTEMPTS} 次被拒(${invalidErrorText}),将更换号码。`, @@ -7090,24 +7296,38 @@ throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, replaceReason || 'unknown'); } - if (shouldCancelActivation && activation) { - await cancelPhoneActivation(state, activation); + const failedActivation = failedActivationForReplacement || activation; + if (shouldCancelActivation && failedActivation && !replacementPreparedInAdvance) { + const releaseAction = ( + replaceReason === 'phone_number_used' + || replaceReason === 'code_rejected' + ) + ? 'ban' + : 'cancel'; + const rotation = await rotateCurrentPhoneActivation(state, failedActivation, { + releaseAction, + reason: `step9 replace number: ${replaceReason || 'unknown'}`, + }); + activation = normalizeActivation(rotation?.nextActivation) || null; } - if (shouldRetireFreeReusableActivationOnFailure(await getState(), activation)) { + if (shouldRetireFreeReusableActivationOnFailure(await getState(), failedActivation)) { await retireFreeReusableActivation( - `自动白嫖复用号码 ${activation.phoneNumber} 在失败后被更换。` + `自动白嫖复用号码 ${failedActivation.phoneNumber} 在失败后被更换。` ); } - if (isPhoneNumberUsedError(replaceReason)) { + if (isPhoneNumberUsedError(replaceReason) && failedActivation) { await discardPhoneActivationFromReuse( `目标站拒绝该号码(${replaceReason})。`, - activation, + failedActivation, await getState() ); } await clearCurrentActivation(); - activation = null; - shouldCancelActivation = false; + if (activation) { + await persistCurrentActivation(activation); + } else { + shouldCancelActivation = false; + } addPhoneReentryWithSameActivation = 0; let returnResult = null; @@ -7153,6 +7373,8 @@ addPhonePage: true, phoneVerificationPage: false, }; + failedActivationForReplacement = null; + replacementPreparedInAdvance = false; } } catch (error) { const errorMessage = String(error?.message || error || ''); diff --git a/phone-sms/providers/madao.js b/phone-sms/providers/madao.js new file mode 100644 index 00000000..93436fb1 --- /dev/null +++ b/phone-sms/providers/madao.js @@ -0,0 +1,373 @@ +// phone-sms/providers/madao.js — MaDao 统一接码后端适配层 +(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_REQUEST_TIMEOUT_MS = 20000; + + function normalizeBaseUrl(value = '', fallback = DEFAULT_BASE_URL) { + const trimmed = String(value || '').trim() || fallback; + try { + return new URL(trimmed).toString().replace(/\/+$/, ''); + } catch { + return fallback; + } + } + + function normalizeText(value = '', fallback = '') { + return String(value || '').trim() || fallback; + } + + function normalizeProviderId(value = '') { + return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); + } + + function normalizeCountry(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; + } + + function normalizeBoolean(value, fallback = false) { + if (value === undefined || value === null) { + return Boolean(fallback); + } + 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; + } + + 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 = String(options.method || 'GET').trim().toUpperCase() || 'GET'; + 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 detail = ( + payload && typeof payload === 'object' + ? String(payload.message || payload.error || response.statusText || response.status).trim() + : String(payload || response.statusText || response.status).trim() + ) || `HTTP ${response.status}`; + const error = new Error(`MaDao 请求失败:${detail}`); + 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 = String(value || '').trim().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 = String(value || '').trim().toLowerCase(); + if (normalized === 'waiting_code') { + return 'waiting_code'; + } + if (normalized === 'code_received') { + return 'code_received'; + } + if (normalized === 'finished') { + return 'finished'; + } + if (normalized === 'cancelled') { + return 'cancelled'; + } + if (normalized === 'failed') { + return 'failed'; + } + return 'pending'; + } + + function buildAcquireRequest(state = {}, options = {}) { + const routingPlanId = normalizeText(options?.routingPlanId || state?.madaoRoutingPlanId); + const directProvider = normalizeProviderId(options?.providerId || state?.madaoProviderId); + const request = { + provider: 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 (routingPlanId) { + request.routing_plan_id = routingPlanId; + } else { + 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 ticketId = normalizeText(payload?.ticket_id || payload?.id); + const phoneNumber = normalizeText(payload?.phone_number || payload?.phone); + if (!ticketId || !phoneNumber) { + return null; + } + return { + activationId: ticketId, + phoneNumber, + provider: PROVIDER_ID, + serviceCode: normalizeText(payload?.service || fallback.service, DEFAULT_SERVICE), + countryId: normalizeCountry(payload?.country || fallback.country), + countryLabel: '', + maxUses: 1, + successfulUses: 0, + madaoProviderId: normalizeProviderId(payload?.provider || fallback.provider), + madaoRoutingPlanId: normalizeText(payload?.routing_plan_id || fallback.routing_plan_id), + madaoRoutingPlanName: normalizeText(payload?.routing_plan_name || fallback.routing_plan_name), + madaoRoutingItemId: normalizeText(payload?.routing_item_id || fallback.routing_item_id), + madaoAcquirePath: mapAcquirePath(payload?.acquire_path), + madaoStatus: mapTicketStatus(payload?.status), + ...(payload?.price !== undefined && payload?.price !== null + ? { madaoPrice: normalizePrice(payload.price) } + : {}), + }; + } + + 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 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'), + }, + }); + } + + 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, { + 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, ticketId), + currentTicketRelease: payload?.current_ticket_release || null, + nextActivation: nextTicket, + }; + } + + async function rotateActivation(state = {}, activation, options = {}, deps = {}) { + const mode = normalizeText(state?.madaoMode, 'routing_plan').toLowerCase() === 'direct' + ? 'direct' + : 'routing_plan'; + 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, + }), + 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, + }), + mapAcquirePath, + mapTicketStatus, + normalizeActivationFromAcquire, + resolveConfig: (state = {}, runtimeDeps = {}) => resolveConfig(state, { + ...deps, + ...runtimeDeps, + }), + }; + } + + return { + PROVIDER_ID, + DEFAULT_BASE_URL, + DEFAULT_SERVICE, + acquireActivation, + createProvider, + mapAcquirePath, + mapTicketStatus, + normalizeActivationFromAcquire, + pollActivation, + rotateActivation, + replaceRoutingActivation, + releaseActivation, + resolveConfig, + }; +}); diff --git a/phone-sms/providers/nexsms.js b/phone-sms/providers/nexsms.js new file mode 100644 index 00000000..ccc6925e --- /dev/null +++ b/phone-sms/providers/nexsms.js @@ -0,0 +1,271 @@ +// phone-sms/providers/nexsms.js — NexSMS 接码平台适配层 +(function attachNexSmsProvider(root, factory) { + root.PhoneSmsNexSmsProvider = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createNexSmsProviderModule() { + const PROVIDER_ID = 'nexsms'; + const DEFAULT_BASE_URL = 'https://api.nexsms.net'; + const DEFAULT_SERVICE_CODE = 'ot'; + const DEFAULT_SERVICE_LABEL = 'OpenAI'; + const DEFAULT_COUNTRY_ID = 1; + const DEFAULT_COUNTRY_LABEL = 'Country #1'; + const DEFAULT_REQUEST_TIMEOUT_MS = 20000; + + function normalizeBaseUrl(value = '') { + const trimmed = String(value || '').trim() || DEFAULT_BASE_URL; + try { + return new URL(trimmed).toString().replace(/\/+$/, ''); + } catch { + return DEFAULT_BASE_URL; + } + } + + function normalizeText(value = '', fallback = '') { + return String(value || '').trim() || fallback; + } + + function normalizeNexSmsCountryId(value, fallback = DEFAULT_COUNTRY_ID) { + const parsed = Math.floor(Number(value)); + if (Number.isFinite(parsed) && parsed >= 0) { + return parsed; + } + const fallbackParsed = Math.floor(Number(fallback)); + if (Number.isFinite(fallbackParsed) && fallbackParsed >= 0) { + return fallbackParsed; + } + return DEFAULT_COUNTRY_ID; + } + + function normalizeNexSmsCountryLabel(value = '', fallback = DEFAULT_COUNTRY_LABEL) { + return normalizeText(value, fallback); + } + + function normalizeNexSmsServiceCode(value = '', fallback = DEFAULT_SERVICE_CODE) { + const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); + if (normalized) { + return normalized; + } + const fallbackNormalized = String(fallback || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); + return fallbackNormalized || DEFAULT_SERVICE_CODE; + } + + function normalizeNexSmsCountryOrder(value = []) { + const source = Array.isArray(value) + ? value + : String(value || '') + .split(/[\r\n,,;;]+/) + .map((entry) => String(entry || '').trim()) + .filter(Boolean); + const normalized = []; + const seen = new Set(); + + source.forEach((entry) => { + let id = -1; + let label = ''; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + id = normalizeNexSmsCountryId(entry.id ?? entry.countryId, -1); + label = normalizeText(entry.label ?? entry.countryLabel, ''); + } else { + const text = String(entry || '').trim(); + const structured = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/); + id = normalizeNexSmsCountryId(structured?.[1] || text, -1); + label = normalizeText(structured?.[2], ''); + } + if (id < 0 || seen.has(id)) { + return; + } + seen.add(id); + normalized.push({ + id, + label: label || `Country #${id}`, + }); + if (normalized.length >= 20) { + return; + } + }); + + return normalized; + } + + function resolveCountryCandidates(state = {}) { + const candidates = normalizeNexSmsCountryOrder(state?.nexSmsCountryOrder); + if (candidates.length) { + return candidates; + } + return [{ + id: normalizeNexSmsCountryId(state?.nexSmsCountryId, DEFAULT_COUNTRY_ID), + label: normalizeNexSmsCountryLabel(state?.nexSmsCountryLabel, DEFAULT_COUNTRY_LABEL), + }]; + } + + function parsePayload(text) { + const trimmed = String(text || '').trim(); + if (!trimmed) { + return ''; + } + try { + return JSON.parse(trimmed); + } catch { + return trimmed; + } + } + + function describePayload(raw) { + if (typeof raw === 'string') { + return raw.trim(); + } + if (raw && typeof raw === 'object') { + const message = normalizeText(raw.message || raw.error || raw.msg || raw.statusText, ''); + if (message) { + return message; + } + try { + return JSON.stringify(raw); + } catch { + return String(raw); + } + } + return String(raw || '').trim(); + } + + function isSuccessPayload(payload) { + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + return false; + } + return Number(payload.code) === 0; + } + + function resolveConfig(state = {}, deps = {}) { + return { + apiKey: normalizeText(state?.nexSmsApiKey), + baseUrl: normalizeBaseUrl(state?.nexSmsBaseUrl || DEFAULT_BASE_URL), + serviceCode: normalizeNexSmsServiceCode(state?.nexSmsServiceCode, DEFAULT_SERVICE_CODE), + fetchImpl: deps.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null), + requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS, + }; + } + + async function fetchPayload(config, path, actionLabel, options = {}) { + if (!config.fetchImpl) { + throw new Error('NexSMS 网络请求实现不可用。'); + } + if (!config.apiKey) { + throw new Error('NexSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); + } + const controller = typeof AbortController === 'function' ? new AbortController() : null; + const timeoutId = controller + ? setTimeout(() => controller.abort(), Number(config.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS) + : null; + + try { + const method = String(options.method || 'GET').trim().toUpperCase() || 'GET'; + const requestUrl = new URL(path.replace(/^\/+/, ''), `${config.baseUrl.replace(/\/+$/, '')}/`); + requestUrl.searchParams.set('apiKey', config.apiKey); + Object.entries(options.query || {}).forEach(([key, value]) => { + if (value === undefined || value === null || value === '') { + return; + } + requestUrl.searchParams.set(key, String(value)); + }); + const headers = { + Accept: 'application/json', + ...(options.headers && typeof options.headers === 'object' ? options.headers : {}), + }; + const requestInit = { + method, + headers, + signal: controller?.signal, + }; + if (method !== 'GET' && method !== 'HEAD' && options.body !== undefined) { + requestInit.body = typeof options.body === 'string' + ? options.body + : JSON.stringify(options.body); + if (!requestInit.headers['Content-Type']) { + requestInit.headers['Content-Type'] = 'application/json'; + } + } + const response = await config.fetchImpl(requestUrl.toString(), requestInit); + const text = await response.text(); + const payload = parsePayload(text); + if (!response.ok) { + const error = new Error(`${actionLabel}失败:${describePayload(payload) || response.status}`); + error.payload = payload; + error.status = response.status; + throw error; + } + return payload; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`${actionLabel}超时。`); + } + throw error; + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } + } + } + + async function fetchBalance(state = {}, deps = {}) { + const config = resolveConfig(state, deps); + const payload = await fetchPayload(config, '/api/user/getBalance', 'NexSMS get balance'); + if (!isSuccessPayload(payload)) { + throw new Error(`NexSMS get balance 失败:${describePayload(payload) || 'empty response'}`); + } + const balance = Number(payload?.data?.balance); + return { + balance: Number.isFinite(balance) ? balance : null, + raw: payload, + }; + } + + async function fetchPrices(state = {}, countryConfig = {}, deps = {}) { + const config = resolveConfig(state, deps); + const countryId = normalizeNexSmsCountryId(countryConfig?.id, DEFAULT_COUNTRY_ID); + return fetchPayload(config, '/api/getCountryByService', 'NexSMS getCountryByService', { + query: { + serviceCode: config.serviceCode, + countryId, + }, + }); + } + + function createProvider(deps = {}) { + const providerDeps = { + fetchImpl: deps.fetchImpl, + requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS, + }; + return { + id: PROVIDER_ID, + label: 'NexSMS', + defaultCountryId: DEFAULT_COUNTRY_ID, + defaultCountryLabel: DEFAULT_COUNTRY_LABEL, + defaultProduct: DEFAULT_SERVICE_LABEL, + defaultServiceCode: DEFAULT_SERVICE_CODE, + normalizeCountryId: normalizeNexSmsCountryId, + normalizeCountryLabel: normalizeNexSmsCountryLabel, + normalizeCountryOrder: normalizeNexSmsCountryOrder, + normalizeServiceCode: normalizeNexSmsServiceCode, + resolveCountryCandidates, + fetchBalance: (state) => fetchBalance(state, providerDeps), + fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps), + describePayload, + isSuccessPayload, + }; + } + + return { + PROVIDER_ID, + DEFAULT_BASE_URL, + DEFAULT_COUNTRY_ID, + DEFAULT_COUNTRY_LABEL, + DEFAULT_SERVICE_CODE, + DEFAULT_SERVICE_LABEL, + createProvider, + describePayload, + isSuccessPayload, + normalizeNexSmsCountryId, + normalizeNexSmsCountryLabel, + normalizeNexSmsCountryOrder, + normalizeNexSmsServiceCode, + }; +}); diff --git a/phone-sms/providers/registry.js b/phone-sms/providers/registry.js index 070da152..d46cd286 100644 --- a/phone-sms/providers/registry.js +++ b/phone-sms/providers/registry.js @@ -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, diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 401a97f2..8eaff9bc 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -1444,11 +1444,15 @@