// phone-sms/providers/sms-bower.js — SMS Bower 接码平台适配层 (function attachSmsBowerProvider(root, factory) { root.PhoneSmsBowerProvider = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createSmsBowerProviderModule() { const PROVIDER_ID = 'sms-bower'; const DEFAULT_BASE_URL = 'https://smsbower.page/stubs/handler_api.php'; const DEFAULT_SERVICE_CODE = 'dr'; const DEFAULT_SERVICE_LABEL = 'OpenAI'; const DEFAULT_COUNTRY_ID = 6; const DEFAULT_COUNTRY_LABEL = 'Indonesia'; const DEFAULT_REQUEST_TIMEOUT_MS = 20000; const DEFAULT_POLL_TIMEOUT_MS = 180000; const DEFAULT_POLL_INTERVAL_MS = 5000; const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::'; const MAX_PRICE_CANDIDATES = 8; function normalizeText(value = '', fallback = '') { return String(value || '').trim() || fallback; } function normalizeSmsBowerCountryId(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 normalizeSmsBowerCountryLabel(value = '', fallback = DEFAULT_COUNTRY_LABEL) { return normalizeText(value, fallback); } function normalizeCountryKey(value) { const countryId = normalizeSmsBowerCountryId(value, -1); return countryId >= 0 ? String(countryId) : ''; } function normalizeSmsBowerServiceCode(value = '', fallback = DEFAULT_SERVICE_CODE) { const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); if (normalized) return normalized; const fallbackNormalized = String(fallback || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); return fallbackNormalized || DEFAULT_SERVICE_CODE; } function normalizePriceLimit(value) { if (value === undefined || value === null || String(value).trim() === '') return null; const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) return null; return Math.round(parsed * 10000) / 10000; } function normalizeSmsBowerMaxPrice(value = '') { const normalized = normalizePriceLimit(value); return normalized === null ? '' : String(normalized); } function normalizeCsvIds(value = '') { const source = Array.isArray(value) ? value : String(value || '').split(/[\r\n,,;;]+/); const seen = new Set(); return source .map((entry) => String(entry || '').trim().replace(/[^0-9a-zA-Z_-]+/g, '')) .filter((entry) => { if (!entry || seen.has(entry)) return false; seen.add(entry); return true; }) .join(','); } function normalizeCountryOrder(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 = []; source.forEach((entry) => { let id = -1; let label = ''; if (entry && typeof entry === 'object' && !Array.isArray(entry)) { id = normalizeSmsBowerCountryId(entry.id ?? entry.countryId, -1); label = normalizeText(entry.label ?? entry.countryLabel, ''); } else { const text = String(entry || '').trim(); const structured = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/); id = normalizeSmsBowerCountryId(structured?.[1] || text, -1); label = normalizeText(structured?.[2], ''); } if (id < 0 || seen.has(id)) return; seen.add(id); normalized.push({ id, label: label || `Country #${id}` }); }); return normalized.slice(0, 20); } function normalizeBaseUrl(value = '') { const trimmed = String(value || '').trim() || DEFAULT_BASE_URL; try { const parsed = new URL(trimmed); return parsed.toString(); } catch { return DEFAULT_BASE_URL; } } 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 = normalizeText(raw.message || raw.msg || raw.error || raw.title || raw.status, ''); if (direct) return direct; try { return JSON.stringify(raw); } catch { return String(raw); } } return String(raw || '').trim(); } function resolveConfig(state = {}, deps = {}) { return { apiKey: normalizeText(state.smsBowerApiKey), baseUrl: normalizeBaseUrl(state.smsBowerBaseUrl || DEFAULT_BASE_URL), serviceCode: normalizeSmsBowerServiceCode(state.smsBowerServiceCode, DEFAULT_SERVICE_CODE), fetchImpl: deps.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null), requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS, }; } function buildUrl(config = {}, query = {}) { const url = new URL(config.baseUrl || DEFAULT_BASE_URL); url.searchParams.set('api_key', config.apiKey); Object.entries(query || {}).forEach(([key, value]) => { if (value === undefined || value === null || value === '') return; url.searchParams.set(key, String(value)); }); return url.toString(); } async function fetchPayload(config = {}, query = {}, actionLabel = 'SMS Bower 请求') { if (!config.fetchImpl) throw new Error('SMS Bower 网络请求实现不可用。'); if (!config.apiKey) throw new Error('SMS Bower API Key 缺失,请先在侧边栏保存接码 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, query), { method: 'GET', signal: controller?.signal, headers: { Accept: 'application/json, text/plain, */*' }, }); 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; } const message = describePayload(payload); const apiStatus = payload && typeof payload === 'object' && !Array.isArray(payload) ? Number(payload.status) : null; if (apiStatus === 0 || /^(BAD_KEY|BAD_ACTION|BAD_SERVICE|BAD_STATUS|NO_ACTIVATION|EARLY_CANCEL_DENIED|BAD_COUNTRY)\b/i.test(message)) { const error = new Error(`${actionLabel}失败:${message || 'SMS Bower API 返回失败状态'}`); error.payload = payload; throw error; } return payload; } catch (error) { if (error?.name === 'AbortError') throw new Error(`${actionLabel}超时。`); throw error; } finally { if (timeoutId) clearTimeout(timeoutId); } } function resolveCountryCandidates(state = {}) { const candidates = normalizeCountryOrder(state.smsBowerCountryOrder); if (candidates.length) return candidates; return [{ id: normalizeSmsBowerCountryId(state.smsBowerCountryId, DEFAULT_COUNTRY_ID), label: normalizeSmsBowerCountryLabel(state.smsBowerCountryLabel, DEFAULT_COUNTRY_LABEL), }]; } function resolveCountryLabel(state = {}, countryId = DEFAULT_COUNTRY_ID) { const key = normalizeCountryKey(countryId); const matched = resolveCountryCandidates(state).find((entry) => normalizeCountryKey(entry.id) === key); return matched?.label || (key ? `Country #${key}` : DEFAULT_COUNTRY_LABEL); } function resolvePriceRange(state = {}) { const minPriceLimit = normalizePriceLimit(state.smsBowerMinPrice); const maxPriceLimit = normalizePriceLimit(state.smsBowerMaxPrice); return { minPriceLimit, maxPriceLimit, hasMinPriceLimit: minPriceLimit !== null, hasMaxPriceLimit: maxPriceLimit !== null, invalidRange: minPriceLimit !== null && maxPriceLimit !== null && minPriceLimit > maxPriceLimit, }; } function formatPriceRangeText(minPriceLimit = null, maxPriceLimit = null) { const minPrice = normalizePriceLimit(minPriceLimit); const maxPrice = normalizePriceLimit(maxPriceLimit); if (minPrice !== null && maxPrice !== null) return `${minPrice}~${maxPrice}`; if (minPrice !== null) return `${minPrice}~`; if (maxPrice !== null) return `~${maxPrice}`; return 'unbounded'; } async function fetchBalance(state = {}, deps = {}) { const config = resolveConfig(state, deps); const payload = await fetchPayload(config, { action: 'getBalance' }, 'SMS Bower 查询余额'); const matched = describePayload(payload).match(/ACCESS_BALANCE\s*:\s*([\d.]+)/i); return { balance: matched ? Number(matched[1]) : null, raw: payload }; } async function fetchPrices(state = {}, countryConfig = {}, deps = {}) { const config = resolveConfig(state, deps); return fetchPayload(config, { action: 'getPricesV3', service: config.serviceCode, country: normalizeSmsBowerCountryId(countryConfig?.id ?? state.smsBowerCountryId, DEFAULT_COUNTRY_ID), }, 'SMS Bower 查询价格'); } function collectPriceEntries(payload, entries = []) { if (Array.isArray(payload)) { payload.forEach((entry) => collectPriceEntries(entry, entries)); return entries; } if (!payload || typeof payload !== 'object') return entries; const directCount = Number(payload.count ?? payload.qty ?? payload.Qty); const directPrice = Number(payload.cost ?? payload.price ?? payload.Price); if (Number.isFinite(directPrice) && directPrice >= 0) { entries.push({ cost: directPrice, count: Number.isFinite(directCount) ? directCount : 0, inStock: !Number.isFinite(directCount) || directCount > 0 }); } Object.entries(payload).forEach(([key, entry]) => { if (entry && typeof entry === 'object' && !Array.isArray(entry)) { const keyedPrice = Number(key); const keyedCount = Number(entry.count ?? entry.qty ?? entry.Qty); if (/^\d+(?:\.\d+)?$/.test(String(key)) && String(key).includes('.') && Number.isFinite(keyedPrice) && keyedPrice >= 0 && !Number.isFinite(Number(entry.cost ?? entry.price ?? entry.Price))) { entries.push({ cost: keyedPrice, count: Number.isFinite(keyedCount) ? keyedCount : 0, inStock: !Number.isFinite(keyedCount) || keyedCount > 0 }); } } collectPriceEntries(entry, entries); }); return entries; } function normalizeActivation(payload, fallback = {}) { if (typeof payload === 'string') { const matched = payload.match(/^ACCESS_NUMBER\s*:\s*([^:]+)\s*:\s*(.+)$/i); if (matched) { payload = { activationId: matched[1], phoneNumber: matched[2] }; } } const source = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {}; const activationId = normalizeText(source.activationId ?? source.activation_id ?? source.id ?? fallback.activationId); const phoneNumber = normalizeText(source.phoneNumber ?? source.phone_number ?? source.phone ?? fallback.phoneNumber); if (!activationId || !phoneNumber) return null; const countryId = normalizeSmsBowerCountryId(source.countryId ?? source.country_id ?? source.countryCode ?? source.country ?? fallback.countryId, DEFAULT_COUNTRY_ID); return { activationId, phoneNumber, provider: PROVIDER_ID, serviceCode: normalizeSmsBowerServiceCode(source.serviceCode || fallback.serviceCode || DEFAULT_SERVICE_CODE), countryId, countryCode: countryId, countryLabel: normalizeSmsBowerCountryLabel(source.countryLabel || fallback.countryLabel, `Country #${countryId}`), successfulUses: Math.max(0, Math.floor(Number(source.successfulUses ?? fallback.successfulUses) || 0)), maxUses: 1, }; } function resolveActivationCountry(activation = {}, state = {}) { const normalized = normalizeActivation(activation) || (activation && typeof activation === 'object' ? activation : {}); const id = normalizeSmsBowerCountryId(normalized.countryId ?? normalized.countryCode ?? normalized.country, DEFAULT_COUNTRY_ID); return { id, label: resolveCountryLabel(state, id) }; } function getActivationCountryKey(activation = {}) { return normalizeCountryKey(activation?.countryCode ?? activation?.countryId ?? activation?.country); } function getActivationPrice(activation = {}) { const price = Number(activation?.selectedPrice ?? activation?.price ?? activation?.maxPrice); return Number.isFinite(price) && price >= 0 ? price : null; } function buildAcquireQuery(state = {}, config = resolveConfig(state), countryConfig = {}) { const range = resolvePriceRange(state); if (range.invalidRange) { throw new Error(`SMS Bower 价格区间无效:最低购买价 ${range.minPriceLimit} 高于价格上限 ${range.maxPriceLimit}。`); } return { action: 'getNumberV2', service: config.serviceCode, country: normalizeSmsBowerCountryId(countryConfig.id, DEFAULT_COUNTRY_ID), maxPrice: range.maxPriceLimit, minPrice: range.minPriceLimit, }; } async function requestActivation(state = {}, options = {}, deps = {}) { const config = resolveConfig(state, deps); const blocked = new Set((Array.isArray(options.blockedCountryIds) ? options.blockedCountryIds : []).map((v) => normalizeCountryKey(v)).filter(Boolean)); const candidates = resolveCountryCandidates(state).filter((entry) => !blocked.has(normalizeCountryKey(entry.id))); const countryList = candidates.length ? candidates : resolveCountryCandidates(state); const errors = []; for (const countryConfig of countryList) { try { const payload = await fetchPayload(config, buildAcquireQuery(state, config, countryConfig), `SMS Bower 购买手机号(${countryConfig.label || countryConfig.id})`); const activation = normalizeActivation(payload, { countryId: countryConfig.id, countryLabel: countryConfig.label, serviceCode: config.serviceCode, }); if (activation) return activation; throw new Error(`SMS Bower 购买手机号返回不可用响应:${describePayload(payload) || '空响应'}`); } catch (error) { errors.push(error.message || String(error)); } } throw new Error(`SMS Bower 购买手机号失败:${errors.join(' | ')}`); } async function setActivationStatus(state = {}, activation, status, actionLabel, deps = {}) { const normalized = normalizeActivation(activation); if (!normalized) return ''; const config = resolveConfig(state, deps); const payload = await fetchPayload(config, { action: 'setStatus', status, id: normalized.activationId, }, actionLabel); return describePayload(payload); } async function finishActivation(state = {}, activation, deps = {}) { return setActivationStatus(state, activation, 6, 'SMS Bower 完成订单', deps); } async function cancelActivation(state = {}, activation, deps = {}) { return setActivationStatus(state, activation, 8, 'SMS Bower 取消订单', deps); } async function banActivation(state = {}, activation, deps = {}) { return cancelActivation(state, activation, deps); } async function requestAdditionalSms(state = {}, activation, deps = {}) { return setActivationStatus(state, activation, 3, 'SMS Bower 请求下一条短信', deps); } async function reuseActivation(state = {}, activation, deps = {}) { await requestAdditionalSms(state, activation, deps); return normalizeActivation(activation); } async function rotateActivation(state = {}, activation, options = {}, deps = {}) { await (String(options?.releaseAction || '').trim().toLowerCase() === 'ban' ? banActivation(state, activation, deps) : cancelActivation(state, activation, deps)); const nextActivation = await requestActivation(state, { ...options, skipPreferredActivation: true, }, deps); return { currentTicketId: String(activation?.activationId || activation?.id || ''), nextActivation }; } function extractVerificationCode(raw = '') { const trimmed = String(raw || '').trim(); if (!trimmed) return ''; const matched = trimmed.match(/\b(\d{4,8})\b/); return matched?.[1] || trimmed.replace(/^STATUS_OK\s*:\s*/i, '').trim(); } async function pollActivationCode(state = {}, activation, options = {}, deps = {}) { const normalized = normalizeActivation(activation); if (!normalized) throw new Error('缺少手机号接码订单。'); const config = resolveConfig(state, deps); const timeoutMs = Math.max(1000, Number(options.timeoutMs) || DEFAULT_POLL_TIMEOUT_MS); const intervalMs = Math.max(1000, Number(options.intervalMs) || DEFAULT_POLL_INTERVAL_MS); 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 fetchPayload(config, { action: 'getStatus', id: normalized.activationId }, 'SMS Bower 查询验证码'); pollCount += 1; lastResponse = describePayload(payload); if (typeof options.onStatus === 'function') { await options.onStatus({ activation: normalized, elapsedMs: Date.now() - start, pollCount, statusText: lastResponse, timeoutMs }); } if (/^STATUS_OK\s*:/i.test(lastResponse)) return extractVerificationCode(lastResponse); if (/^STATUS_CANCEL\b/i.test(lastResponse)) throw new Error('SMS Bower 查询验证码失败:订单已取消'); if (typeof options.onWaitingForCode === 'function') { await options.onWaitingForCode({ activation: normalized, elapsedMs: Date.now() - start, pollCount, statusText: lastResponse, timeoutMs }); } await deps.sleepWithStop?.(intervalMs); } throw new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}等待手机验证码超时。${lastResponse ? ` SMS Bower 最后状态:${lastResponse}` : ''}`); } function createProvider(deps = {}) { const providerDeps = { fetchImpl: deps.fetchImpl, sleepWithStop: deps.sleepWithStop, throwIfStopped: deps.throwIfStopped, requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS, }; const capabilities = Object.freeze({ supportsReusableActivation: true, supportsAutomaticFreeReuse: true, supportsFreeReusePreservation: false, supportsPageResend: false, supportsPageResendProbe: true, requiresCountrySelection: true, }); return { id: PROVIDER_ID, label: 'SMS Bower', capabilities, defaultCountryId: DEFAULT_COUNTRY_ID, defaultCountryLabel: DEFAULT_COUNTRY_LABEL, defaultProduct: DEFAULT_SERVICE_LABEL, defaultServiceCode: DEFAULT_SERVICE_CODE, normalizeCountryId: normalizeSmsBowerCountryId, normalizeCountryLabel: normalizeSmsBowerCountryLabel, normalizeCountryKey, normalizeServiceCode: normalizeSmsBowerServiceCode, normalizeMaxPrice: normalizeSmsBowerMaxPrice, normalizeCountryFallback: normalizeCountryOrder, normalizeActivation, resolveCountryCandidates, resolveCountryLabel, resolveActivationCountry, getActivationCountryKey, getActivationPrice, 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), requestAdditionalSms: (state, activation) => requestAdditionalSms(state, activation, providerDeps), rotateActivation: (state, activation, options) => rotateActivation(state, activation, options, providerDeps), pollActivationCode: (state, activation, options) => pollActivationCode(state, activation, options, providerDeps), prepareActivationForReuse: async (_state, activation) => ({ activation: normalizeActivation(activation), ignoredPhoneCodeKeys: [] }), canPersistReusableActivation: () => false, canPreserveActivationForFreeReuse: () => false, shouldUsePageResend: () => false, shouldProbePageResend: () => true, fetchBalance: (state) => fetchBalance(state, providerDeps), fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps), resolvePriceRange, formatPriceRangeText, collectPriceEntries, describePayload, }; } return { PROVIDER_ID, DEFAULT_COUNTRY_ID, DEFAULT_COUNTRY_LABEL, DEFAULT_SERVICE_CODE, createProvider, normalizeSmsBowerCountryId, normalizeSmsBowerCountryLabel, normalizeCountryKey, normalizeSmsBowerServiceCode, normalizeSmsBowerMaxPrice, normalizeActivation, }; });