import GuJumpgate snapshot from GitHub

This commit is contained in:
2026-05-20 20:33:51 +08:00
commit f8903dcd14
335 changed files with 179790 additions and 0 deletions
+920
View File
@@ -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,
};
});
+213
View File
@@ -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,
};
});
+139
View File
@@ -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,
};
});