Files
gpt/account-manager/node/smscode.js
T
432539 fef45134d5 Initial commit: GPT 账号注册机 (open source)
只开源「GPT 账号注册机 / 管理」(account-manager):账号列表/导入导出、检测/批量检测、
拿 RT、协议/浏览器注册等注册机能力。批量直开 / 协议支付 / 提取链接 / 直卡直开 等其它功能
不含实现代码,仅通过 iframe 内嵌外链访问。真实机密均由 .gitignore 忽略,仅提供脱敏示例配置。
2026-08-05 14:54:32 +08:00

242 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 接码 / SMS 验证码服务 —— smscode.gg API 客户端(零依赖,使用全局 fetch)
// 文档:https://smscode.gg/docs Base URL: https://api.smscode.gg/v1
// 鉴权:Authorization: Bearer <apiKey>
// 统一响应包络:{ 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;
}