import GuJumpgate snapshot from GitHub
This commit is contained in:
@@ -0,0 +1,920 @@
|
||||
// phone-sms/providers/five-sim.js — 5sim 接码平台适配层
|
||||
(function attachFiveSimSmsProvider(root, factory) {
|
||||
root.PhoneSmsFiveSimProvider = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createFiveSimSmsProviderModule() {
|
||||
const PROVIDER_ID = '5sim';
|
||||
const DEFAULT_BASE_URL = 'https://5sim.net';
|
||||
const DEFAULT_PRODUCT = 'openai';
|
||||
const DEFAULT_OPERATOR = 'any';
|
||||
const DEFAULT_COUNTRY_ID = 'vietnam';
|
||||
const DEFAULT_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
|
||||
const DEFAULT_MAX_USES = 3;
|
||||
const FIVE_SIM_RATE_LIMIT_ERROR_PREFIX = 'FIVE_SIM_RATE_LIMIT::';
|
||||
const MAX_PRICE_CANDIDATES = 8;
|
||||
const SUPPORTED_COUNTRY_ITEMS = Object.freeze([
|
||||
{ id: 'indonesia', label: '印度尼西亚 (Indonesia)' },
|
||||
{ id: 'thailand', label: '泰国 (Thailand)' },
|
||||
{ id: 'england', label: '英国 (England)' },
|
||||
{ id: 'usa', label: '美国 (United States)' },
|
||||
{ id: 'japan', label: '日本 (Japan)' },
|
||||
{ id: 'germany', label: '德国 (Germany)' },
|
||||
{ id: 'vietnam', label: '越南 (Vietnam)' },
|
||||
]);
|
||||
const SUPPORTED_COUNTRY_ID_SET = new Set(SUPPORTED_COUNTRY_ITEMS.map((item) => item.id));
|
||||
const COUNTRY_CN_BY_ID = Object.freeze({
|
||||
afghanistan: '阿富汗',
|
||||
albania: '阿尔巴尼亚',
|
||||
algeria: '阿尔及利亚',
|
||||
angola: '安哥拉',
|
||||
argentina: '阿根廷',
|
||||
armenia: '亚美尼亚',
|
||||
australia: '澳大利亚',
|
||||
austria: '奥地利',
|
||||
azerbaijan: '阿塞拜疆',
|
||||
bahamas: '巴哈马',
|
||||
bahrain: '巴林',
|
||||
bangladesh: '孟加拉国',
|
||||
belarus: '白俄罗斯',
|
||||
belgium: '比利时',
|
||||
bolivia: '玻利维亚',
|
||||
bosnia: '波黑',
|
||||
brazil: '巴西',
|
||||
bulgaria: '保加利亚',
|
||||
cambodia: '柬埔寨',
|
||||
cameroon: '喀麦隆',
|
||||
canada: '加拿大',
|
||||
chile: '智利',
|
||||
china: '中国',
|
||||
colombia: '哥伦比亚',
|
||||
croatia: '克罗地亚',
|
||||
cyprus: '塞浦路斯',
|
||||
czech: '捷克',
|
||||
denmark: '丹麦',
|
||||
egypt: '埃及',
|
||||
england: '英国',
|
||||
estonia: '爱沙尼亚',
|
||||
ethiopia: '埃塞俄比亚',
|
||||
finland: '芬兰',
|
||||
france: '法国',
|
||||
georgia: '格鲁吉亚',
|
||||
germany: '德国',
|
||||
ghana: '加纳',
|
||||
greece: '希腊',
|
||||
hongkong: '中国香港',
|
||||
hungary: '匈牙利',
|
||||
india: '印度',
|
||||
indonesia: '印度尼西亚',
|
||||
ireland: '爱尔兰',
|
||||
israel: '以色列',
|
||||
italy: '意大利',
|
||||
japan: '日本',
|
||||
jordan: '约旦',
|
||||
kazakhstan: '哈萨克斯坦',
|
||||
kenya: '肯尼亚',
|
||||
kyrgyzstan: '吉尔吉斯斯坦',
|
||||
laos: '老挝',
|
||||
latvia: '拉脱维亚',
|
||||
lithuania: '立陶宛',
|
||||
malaysia: '马来西亚',
|
||||
mexico: '墨西哥',
|
||||
moldova: '摩尔多瓦',
|
||||
morocco: '摩洛哥',
|
||||
myanmar: '缅甸',
|
||||
nepal: '尼泊尔',
|
||||
netherlands: '荷兰',
|
||||
newzealand: '新西兰',
|
||||
nigeria: '尼日利亚',
|
||||
norway: '挪威',
|
||||
pakistan: '巴基斯坦',
|
||||
paraguay: '巴拉圭',
|
||||
peru: '秘鲁',
|
||||
philippines: '菲律宾',
|
||||
poland: '波兰',
|
||||
portugal: '葡萄牙',
|
||||
romania: '罗马尼亚',
|
||||
russia: '俄罗斯',
|
||||
saudiarabia: '沙特阿拉伯',
|
||||
serbia: '塞尔维亚',
|
||||
singapore: '新加坡',
|
||||
slovakia: '斯洛伐克',
|
||||
slovenia: '斯洛文尼亚',
|
||||
southafrica: '南非',
|
||||
spain: '西班牙',
|
||||
srilanka: '斯里兰卡',
|
||||
sweden: '瑞典',
|
||||
switzerland: '瑞士',
|
||||
taiwan: '中国台湾',
|
||||
tajikistan: '塔吉克斯坦',
|
||||
tanzania: '坦桑尼亚',
|
||||
thailand: '泰国',
|
||||
turkey: '土耳其',
|
||||
ukraine: '乌克兰',
|
||||
uruguay: '乌拉圭',
|
||||
usa: '美国',
|
||||
uzbekistan: '乌兹别克斯坦',
|
||||
venezuela: '委内瑞拉',
|
||||
vietnam: '越南',
|
||||
});
|
||||
|
||||
function normalizeFiveSimCountryId(value, fallback = DEFAULT_COUNTRY_ID) {
|
||||
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
const fallbackSource = fallback === undefined || fallback === null ? DEFAULT_COUNTRY_ID : fallback;
|
||||
const normalizedFallback = String(fallbackSource).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
if (!normalizedFallback) {
|
||||
return '';
|
||||
}
|
||||
return normalizedFallback || DEFAULT_COUNTRY_ID;
|
||||
}
|
||||
|
||||
function getCountryIdFromPayload(record = {}, fallback = DEFAULT_COUNTRY_ID) {
|
||||
if (record?.country && typeof record.country === 'object' && !Array.isArray(record.country)) {
|
||||
return normalizeFiveSimCountryId(record.country.name || record.country.id || record.country.slug, fallback);
|
||||
}
|
||||
return normalizeFiveSimCountryId(record.countryId ?? record.country, fallback);
|
||||
}
|
||||
|
||||
function getCountryLabelFromPayload(record = {}, fallbackLabel = DEFAULT_COUNTRY_LABEL, fallbackId = DEFAULT_COUNTRY_ID) {
|
||||
if (record?.country && typeof record.country === 'object' && !Array.isArray(record.country)) {
|
||||
const countryId = normalizeFiveSimCountryId(record.country.name || record.country.id || record.country.slug || fallbackId, fallbackId);
|
||||
return formatFiveSimCountryLabel(countryId, record.country.text_en || record.country.label || countryId, fallbackLabel || fallbackId);
|
||||
}
|
||||
const countryId = normalizeFiveSimCountryId(record.countryId ?? record.country ?? fallbackId, fallbackId);
|
||||
return normalizeFiveSimCountryLabel(record.countryLabel, formatFiveSimCountryLabel(countryId, countryId, fallbackLabel || fallbackId));
|
||||
}
|
||||
|
||||
function normalizeFiveSimCountryLabel(value = '', fallback = DEFAULT_COUNTRY_LABEL) {
|
||||
return String(value || '').trim() || fallback;
|
||||
}
|
||||
|
||||
function formatFiveSimCountryLabel(id = '', englishValue = '', fallback = DEFAULT_COUNTRY_LABEL) {
|
||||
const countryId = normalizeFiveSimCountryId(id, '');
|
||||
const english = normalizeFiveSimCountryLabel(englishValue || countryId || fallback, fallback);
|
||||
const chinese = COUNTRY_CN_BY_ID[countryId] || '';
|
||||
if (chinese && english) {
|
||||
if (String(english).includes(chinese)) {
|
||||
return english;
|
||||
}
|
||||
if (chinese.toLowerCase() !== english.toLowerCase()) {
|
||||
return `${chinese} (${english})`;
|
||||
}
|
||||
}
|
||||
return chinese || english;
|
||||
}
|
||||
|
||||
function normalizeFiveSimOperator(value = '') {
|
||||
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || DEFAULT_OPERATOR;
|
||||
}
|
||||
|
||||
function normalizeFiveSimMaxPrice(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 normalizeFiveSimCountryFallback(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 = '';
|
||||
let label = '';
|
||||
|
||||
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||||
id = normalizeFiveSimCountryId(entry.id ?? entry.countryId ?? entry.slug, '');
|
||||
label = String((entry.label ?? entry.countryLabel ?? entry.text_en ?? entry.name) || '').trim();
|
||||
} else {
|
||||
const text = String(entry || '').trim();
|
||||
const structured = text.match(/^([a-z0-9_-]+)\s*(?:[:|/-]\s*(.+))?$/i);
|
||||
id = normalizeFiveSimCountryId(structured?.[1] || text, '');
|
||||
label = String(structured?.[2] || '').trim();
|
||||
}
|
||||
|
||||
if (!id || seen.has(id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(id);
|
||||
normalized.push({
|
||||
id,
|
||||
label: label || formatFiveSimCountryLabel(id, id, id),
|
||||
});
|
||||
if (normalized.length >= 20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizePrice(value) {
|
||||
const price = Number(value);
|
||||
if (!Number.isFinite(price) || price < 0) {
|
||||
return null;
|
||||
}
|
||||
return price;
|
||||
}
|
||||
|
||||
function buildSortedUniquePriceCandidates(values = []) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((value) => normalizePrice(value))
|
||||
.filter((value) => value !== null)
|
||||
.map((value) => Math.round(value * 10000) / 10000)
|
||||
)
|
||||
)
|
||||
.sort((left, right) => left - right)
|
||||
.slice(0, MAX_PRICE_CANDIDATES);
|
||||
}
|
||||
|
||||
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 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 normalizeBaseUrl(value = '') {
|
||||
const trimmed = String(value || '').trim() || DEFAULT_BASE_URL;
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
return `${url.origin}${url.pathname.replace(/\/+$/, '')}`;
|
||||
} catch {
|
||||
return DEFAULT_BASE_URL;
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(config = {}, path = '', query = {}) {
|
||||
const url = new URL(path, `${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();
|
||||
}
|
||||
|
||||
async function fetchJson(config = {}, path = '', options = {}) {
|
||||
const { query = {}, actionLabel = '5sim 请求', requireAuth = true } = options;
|
||||
const token = String(config.apiKey || '').trim();
|
||||
if (requireAuth && !token) {
|
||||
throw new Error('缺少 5sim 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 response = await config.fetchImpl(buildUrl(config, path, query), {
|
||||
method: 'GET',
|
||||
signal: controller?.signal,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(requireAuth ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
});
|
||||
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 resolveConfig(state = {}, deps = {}) {
|
||||
return {
|
||||
apiKey: String(state.fiveSimApiKey || '').trim(),
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
fetchImpl: deps.fetchImpl,
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCountryConfig(state = {}) {
|
||||
const rawCountryId = normalizeFiveSimCountryId(state.fiveSimCountryId, '');
|
||||
const id = rawCountryId || DEFAULT_COUNTRY_ID;
|
||||
return {
|
||||
id,
|
||||
label: formatFiveSimCountryLabel(id, state.fiveSimCountryLabel || id, DEFAULT_COUNTRY_LABEL),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCountryCandidates(state = {}) {
|
||||
const primary = resolveCountryConfig(state);
|
||||
const fallbackList = normalizeFiveSimCountryFallback(state.fiveSimCountryFallback);
|
||||
const seen = new Set([primary.id]);
|
||||
const candidates = [primary];
|
||||
|
||||
fallbackList.forEach((entry) => {
|
||||
const nextId = normalizeFiveSimCountryId(entry.id, '');
|
||||
if (!nextId || seen.has(nextId)) {
|
||||
return;
|
||||
}
|
||||
seen.add(nextId);
|
||||
candidates.push({
|
||||
id: nextId,
|
||||
label: formatFiveSimCountryLabel(nextId, entry.label || nextId, nextId),
|
||||
});
|
||||
});
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async function fetchBalance(state = {}, deps = {}) {
|
||||
const config = resolveConfig(state, deps);
|
||||
const payload = await fetchJson(config, '/v1/user/profile', {
|
||||
actionLabel: '5sim 查询余额',
|
||||
});
|
||||
return {
|
||||
balance: Number(payload?.balance),
|
||||
frozenBalance: Number(payload?.frozen_balance),
|
||||
rating: Number(payload?.rating),
|
||||
raw: payload,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchCountries(_state = {}, deps = {}) {
|
||||
const config = {
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
fetchImpl: deps.fetchImpl,
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
const payload = await fetchJson(config, '/v1/guest/countries', {
|
||||
actionLabel: '5sim 查询国家列表',
|
||||
requireAuth: false,
|
||||
});
|
||||
return Object.entries(payload || {})
|
||||
.map(([slug, entry]) => {
|
||||
const id = normalizeFiveSimCountryId(slug, '');
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
label: formatFiveSimCountryLabel(id, entry?.text_en || slug, slug),
|
||||
searchText: [
|
||||
slug,
|
||||
formatFiveSimCountryLabel(id, entry?.text_en || slug, slug),
|
||||
entry?.text_en,
|
||||
entry?.text_ru,
|
||||
Object.keys(entry?.iso || {}).join(' '),
|
||||
Object.keys(entry?.prefix || {}).join(' '),
|
||||
].filter(Boolean).join(' '),
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((left, right) => String(left.label || '').localeCompare(String(right.label || '')));
|
||||
}
|
||||
|
||||
function collectPriceEntries(payload, entries = []) {
|
||||
if (Array.isArray(payload)) {
|
||||
payload.forEach((entry) => collectPriceEntries(entry, entries));
|
||||
return entries;
|
||||
}
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return entries;
|
||||
}
|
||||
|
||||
const cost = normalizePrice(payload.cost ?? payload.Price ?? payload.price);
|
||||
if (cost !== null) {
|
||||
const count = Number(payload.count ?? payload.Qty ?? payload.qty);
|
||||
entries.push({
|
||||
cost,
|
||||
count: Number.isFinite(count) ? count : 0,
|
||||
inStock: !Number.isFinite(count) || count > 0,
|
||||
});
|
||||
}
|
||||
|
||||
Object.values(payload).forEach((entry) => collectPriceEntries(entry, entries));
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function fetchPrices(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
|
||||
const config = {
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
fetchImpl: deps.fetchImpl,
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
return fetchJson(config, '/v1/guest/prices', {
|
||||
query: {
|
||||
country: normalizeFiveSimCountryId(countryConfig?.id),
|
||||
product: DEFAULT_PRODUCT,
|
||||
},
|
||||
actionLabel: '5sim 查询价格',
|
||||
requireAuth: false,
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchProducts(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
|
||||
const config = {
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
fetchImpl: deps.fetchImpl,
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
const operator = normalizeFiveSimOperator(state.fiveSimOperator);
|
||||
return fetchJson(
|
||||
config,
|
||||
`/v1/guest/products/${encodeURIComponent(normalizeFiveSimCountryId(countryConfig?.id))}/${encodeURIComponent(operator)}`,
|
||||
{
|
||||
actionLabel: '5sim 查询产品价格',
|
||||
requireAuth: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function resolvePricePlan(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
|
||||
const userLimitText = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
|
||||
const userLimit = userLimitText ? Number(userLimitText) : null;
|
||||
let priceCandidates = [];
|
||||
|
||||
try {
|
||||
const productsPayload = await fetchProducts(state, countryConfig, deps);
|
||||
const openaiProduct = productsPayload?.[DEFAULT_PRODUCT];
|
||||
const productPrice = normalizePrice(openaiProduct?.Price ?? openaiProduct?.price ?? openaiProduct?.cost);
|
||||
const productQty = Number(openaiProduct?.Qty ?? openaiProduct?.qty ?? openaiProduct?.count);
|
||||
if (productPrice !== null && (!Number.isFinite(productQty) || productQty > 0)) {
|
||||
priceCandidates.push(productPrice);
|
||||
}
|
||||
} catch (_) {
|
||||
// Products endpoint is only used to find the buy-compatible operator price.
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await fetchPrices(state, countryConfig, deps);
|
||||
priceCandidates = [
|
||||
...priceCandidates,
|
||||
...buildSortedUniquePriceCandidates(
|
||||
collectPriceEntries(payload, [])
|
||||
.filter((entry) => entry.inStock)
|
||||
.map((entry) => entry.cost)
|
||||
),
|
||||
];
|
||||
} catch (_) {
|
||||
// Best-effort lookup. Purchase can still be attempted without a catalog price.
|
||||
}
|
||||
priceCandidates = buildSortedUniquePriceCandidates(priceCandidates);
|
||||
|
||||
const minCatalogPrice = priceCandidates.length > 0 ? priceCandidates[0] : null;
|
||||
if (userLimit !== null) {
|
||||
const bounded = priceCandidates.filter((price) => price <= userLimit);
|
||||
return {
|
||||
prices: bounded.length > 0 ? [userLimit, ...bounded.filter((price) => price !== userLimit)] : [userLimit],
|
||||
userLimit,
|
||||
minCatalogPrice,
|
||||
};
|
||||
}
|
||||
|
||||
if (priceCandidates.length > 0) {
|
||||
return { prices: priceCandidates, userLimit: null, minCatalogPrice };
|
||||
}
|
||||
return { prices: [null], userLimit: null, minCatalogPrice: null };
|
||||
}
|
||||
|
||||
function normalizeActivation(record, fallback = {}) {
|
||||
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
||||
return null;
|
||||
}
|
||||
const activationId = String(record.activationId ?? record.id ?? '').trim();
|
||||
const phoneNumber = String(record.phoneNumber ?? record.phone ?? '').trim();
|
||||
if (!activationId || !phoneNumber) {
|
||||
return null;
|
||||
}
|
||||
const countryId = getCountryIdFromPayload(record, fallback.countryId || DEFAULT_COUNTRY_ID);
|
||||
const countryLabel = getCountryLabelFromPayload(record, fallback.countryLabel || countryId, countryId);
|
||||
return {
|
||||
activationId,
|
||||
phoneNumber,
|
||||
provider: PROVIDER_ID,
|
||||
serviceCode: DEFAULT_PRODUCT,
|
||||
countryId,
|
||||
countryLabel,
|
||||
successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)),
|
||||
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_MAX_USES)),
|
||||
operator: normalizeFiveSimOperator(record.operator || fallback.operator),
|
||||
...(record.price !== undefined ? { price: Number(record.price) } : {}),
|
||||
...(record.status ? { status: String(record.status) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function isNoNumbersPayload(payloadOrMessage) {
|
||||
const text = describePayload(payloadOrMessage);
|
||||
return /no\s+free\s+phones|no\s+numbers|not\s+found/i.test(text);
|
||||
}
|
||||
|
||||
function isRateLimitPayload(payloadOrMessage) {
|
||||
const text = describePayload(payloadOrMessage);
|
||||
return /rate\s*limit|too\s*many\s*requests|request\s*limit|429/i.test(text);
|
||||
}
|
||||
|
||||
function buildFiveSimRateLimitError(details = []) {
|
||||
const suffix = Array.isArray(details) && details.length
|
||||
? `:${details.join(' | ')}。`
|
||||
: '。';
|
||||
return new Error(`${FIVE_SIM_RATE_LIMIT_ERROR_PREFIX}5sim 购买接口触发限流,请稍后再试${suffix}`);
|
||||
}
|
||||
|
||||
function isTerminalError(payloadOrMessage) {
|
||||
const text = describePayload(payloadOrMessage);
|
||||
return /not\s+enough\s+(?:user\s+)?balance|not\s+enough\s+rating|unauthorized|invalid\s+token|banned|bad\s+(?:country|operator)|no\s+product|server\s+offline/i.test(text);
|
||||
}
|
||||
|
||||
function assertMaxPriceCompatibleWithOperator(state = {}) {
|
||||
const maxPrice = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
|
||||
const operator = normalizeFiveSimOperator(state.fiveSimOperator);
|
||||
if (maxPrice && operator !== DEFAULT_OPERATOR) {
|
||||
throw new Error('5sim 价格上限仅支持运营商为 "any" 时使用;请清空价格上限,或先把运营商切换为 any。');
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeReuseEnabled(state = {}) {
|
||||
if (Object.prototype.hasOwnProperty.call(state, 'phoneSmsReuseEnabled')) {
|
||||
return state.phoneSmsReuseEnabled !== false;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(state, 'heroSmsReuseEnabled')) {
|
||||
return state.heroSmsReuseEnabled !== false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function buyActivationWithPrice(state = {}, countryConfig, maxPrice, deps = {}) {
|
||||
const config = resolveConfig(state, deps);
|
||||
const operator = normalizeFiveSimOperator(state.fiveSimOperator);
|
||||
const query = {};
|
||||
if (maxPrice !== null && maxPrice !== undefined) {
|
||||
query.maxPrice = maxPrice;
|
||||
}
|
||||
if (normalizeReuseEnabled(state)) {
|
||||
query.reuse = 1;
|
||||
}
|
||||
const payload = await fetchJson(
|
||||
config,
|
||||
`/v1/user/buy/activation/${encodeURIComponent(normalizeFiveSimCountryId(countryConfig.id))}/${encodeURIComponent(operator)}/${DEFAULT_PRODUCT}`,
|
||||
{
|
||||
query,
|
||||
actionLabel: '5sim 购买手机号',
|
||||
}
|
||||
);
|
||||
const activation = normalizeActivation(payload, {
|
||||
countryId: countryConfig.id,
|
||||
countryLabel: countryConfig.label,
|
||||
operator,
|
||||
});
|
||||
if (!activation) {
|
||||
const error = new Error(`5sim 购买手机号返回不可用响应:${describePayload(payload) || '空响应'}`);
|
||||
error.payload = payload;
|
||||
throw error;
|
||||
}
|
||||
return activation;
|
||||
}
|
||||
|
||||
async function requestActivation(state = {}, options = {}, deps = {}) {
|
||||
assertMaxPriceCompatibleWithOperator(state);
|
||||
|
||||
const allCountryCandidates = resolveCountryCandidates(state);
|
||||
const blockedCountryIds = new Set(
|
||||
(Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : [])
|
||||
.map((value) => normalizeFiveSimCountryId(value, ''))
|
||||
.filter(Boolean)
|
||||
);
|
||||
let countryCandidates = allCountryCandidates.filter((entry) => !blockedCountryIds.has(normalizeFiveSimCountryId(entry.id, '')));
|
||||
if (!countryCandidates.length) {
|
||||
countryCandidates = allCountryCandidates;
|
||||
if (blockedCountryIds.size && typeof deps.addLog === 'function') {
|
||||
await deps.addLog(
|
||||
'步骤 9:已选 5sim 国家均达到临时收码失败跳过阈值,本轮解除跳过并重新尝试。',
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const acquirePriority = String(state?.heroSmsAcquirePriority || 'country').trim().toLowerCase() === 'price' ? 'price' : 'country';
|
||||
const countryAttempts = countryCandidates.map((countryConfig, index) => ({
|
||||
index,
|
||||
countryConfig,
|
||||
pricePlan: null,
|
||||
orderingPrice: Number.POSITIVE_INFINITY,
|
||||
}));
|
||||
|
||||
if (acquirePriority === 'price') {
|
||||
for (const attempt of countryAttempts) {
|
||||
const pricePlan = await resolvePricePlan(state, attempt.countryConfig, deps);
|
||||
const numericPrices = Array.isArray(pricePlan?.prices)
|
||||
? pricePlan.prices.map(Number).filter((value) => Number.isFinite(value) && value >= 0)
|
||||
: [];
|
||||
const minCandidatePrice = numericPrices.length ? Math.min(...numericPrices) : null;
|
||||
const cappedByUserLimit = (
|
||||
pricePlan?.userLimit !== null
|
||||
&& pricePlan?.userLimit !== undefined
|
||||
&& pricePlan?.minCatalogPrice !== null
|
||||
&& pricePlan?.minCatalogPrice !== undefined
|
||||
&& Number(pricePlan.minCatalogPrice) > Number(pricePlan.userLimit)
|
||||
);
|
||||
attempt.pricePlan = pricePlan;
|
||||
attempt.orderingPrice = cappedByUserLimit
|
||||
? Number.POSITIVE_INFINITY
|
||||
: (minCandidatePrice !== null ? minCandidatePrice : Number.POSITIVE_INFINITY);
|
||||
}
|
||||
countryAttempts.sort((left, right) => (
|
||||
left.orderingPrice !== right.orderingPrice
|
||||
? left.orderingPrice - right.orderingPrice
|
||||
: left.index - right.index
|
||||
));
|
||||
}
|
||||
|
||||
const noNumbersByCountry = [];
|
||||
const rateLimitByCountry = [];
|
||||
let lastError = null;
|
||||
let lastFailureText = '';
|
||||
let sawOnlyRetryableFailures = true;
|
||||
for (const attempt of countryAttempts) {
|
||||
const countryConfig = attempt.countryConfig;
|
||||
const countryFailures = [];
|
||||
const pricePlan = attempt.pricePlan || await resolvePricePlan(state, countryConfig, deps);
|
||||
for (const maxPrice of pricePlan.prices) {
|
||||
try {
|
||||
const activation = await buyActivationWithPrice(state, countryConfig, maxPrice, deps);
|
||||
if (activation) {
|
||||
return activation;
|
||||
}
|
||||
lastFailureText = '空响应';
|
||||
} catch (error) {
|
||||
const payloadOrMessage = error?.payload || error?.message;
|
||||
if (isTerminalError(payloadOrMessage)) {
|
||||
throw new Error(`5sim 购买手机号失败:${describePayload(payloadOrMessage) || '空响应'}`);
|
||||
}
|
||||
const failureText = describePayload(payloadOrMessage) || lastFailureText;
|
||||
if (isRateLimitPayload(payloadOrMessage)) {
|
||||
lastFailureText = failureText;
|
||||
countryFailures.push(failureText || 'rate limit');
|
||||
continue;
|
||||
}
|
||||
if (isNoNumbersPayload(payloadOrMessage)) {
|
||||
lastFailureText = failureText;
|
||||
countryFailures.push(failureText || '无可用号码');
|
||||
continue;
|
||||
}
|
||||
sawOnlyRetryableFailures = false;
|
||||
lastError = error;
|
||||
lastFailureText = failureText;
|
||||
}
|
||||
}
|
||||
const countryFailureText = countryFailures.length
|
||||
? Array.from(new Set(countryFailures)).join(', ')
|
||||
: (lastFailureText || '无可用号码');
|
||||
if (countryFailures.some((text) => isRateLimitPayload(text))) {
|
||||
rateLimitByCountry.push(`${countryConfig.label}: ${countryFailureText}`);
|
||||
} else {
|
||||
noNumbersByCountry.push(`${countryConfig.label}: ${countryFailureText}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (rateLimitByCountry.length) {
|
||||
throw buildFiveSimRateLimitError(rateLimitByCountry);
|
||||
}
|
||||
|
||||
if (noNumbersByCountry.length && sawOnlyRetryableFailures) {
|
||||
throw new Error(`5sim 已尝试 ${countryCandidates.length} 个候选国家,均无可用号码:${noNumbersByCountry.join(' | ')}。`);
|
||||
}
|
||||
if (lastError) {
|
||||
throw lastError;
|
||||
}
|
||||
throw new Error(`5sim 获取手机号失败,最后状态:${lastFailureText || '未知'}。`);
|
||||
}
|
||||
|
||||
async function reuseActivation(state = {}, activation, deps = {}) {
|
||||
const normalizedActivation = normalizeActivation(activation);
|
||||
if (!normalizedActivation) {
|
||||
throw new Error('缺少可复用的 5sim 手机号订单。');
|
||||
}
|
||||
const phoneDigits = String(normalizedActivation.phoneNumber || '').replace(/\D+/g, '');
|
||||
if (!phoneDigits) {
|
||||
throw new Error('可复用的 5sim 手机号无效。');
|
||||
}
|
||||
const config = resolveConfig(state, deps);
|
||||
const numberWithoutPlus = String(normalizedActivation.phoneNumber || '')
|
||||
.replace(/^\+/, '')
|
||||
.replace(/[^0-9]+/g, '');
|
||||
const payload = await fetchJson(config, `/v1/user/reuse/${DEFAULT_PRODUCT}/${numberWithoutPlus || phoneDigits}`, {
|
||||
actionLabel: '5sim 复用手机号',
|
||||
});
|
||||
return normalizeActivation(payload, normalizedActivation);
|
||||
}
|
||||
|
||||
async function finishActivation(state = {}, activation, deps = {}) {
|
||||
const normalizedActivation = normalizeActivation(activation);
|
||||
if (!normalizedActivation) return '';
|
||||
const config = resolveConfig(state, deps);
|
||||
const payload = await fetchJson(config, `/v1/user/finish/${encodeURIComponent(normalizedActivation.activationId)}`, {
|
||||
actionLabel: '5sim 完成订单',
|
||||
});
|
||||
return describePayload(payload);
|
||||
}
|
||||
|
||||
async function cancelActivation(state = {}, activation, deps = {}) {
|
||||
const normalizedActivation = normalizeActivation(activation);
|
||||
if (!normalizedActivation) return '';
|
||||
const config = resolveConfig(state, deps);
|
||||
const payload = await fetchJson(config, `/v1/user/cancel/${encodeURIComponent(normalizedActivation.activationId)}`, {
|
||||
actionLabel: '5sim 取消订单',
|
||||
});
|
||||
return describePayload(payload);
|
||||
}
|
||||
|
||||
async function banActivation(state = {}, activation, deps = {}) {
|
||||
const normalizedActivation = normalizeActivation(activation);
|
||||
if (!normalizedActivation) return '';
|
||||
const config = resolveConfig(state, deps);
|
||||
const payload = await fetchJson(config, `/v1/user/ban/${encodeURIComponent(normalizedActivation.activationId)}`, {
|
||||
actionLabel: '5sim 拉黑号码',
|
||||
});
|
||||
return describePayload(payload);
|
||||
}
|
||||
|
||||
function extractVerificationCode(rawCodeOrText) {
|
||||
const trimmed = String(rawCodeOrText || '').trim();
|
||||
if (!trimmed) {
|
||||
return '';
|
||||
}
|
||||
const digitMatch = trimmed.match(/\b(\d{4,8})\b/);
|
||||
return digitMatch?.[1] || trimmed;
|
||||
}
|
||||
|
||||
function extractCodeFromOrder(payload) {
|
||||
const smsList = Array.isArray(payload?.sms) ? payload.sms : [];
|
||||
for (let index = smsList.length - 1; index >= 0; index -= 1) {
|
||||
const message = smsList[index] || {};
|
||||
const code = extractVerificationCode(message.code) || extractVerificationCode(message.text);
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function pollActivationCode(state = {}, activation, options = {}, deps = {}) {
|
||||
const normalizedActivation = normalizeActivation(activation);
|
||||
if (!normalizedActivation) {
|
||||
throw new Error('缺少手机号接码订单。');
|
||||
}
|
||||
const config = resolveConfig(state, deps);
|
||||
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 180000);
|
||||
const intervalMs = Math.max(1000, Number(options.intervalMs) || 5000);
|
||||
const maxRoundsRaw = Math.floor(Number(options.maxRounds));
|
||||
const maxRounds = Number.isFinite(maxRoundsRaw) && maxRoundsRaw > 0 ? maxRoundsRaw : 0;
|
||||
const start = Date.now();
|
||||
let pollCount = 0;
|
||||
let lastResponse = '';
|
||||
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (maxRounds > 0 && pollCount >= maxRounds) {
|
||||
break;
|
||||
}
|
||||
deps.throwIfStopped?.();
|
||||
const payload = await fetchJson(config, `/v1/user/check/${encodeURIComponent(normalizedActivation.activationId)}`, {
|
||||
actionLabel: '5sim 查询验证码',
|
||||
});
|
||||
pollCount += 1;
|
||||
lastResponse = describePayload(payload);
|
||||
if (typeof options.onStatus === 'function') {
|
||||
await options.onStatus({
|
||||
activation: normalizedActivation,
|
||||
elapsedMs: Date.now() - start,
|
||||
pollCount,
|
||||
statusText: String(payload?.status || lastResponse || '未知'),
|
||||
timeoutMs,
|
||||
});
|
||||
}
|
||||
const code = extractCodeFromOrder(payload);
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
const status = String(payload?.status || '').trim().toUpperCase();
|
||||
if (['CANCELED', 'BANNED', 'FINISHED', 'TIMEOUT'].includes(status)) {
|
||||
throw new Error(`5sim 查询验证码失败:订单状态 ${status}`);
|
||||
}
|
||||
if (typeof options.onWaitingForCode === 'function') {
|
||||
await options.onWaitingForCode({
|
||||
activation: normalizedActivation,
|
||||
elapsedMs: Date.now() - start,
|
||||
pollCount,
|
||||
statusText: String(payload?.status || lastResponse || '未知'),
|
||||
timeoutMs,
|
||||
});
|
||||
}
|
||||
await deps.sleepWithStop(intervalMs);
|
||||
}
|
||||
|
||||
const suffix = lastResponse ? ` 5sim 最后状态:${lastResponse}` : '';
|
||||
throw new Error(`PHONE_CODE_TIMEOUT::等待手机验证码超时。${suffix}`);
|
||||
}
|
||||
|
||||
function createProvider(deps = {}) {
|
||||
const providerDeps = {
|
||||
fetchImpl: deps.fetchImpl,
|
||||
sleepWithStop: deps.sleepWithStop,
|
||||
throwIfStopped: deps.throwIfStopped,
|
||||
addLog: deps.addLog,
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
return {
|
||||
id: PROVIDER_ID,
|
||||
label: '5sim',
|
||||
defaultCountryId: DEFAULT_COUNTRY_ID,
|
||||
defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
|
||||
supportedCountries: SUPPORTED_COUNTRY_ITEMS,
|
||||
defaultProduct: DEFAULT_PRODUCT,
|
||||
defaultOperator: DEFAULT_OPERATOR,
|
||||
normalizeCountryId: normalizeFiveSimCountryId,
|
||||
normalizeCountryLabel: normalizeFiveSimCountryLabel,
|
||||
formatCountryLabel: formatFiveSimCountryLabel,
|
||||
normalizeCountryFallback: normalizeFiveSimCountryFallback,
|
||||
normalizeMaxPrice: normalizeFiveSimMaxPrice,
|
||||
normalizeOperator: normalizeFiveSimOperator,
|
||||
resolveCountryCandidates,
|
||||
requestActivation: (state, options) => requestActivation(state, options, providerDeps),
|
||||
reuseActivation: (state, activation) => reuseActivation(state, activation, providerDeps),
|
||||
finishActivation: (state, activation) => finishActivation(state, activation, providerDeps),
|
||||
cancelActivation: (state, activation) => cancelActivation(state, activation, providerDeps),
|
||||
banActivation: (state, activation) => banActivation(state, activation, providerDeps),
|
||||
pollActivationCode: (state, activation, options) => pollActivationCode(state, activation, options, providerDeps),
|
||||
fetchBalance: (state) => fetchBalance(state, providerDeps),
|
||||
fetchCountries: (state) => fetchCountries(state, providerDeps),
|
||||
fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps),
|
||||
collectPriceEntries,
|
||||
describePayload,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
PROVIDER_ID,
|
||||
FIVE_SIM_RATE_LIMIT_ERROR_PREFIX,
|
||||
DEFAULT_COUNTRY_ID,
|
||||
DEFAULT_COUNTRY_LABEL,
|
||||
DEFAULT_OPERATOR,
|
||||
DEFAULT_PRODUCT,
|
||||
SUPPORTED_COUNTRY_ITEMS,
|
||||
createProvider,
|
||||
normalizeFiveSimCountryFallback,
|
||||
normalizeFiveSimCountryId,
|
||||
normalizeFiveSimCountryLabel,
|
||||
formatFiveSimCountryLabel,
|
||||
normalizeFiveSimMaxPrice,
|
||||
normalizeFiveSimOperator,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
// 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,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
// phone-sms/providers/registry.js - phone SMS provider registry
|
||||
(function attachPhoneSmsProviderRegistry(root, factory) {
|
||||
root.PhoneSmsProviderRegistry = factory(root);
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createPhoneSmsProviderRegistry(root) {
|
||||
const PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PROVIDER_FIVE_SIM = '5sim';
|
||||
const PROVIDER_NEXSMS = 'nexsms';
|
||||
const DEFAULT_PROVIDER = PROVIDER_HERO_SMS;
|
||||
const DEFAULT_PROVIDER_ORDER = Object.freeze([
|
||||
PROVIDER_HERO_SMS,
|
||||
PROVIDER_FIVE_SIM,
|
||||
PROVIDER_NEXSMS,
|
||||
]);
|
||||
const PROVIDER_DEFINITIONS = Object.freeze({
|
||||
[PROVIDER_HERO_SMS]: Object.freeze({
|
||||
id: PROVIDER_HERO_SMS,
|
||||
label: 'HeroSMS',
|
||||
moduleKey: 'PhoneSmsHeroSmsProvider',
|
||||
}),
|
||||
[PROVIDER_FIVE_SIM]: Object.freeze({
|
||||
id: PROVIDER_FIVE_SIM,
|
||||
label: '5sim',
|
||||
moduleKey: 'PhoneSmsFiveSimProvider',
|
||||
}),
|
||||
[PROVIDER_NEXSMS]: Object.freeze({
|
||||
id: PROVIDER_NEXSMS,
|
||||
label: 'NexSMS',
|
||||
moduleKey: 'PhoneSmsNexSmsProvider',
|
||||
}),
|
||||
});
|
||||
|
||||
function resolveProviderKey(value = '') {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return String(value.provider || value.id || value.value || '').trim().toLowerCase();
|
||||
}
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isKnownProviderId(providerId = '') {
|
||||
return Boolean(PROVIDER_DEFINITIONS[providerId]);
|
||||
}
|
||||
|
||||
function normalizeProviderId(value = '', fallback = DEFAULT_PROVIDER) {
|
||||
const normalized = resolveProviderKey(value);
|
||||
if (isKnownProviderId(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
const fallbackNormalized = resolveProviderKey(fallback);
|
||||
if (isKnownProviderId(fallbackNormalized)) {
|
||||
return fallbackNormalized;
|
||||
}
|
||||
return DEFAULT_PROVIDER;
|
||||
}
|
||||
|
||||
function normalizeProviderOrder(value = [], fallbackOrder = DEFAULT_PROVIDER_ORDER) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '')
|
||||
.split(/[\r\n,,;;]+/)
|
||||
.map((entry) => String(entry || '').trim())
|
||||
.filter(Boolean);
|
||||
const normalized = [];
|
||||
const seen = new Set();
|
||||
|
||||
const pushProvider = (entry) => {
|
||||
const provider = normalizeProviderId(entry, DEFAULT_PROVIDER);
|
||||
if (!provider || seen.has(provider)) {
|
||||
return;
|
||||
}
|
||||
seen.add(provider);
|
||||
normalized.push(provider);
|
||||
};
|
||||
|
||||
source.forEach((entry) => {
|
||||
pushProvider(entry && typeof entry === 'object' && !Array.isArray(entry)
|
||||
? (entry.provider || entry.id || entry.value || '')
|
||||
: entry);
|
||||
});
|
||||
|
||||
if (normalized.length) {
|
||||
return normalized.slice(0, DEFAULT_PROVIDER_ORDER.length);
|
||||
}
|
||||
|
||||
const fallback = Array.isArray(fallbackOrder) ? fallbackOrder : [];
|
||||
fallback.forEach((entry) => {
|
||||
pushProvider(entry && typeof entry === 'object' && !Array.isArray(entry)
|
||||
? (entry.provider || entry.id || entry.value || '')
|
||||
: entry);
|
||||
});
|
||||
|
||||
return normalized.slice(0, DEFAULT_PROVIDER_ORDER.length);
|
||||
}
|
||||
|
||||
function getProviderIds() {
|
||||
return DEFAULT_PROVIDER_ORDER.slice();
|
||||
}
|
||||
|
||||
function getProviderDefinition(providerId = DEFAULT_PROVIDER) {
|
||||
return PROVIDER_DEFINITIONS[normalizeProviderId(providerId)] || PROVIDER_DEFINITIONS[DEFAULT_PROVIDER];
|
||||
}
|
||||
|
||||
function getProviderModule(providerId = DEFAULT_PROVIDER) {
|
||||
const definition = getProviderDefinition(providerId);
|
||||
const moduleKey = definition?.moduleKey;
|
||||
if (!moduleKey) {
|
||||
return null;
|
||||
}
|
||||
return root?.[moduleKey] || null;
|
||||
}
|
||||
|
||||
function createProvider(providerId = DEFAULT_PROVIDER, deps = {}) {
|
||||
const definition = getProviderDefinition(providerId);
|
||||
const module = getProviderModule(providerId);
|
||||
if (!module || typeof module.createProvider !== 'function') {
|
||||
throw new Error(`接码平台模块未加载:${definition.id}`);
|
||||
}
|
||||
return module.createProvider(deps);
|
||||
}
|
||||
|
||||
function getProviderLabel(providerId = DEFAULT_PROVIDER) {
|
||||
return getProviderDefinition(providerId)?.label || 'HeroSMS';
|
||||
}
|
||||
|
||||
return {
|
||||
PROVIDER_HERO_SMS,
|
||||
PROVIDER_FIVE_SIM,
|
||||
PROVIDER_NEXSMS,
|
||||
DEFAULT_PROVIDER,
|
||||
DEFAULT_PROVIDER_ORDER,
|
||||
PROVIDER_DEFINITIONS,
|
||||
normalizeProviderId,
|
||||
normalizeProviderOrder,
|
||||
getProviderIds,
|
||||
getProviderDefinition,
|
||||
getProviderModule,
|
||||
createProvider,
|
||||
getProviderLabel,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user