214 lines
7.6 KiB
JavaScript
214 lines
7.6 KiB
JavaScript
// phone-sms/providers/hero-sms.js — HeroSMS 接码平台适配层
|
||
(function attachHeroSmsProvider(root, factory) {
|
||
root.PhoneSmsHeroSmsProvider = factory();
|
||
})(typeof self !== 'undefined' ? self : globalThis, function createHeroSmsProviderModule() {
|
||
const PROVIDER_ID = 'hero-sms';
|
||
const DEFAULT_BASE_URL = 'https://hero-sms.com/stubs/handler_api.php';
|
||
const DEFAULT_SERVICE_CODE = 'dr';
|
||
const DEFAULT_SERVICE_LABEL = 'OpenAI';
|
||
const DEFAULT_COUNTRY_ID = 52;
|
||
const DEFAULT_COUNTRY_LABEL = 'Thailand';
|
||
const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
|
||
|
||
function normalizeHeroSmsCountryId(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));
|
||
return Number.isFinite(fallbackParsed) && fallbackParsed > 0 ? fallbackParsed : DEFAULT_COUNTRY_ID;
|
||
}
|
||
|
||
function normalizeHeroSmsCountryLabel(value = '', fallback = DEFAULT_COUNTRY_LABEL) {
|
||
return String(value || '').trim() || fallback;
|
||
}
|
||
|
||
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 normalizeHeroSmsCountryFallback(value = []) {
|
||
const source = Array.isArray(value)
|
||
? value
|
||
: String(value || '')
|
||
.split(/[\r\n,,;;]+/)
|
||
.map((entry) => String(entry || '').trim())
|
||
.filter(Boolean);
|
||
const seen = new Set();
|
||
const normalized = [];
|
||
for (const entry of source) {
|
||
let id = 0;
|
||
let label = '';
|
||
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||
id = normalizeHeroSmsCountryId(entry.id ?? entry.countryId, 0);
|
||
label = String((entry.label ?? entry.countryLabel) || '').trim();
|
||
} else {
|
||
const text = String(entry || '').trim();
|
||
const structured = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/);
|
||
id = normalizeHeroSmsCountryId(structured?.[1] || text, 0);
|
||
label = String(structured?.[2] || '').trim();
|
||
}
|
||
if (!id || seen.has(id)) continue;
|
||
seen.add(id);
|
||
normalized.push({ id, label: label || `Country #${id}` });
|
||
if (normalized.length >= 20) break;
|
||
}
|
||
return normalized;
|
||
}
|
||
|
||
function normalizeBaseUrl(value = '') {
|
||
const trimmed = String(value || '').trim() || DEFAULT_BASE_URL;
|
||
try {
|
||
return new URL(trimmed).toString();
|
||
} catch {
|
||
return DEFAULT_BASE_URL;
|
||
}
|
||
}
|
||
|
||
function buildUrl(config = {}, query = {}) {
|
||
const url = new URL(normalizeBaseUrl(config.baseUrl));
|
||
Object.entries(query || {}).forEach(([key, value]) => {
|
||
if (value === undefined || value === null || value === '') return;
|
||
url.searchParams.set(key, String(value));
|
||
});
|
||
return url.toString();
|
||
}
|
||
|
||
function parsePayload(text) {
|
||
const trimmed = String(text || '').trim();
|
||
if (!trimmed) return '';
|
||
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
||
try { return JSON.parse(trimmed); } catch { return trimmed; }
|
||
}
|
||
return trimmed;
|
||
}
|
||
|
||
function describePayload(raw) {
|
||
if (typeof raw === 'string') return raw.trim();
|
||
if (raw && typeof raw === 'object') {
|
||
const direct = String(raw.message || raw.msg || raw.error || raw.title || raw.status || '').trim();
|
||
if (direct) return direct;
|
||
try { return JSON.stringify(raw); } catch { return String(raw); }
|
||
}
|
||
return String(raw || '').trim();
|
||
}
|
||
|
||
function resolveConfig(state = {}, deps = {}) {
|
||
return {
|
||
apiKey: String(state.heroSmsApiKey || '').trim(),
|
||
baseUrl: state.heroSmsBaseUrl || DEFAULT_BASE_URL,
|
||
fetchImpl: deps.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null),
|
||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||
};
|
||
}
|
||
|
||
async function fetchPayload(config, query, actionLabel = 'HeroSMS request') {
|
||
if (query.api_key === undefined && config.apiKey) {
|
||
query = { api_key: config.apiKey, ...query };
|
||
}
|
||
if (!config.fetchImpl) {
|
||
throw new Error('HeroSMS 网络请求实现不可用。');
|
||
}
|
||
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||
const timeoutId = controller
|
||
? setTimeout(() => controller.abort(), Number(config.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS)
|
||
: null;
|
||
try {
|
||
const response = await config.fetchImpl(buildUrl(config, query), {
|
||
method: 'GET',
|
||
signal: controller?.signal,
|
||
});
|
||
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);
|
||
}
|
||
}
|
||
|
||
function resolveCountryConfig(state = {}) {
|
||
return {
|
||
id: normalizeHeroSmsCountryId(state.heroSmsCountryId),
|
||
label: normalizeHeroSmsCountryLabel(state.heroSmsCountryLabel),
|
||
};
|
||
}
|
||
|
||
function resolveCountryCandidates(state = {}) {
|
||
const primary = resolveCountryConfig(state);
|
||
const seen = new Set([primary.id]);
|
||
const candidates = [primary];
|
||
normalizeHeroSmsCountryFallback(state.heroSmsCountryFallback).forEach((entry) => {
|
||
const id = normalizeHeroSmsCountryId(entry.id, 0);
|
||
if (!id || seen.has(id)) return;
|
||
seen.add(id);
|
||
candidates.push({ id, label: normalizeHeroSmsCountryLabel(entry.label, `Country #${id}`) });
|
||
});
|
||
return candidates;
|
||
}
|
||
|
||
async function fetchBalance(state = {}, deps = {}) {
|
||
const config = resolveConfig(state, deps);
|
||
if (!config.apiKey) {
|
||
throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。');
|
||
}
|
||
const payload = await fetchPayload(config, { action: 'getBalance' }, 'HeroSMS getBalance');
|
||
const balance = Number(String(describePayload(payload)).replace(/^ACCESS_BALANCE:/i, '').trim());
|
||
return { balance, raw: payload };
|
||
}
|
||
|
||
async function fetchPrices(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
|
||
const config = resolveConfig(state, deps);
|
||
return fetchPayload(config, {
|
||
action: 'getPrices',
|
||
service: DEFAULT_SERVICE_CODE,
|
||
country: normalizeHeroSmsCountryId(countryConfig?.id),
|
||
}, 'HeroSMS getPrices');
|
||
}
|
||
|
||
function createProvider(deps = {}) {
|
||
return {
|
||
id: PROVIDER_ID,
|
||
label: 'HeroSMS',
|
||
defaultCountryId: DEFAULT_COUNTRY_ID,
|
||
defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
|
||
defaultProduct: DEFAULT_SERVICE_LABEL,
|
||
normalizeCountryId: normalizeHeroSmsCountryId,
|
||
normalizeCountryLabel: normalizeHeroSmsCountryLabel,
|
||
normalizeCountryFallback: normalizeHeroSmsCountryFallback,
|
||
normalizeMaxPrice: normalizeHeroSmsMaxPrice,
|
||
resolveCountryCandidates,
|
||
fetchBalance: (state) => fetchBalance(state, deps),
|
||
fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, deps),
|
||
describePayload,
|
||
};
|
||
}
|
||
|
||
return {
|
||
PROVIDER_ID,
|
||
DEFAULT_BASE_URL,
|
||
DEFAULT_COUNTRY_ID,
|
||
DEFAULT_COUNTRY_LABEL,
|
||
DEFAULT_SERVICE_CODE,
|
||
DEFAULT_SERVICE_LABEL,
|
||
createProvider,
|
||
describePayload,
|
||
normalizeHeroSmsCountryFallback,
|
||
normalizeHeroSmsCountryId,
|
||
normalizeHeroSmsCountryLabel,
|
||
normalizeHeroSmsMaxPrice,
|
||
};
|
||
});
|