// 接码 / SMS 验证码服务 —— smscode.gg API 客户端(零依赖,使用全局 fetch) // 文档:https://smscode.gg/docs Base URL: https://api.smscode.gg/v1 // 鉴权:Authorization: Bearer // 统一响应包络:{ success:true, data:{...} } / { success:false, error:{ code, message } } // // 订单生命周期: // 1. createOrder → 下单拿号(product_id 精确档位 或 catalog_product_id 路由) // 2. pollOtp → 轮询 GET /orders/{id} 直到 status=OTP_RECEIVED 拿到 otp_code // 3. finishOrder → 收到验证码后标记完成(立即释放号码) // 或 cancelOrder → 未收到验证码时取消(退款) const DEFAULT_BASE = 'https://api.smscode.gg/v1'; function normBase(baseUrl) { return String(baseUrl || DEFAULT_BASE).trim().replace(/\/+$/, '') || DEFAULT_BASE; } // 统一请求:注入 Bearer、解析包络、处理 429/超时/HTTP 错误 async function request(cfg, method, pathAndQuery, { body, idempotencyKey, timeoutMs = 30000 } = {}) { const base = normBase(cfg && cfg.baseUrl); const apiKey = String((cfg && cfg.apiKey) || '').trim(); if (!apiKey) { const e = new Error('未配置接码 API Key(系统设置 → 接码配置)'); e.code = 'NO_API_KEY'; throw e; } const url = `${base}${pathAndQuery}`; const headers = { authorization: `Bearer ${apiKey}`, accept: 'application/json', }; if (body !== undefined) headers['content-type'] = 'application/json'; if (idempotencyKey) headers['idempotency-key'] = idempotencyKey; const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), timeoutMs); let res; try { res = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal: ctrl.signal, }); } catch (err) { clearTimeout(timer); const e = new Error(err.name === 'AbortError' ? `接码请求超时(${method} ${pathAndQuery})` : `接码请求失败:${err.message}`); e.code = 'NETWORK'; throw e; } clearTimeout(timer); if (res.status === 429) { const retry = Number(res.headers.get('retry-after')) || 5; const e = new Error(`接码接口限流(429),请 ${retry}s 后重试`); e.code = 'RATE_LIMIT_EXCEEDED'; e.retryAfter = retry; throw e; } const data = await res.json().catch(() => ({})); if (!res.ok || data.success === false) { const err = (data && data.error) || {}; const e = new Error(`接码接口错误:${err.code || res.status}${err.message ? ' · ' + err.message : ''}`); e.code = err.code || `HTTP_${res.status}`; e.status = res.status; throw e; } return data.data; } // ---------- 目录(catalog) ---------- export async function listCountries(cfg) { return request(cfg, 'GET', '/catalog/countries'); } export async function listServices(cfg, countryId) { const q = countryId ? `?country_id=${encodeURIComponent(countryId)}` : ''; return request(cfg, 'GET', `/catalog/services${q}`); } export async function listProducts(cfg, { countryId, platformId, operatorId, limit = 50, page = 1 } = {}) { const p = new URLSearchParams(); if (countryId) p.set('country_id', countryId); if (platformId) p.set('platform_id', platformId); if (operatorId) p.set('operator_id', operatorId); p.set('limit', limit); p.set('page', page); return request(cfg, 'GET', `/catalog/products?${p.toString()}`); } export async function getBalance(cfg) { return request(cfg, 'GET', '/balance'); } // ---------- 订单 ---------- // 下单拿号。优先使用 productId(精确档位),否则用 catalogProductId(路由下单)。 export async function createOrder(cfg, { productId, catalogProductId, operatorId, maxPrice, quantity = 1 } = {}) { const body = { quantity }; if (productId) body.product_id = Number(productId); else if (catalogProductId) { body.catalog_product_id = Number(catalogProductId); if (operatorId) body.operator_id = Number(operatorId); if (maxPrice) body.max_price = /^\d+$/.test(String(maxPrice)) ? Number(maxPrice) : String(maxPrice); } else { const e = new Error('下单需提供 productId 或 catalogProductId'); e.code = 'VALIDATION_ERROR'; throw e; } const idempotencyKey = `getrt-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; const data = await request(cfg, 'POST', '/orders/create', { body, idempotencyKey }); const order = (data.orders && data.orders[0]) || data; if (!order || !order.id) { const e = new Error('下单未返回有效订单'); e.code = 'NO_ORDER'; throw e; } return order; // { id, status, phone_number, ... } } export async function getOrder(cfg, id) { return request(cfg, 'GET', `/orders/${encodeURIComponent(id)}`); } export async function finishOrder(cfg, id) { return request(cfg, 'POST', '/orders/finish', { body: { id: Number(id) } }); } export async function cancelOrder(cfg, id) { return request(cfg, 'POST', '/orders/cancel', { body: { id: Number(id) } }); } export async function resendOrder(cfg, id) { return request(cfg, 'POST', '/orders/resend', { body: { id: Number(id) } }); } // 轮询等待验证码:直到 status=OTP_RECEIVED 且拿到 otp_code,或超时。 // log(level,msg) 可选,用于向上层推送进度。 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 order; try { order = await getOrder(cfg, id); } catch (e) { say('warn', `查询订单失败(将重试):${e.message}`); await sleep(intervalMs); continue; } const status = String(order.status || '').toUpperCase(); if (order.otp_code) { say('ok', `已收到验证码:${order.otp_code}`); return { code: order.otp_code, message: order.otp_message || '', order }; } if (status === 'CANCELED' || status === 'EXPIRED') { const e = new Error(`订单已${status === 'CANCELED' ? '取消' : '过期'},未收到验证码`); e.code = status; throw e; } say('info', `等待验证码…(订单 ${id} 状态 ${status || '未知'})`); await sleep(intervalMs); } const e = new Error(`等待验证码超时(${Math.round(timeoutMs / 1000)}s)`); e.code = 'OTP_TIMEOUT'; throw e; } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } // 一站式:拿号 → 轮询验证码。返回 { orderId, phone, code }。 // 判断下单失败是否属于“无库存/无号码”类(可回退下一档位),而非鉴权/余额类(应立即抛出)。 function isStockError(e) { const code = String((e && e.code) || '').toUpperCase(); if (['NO_OFFER_AVAILABLE', 'PROVIDER_ERROR', 'NO_ORDER'].includes(code)) return true; const msg = String((e && e.message) || '').toLowerCase(); if ((e && e.status === 422) && /(no[_\s-]?number|no[_\s-]?offer|out.?of.?stock|no.?stock|stock|available|sold.?out)/.test(msg)) return true; return false; } // 拉取 country+service 的产品档位,构造“有货 + 按价格升序”的候选下单顺序。 // 若配置了 productId 且该档位有货,则置于队首。返回 [{ productId, price, available }]。 export async function buildCandidates(cfg, { log } = {}) { const say = (level, msg) => { if (typeof log === 'function') log(level, msg); }; const products = await listProducts(cfg, { countryId: cfg.countryId, platformId: cfg.serviceId, limit: 10000, page: 1 }); const list = Array.isArray(products) ? products : []; const inStock = list .filter((p) => p && p.active !== false && (Number(p.available) || 0) > 0) .map((p) => ({ productId: p.id, price: Number(p.price) || 0, available: Number(p.available) || 0 })) .sort((a, b) => a.price - b.price); const configured = Number(cfg.productId) || 0; let ordered = inStock; if (configured) { const hit = inStock.find((c) => Number(c.productId) === configured); if (hit) ordered = [hit, ...inStock.filter((c) => Number(c.productId) !== configured)]; else say('warn', `配置的 productId=${configured} 当前无货,改用有货档位(按价格升序)`); } say('info', `候选档位(有货,共 ${ordered.length}):${ordered.slice(0, 8).map((c) => `#${c.productId}@${c.price}(${c.available})`).join(' → ')}${ordered.length > 8 ? ' …' : ''}`); return ordered; } // 拿号(带无货自动回退)。返回 { orderId, phone, order },与旧签名一致。 // 调用方应在使用完验证码后调用 finishOrder;失败时调用 cancelOrder(若 can_cancel)。 export async function acquireNumber(cfg, { log } = {}) { const say = (level, msg) => { if (typeof log === 'function') log(level, msg); }; // 未配置 country+service:维持旧行为(单一 productId / catalogProductId)。 if (!cfg.countryId || !cfg.serviceId) { const order = await createOrder(cfg, { productId: cfg.productId, catalogProductId: cfg.catalogProductId, operatorId: cfg.operatorId, maxPrice: cfg.maxPrice, }); say('ok', `已获取号码:${order.phone_number}(订单 ${order.id})`); return { orderId: order.id, phone: order.phone_number, order }; } const candidates = await buildCandidates(cfg, { log }); if (!candidates.length) { const e = new Error(`所有档位均无库存/下单失败(country=${cfg.countryId} service=${cfg.serviceId})`); e.code = 'NO_OFFER_AVAILABLE'; throw e; } for (let i = 0; i < candidates.length; i++) { const c = candidates[i]; try { say('info', `尝试下单档位 #${c.productId}(价格 ${c.price} IDR,库存 ${c.available})`); const order = await createOrder(cfg, { productId: c.productId }); say('ok', `已获取号码:${order.phone_number}(订单 ${order.id},档位 #${c.productId} 价格 ${c.price} IDR)`); return { orderId: order.id, phone: order.phone_number, order }; } catch (e) { if (isStockError(e)) { say('warn', `档位 #${c.productId} 无库存/下单失败(${e.code || e.message}),尝试下一档位…`); continue; } throw e; // 鉴权/余额等非库存错误:立即抛出 } } const e = new Error(`所有档位均无库存/下单失败(country=${cfg.countryId} service=${cfg.serviceId})`); e.code = 'NO_OFFER_AVAILABLE'; throw e; }