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