// 接码 / SMS 验证码服务 —— smsbower.app API 客户端(零依赖,使用全局 fetch) // 协议:经典 sms-activate 风格,所有请求为 GET https://smsbower.page/stubs/handler_api.php?api_key=..&action=.. // 响应为纯文本(冒号分隔),如 ACCESS_NUMBER::、STATUS_OK: // // 与 smscode.js 接口对齐(可作为调度器的替换供应商): // acquireNumber(cfg, { log }) → { orderId, phone, order } // pollOtp(cfg, id, { timeoutMs, intervalMs, log }) → { code, message, order } // finishOrder(cfg, id) → setStatus status=6(完成) // cancelOrder(cfg, id) → setStatus status=8(取消,2 分钟内不可取消时静默处理) // getBalance(cfg) → 余额数字 // // 配置字段:apiKey、baseUrl(默认 .page 地址)、country(默认 22=印度)、 // service(默认 "dr"=OpenAI/ChatGPT)、maxPrice、pollTimeout const DEFAULT_BASE = 'https://smsbower.page/stubs/handler_api.php'; const DEFAULT_COUNTRY = 22; // 印度 const DEFAULT_SERVICE = 'dr'; // OpenAI / ChatGPT function normBase(baseUrl) { return String(baseUrl || DEFAULT_BASE).trim().replace(/\/+$/, '') || DEFAULT_BASE; } // 统一请求:拼接 api_key + action + 参数,返回纯文本响应(已 trim) async function request(cfg, action, params = {}, { timeoutMs = 30000 } = {}) { const apiKey = String((cfg && cfg.apiKey) || '').trim(); if (!apiKey) { const e = new Error('未配置 smsbower API Key(系统设置 → 接码配置)'); e.code = 'NO_API_KEY'; throw e; } const q = new URLSearchParams({ api_key: apiKey, action }); for (const [k, v] of Object.entries(params)) { if (v !== undefined && v !== null && v !== '') q.set(k, String(v)); } const url = `${normBase(cfg && cfg.baseUrl)}?${q.toString()}`; const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), timeoutMs); let res; try { res = await fetch(url, { method: 'GET', signal: ctrl.signal }); } catch (err) { clearTimeout(timer); const e = new Error(err.name === 'AbortError' ? `接码请求超时(action=${action})` : `接码请求失败:${err.message}`); e.code = 'NETWORK'; throw e; } clearTimeout(timer); const text = String(await res.text().catch(() => '')).trim(); if (!res.ok) { const e = new Error(`接码接口错误:HTTP_${res.status}${text ? ' · ' + text.slice(0, 200) : ''}`); e.code = `HTTP_${res.status}`; e.status = res.status; throw e; } return text; } // 通用错误码 → 带 code 的 Error(BAD_KEY / BAD_ACTION / BAD_SERVICE / NO_ACTIVATION 等) function apiError(token, action) { const map = { BAD_KEY: 'API Key 无效', BAD_ACTION: '接口 action 无效', BAD_SERVICE: '服务代码无效', NO_ACTIVATION: '激活订单不存在', NO_BALANCE: '余额不足', NO_NUMBERS: '当前无可用号码', }; const e = new Error(`接码接口错误:${token}${map[token] ? ' · ' + map[token] : ''}(action=${action})`); e.code = token; return e; } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } // ---------- 余额 ---------- // 返回余额数字。响应:ACCESS_BALANCE: export async function getBalance(cfg) { const text = await request(cfg, 'getBalance'); const m = /^ACCESS_BALANCE:(.+)$/.exec(text); if (m) { const n = Number(m[1]); if (Number.isFinite(n)) return n; } throw apiError(text || 'EMPTY_RESPONSE', 'getBalance'); } // ---------- 拿号 ---------- // 返回 { orderId, phone, order },与 smscode.acquireNumber 一致(orderId = activationId)。 // NO_NUMBERS(无库存,可回退重试)→ e.code='NO_OFFER_AVAILABLE'(与 smscode 的库存错误标记一致), // 并附 e.providerCode='NO_NUMBERS';NO_BALANCE / BAD_KEY / BAD_SERVICE 为硬错误,直接抛出。 export async function acquireNumber(cfg, { log } = {}) { const say = (level, msg) => { if (typeof log === 'function') log(level, msg); }; const country = cfg.country !== undefined && cfg.country !== null && cfg.country !== '' ? cfg.country : DEFAULT_COUNTRY; const service = String(cfg.service || DEFAULT_SERVICE); const params = { service, country }; if (cfg.maxPrice) params.maxPrice = cfg.maxPrice; if (cfg.minPrice) params.minPrice = cfg.minPrice; say('info', `向 smsbower 请求号码(service=${service} country=${country}${cfg.maxPrice ? ' maxPrice=' + cfg.maxPrice : ''})…`); const text = await request(cfg, 'getNumber', params); const m = /^ACCESS_NUMBER:([^:]+):(.+)$/.exec(text); if (m) { const orderId = m[1]; const phone = m[2]; const order = { id: orderId, phone_number: phone, status: 'PENDING', raw: text }; say('ok', `已获取号码:${phone}(订单 ${orderId})`); return { orderId, phone, order }; } if (text === 'NO_NUMBERS' || text.startsWith('NO_NUMBERS')) { // 无库存:标记为可回退的库存错误(与 smscode 的 isStockError 判定一致) const e = new Error(`当前无可用号码(service=${service} country=${country})`); e.code = 'NO_OFFER_AVAILABLE'; e.providerCode = 'NO_NUMBERS'; throw e; } throw apiError(text || 'EMPTY_RESPONSE', 'getNumber'); } // ---------- 查询状态 ---------- // 返回 { status, code }。status ∈ WAIT_CODE / WAIT_RETRY / CANCEL / OK export async function getStatus(cfg, id) { const text = await request(cfg, 'getStatus', { id }); if (text === 'STATUS_WAIT_CODE') return { status: 'WAIT_CODE', code: '' }; let m = /^STATUS_WAIT_RETRY:(.*)$/.exec(text); if (m) return { status: 'WAIT_RETRY', code: m[1] }; if (text === 'STATUS_CANCEL') return { status: 'CANCEL', code: '' }; m = /^STATUS_OK:(.+)$/.exec(text); if (m) return { status: 'OK', code: m[1] }; throw apiError(text || 'EMPTY_RESPONSE', 'getStatus'); } // ---------- 轮询等待验证码 ---------- // 直到 STATUS_OK 拿到 code,或订单取消/超时抛错。返回 { code, message, order },与 smscode.pollOtp 一致。 export async function pollOtp(cfg, id, { timeoutMs = 180000, intervalMs = 4000, log } = {}) { const start = Date.now(); const say = (level, msg) => { if (typeof log === 'function') log(level, msg); }; while (Date.now() - start < timeoutMs) { let st; try { st = await getStatus(cfg, id); } catch (e) { if (e.code === 'NO_ACTIVATION' || e.code === 'BAD_KEY') throw e; // 不可恢复 say('warn', `查询订单失败(将重试):${e.message}`); await sleep(intervalMs); continue; } if (st.status === 'OK' && st.code) { say('ok', `已收到验证码:${st.code}`); const order = { id, status: 'OTP_RECEIVED', otp_code: st.code }; return { code: st.code, message: '', order }; } if (st.status === 'CANCEL') { const e = new Error('订单已取消,未收到验证码'); e.code = 'CANCELED'; throw e; } // WAIT_CODE / WAIT_RETRY:继续等待 say('info', `等待验证码…(订单 ${id} 状态 ${st.status}${st.status === 'WAIT_RETRY' && st.code ? ',上一条验证码 ' + st.code : ''})`); await sleep(intervalMs); } const e = new Error(`等待验证码超时(${Math.round(timeoutMs / 1000)}s)`); e.code = 'OTP_TIMEOUT'; throw e; } // ---------- 状态变更 ---------- // setStatus:1=已就绪 3=请求下一条验证码 6=完成 8=取消 async function setStatus(cfg, id, status) { return request(cfg, 'setStatus', { id, status }); } // 完成订单(收到验证码后调用,setStatus=6)。响应:ACCESS_ACTIVATION export async function finishOrder(cfg, id) { const text = await setStatus(cfg, id, 6); if (text === 'ACCESS_ACTIVATION') return { id, status: 'FINISHED' }; throw apiError(text || 'EMPTY_RESPONSE', 'setStatus(6)'); } // 取消订单(setStatus=8)。响应:ACCESS_CANCEL。 // EARLY_CANCEL_DENIED(购买 2 分钟内不可取消)按非致命处理:记 warn 后正常返回,号码到期自动退款。 export async function cancelOrder(cfg, id, { log } = {}) { const say = (level, msg) => { if (typeof log === 'function') log(level, msg); }; const text = await setStatus(cfg, id, 8); if (text === 'ACCESS_CANCEL') return { id, status: 'CANCELED' }; if (text === 'EARLY_CANCEL_DENIED') { say('warn', `订单 ${id} 购买未满 2 分钟,暂不可取消(到期后将自动退款)`); return { id, status: 'CANCEL_PENDING', earlyCancelDenied: true }; } throw apiError(text || 'EMPTY_RESPONSE', 'setStatus(8)'); } // 请求下一条验证码(setStatus=3),对应 smscode.resendOrder。响应:ACCESS_RETRY_GET export async function resendOrder(cfg, id) { const text = await setStatus(cfg, id, 3); if (text === 'ACCESS_RETRY_GET') return { id, status: 'WAIT_RETRY' }; throw apiError(text || 'EMPTY_RESPONSE', 'setStatus(3)'); }