Files
FlowPilot/phone-sms/providers/madao.js
T
2026-05-29 00:44:30 +08:00

463 lines
16 KiB
JavaScript

// 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,
};
});