mirror of
https://wget.la/https://github.com/432539/gpt
synced 2026-08-17 03:26:59 +08:00
只开源「GPT 账号注册机 / 管理」(account-manager):账号列表/导入导出、检测/批量检测、 拿 RT、协议/浏览器注册等注册机能力。批量直开 / 协议支付 / 提取链接 / 直卡直开 等其它功能 不含实现代码,仅通过 iframe 内嵌外链访问。真实机密均由 .gitignore 忽略,仅提供脱敏示例配置。
205 lines
8.8 KiB
JavaScript
205 lines
8.8 KiB
JavaScript
// 接码 / SMS 验证码服务 —— smsbower.app API 客户端(零依赖,使用全局 fetch)
|
||
// 协议:经典 sms-activate 风格,所有请求为 GET https://smsbower.page/stubs/handler_api.php?api_key=..&action=..
|
||
// 响应为纯文本(冒号分隔),如 ACCESS_NUMBER:<activationId>:<phone>、STATUS_OK:<code>
|
||
//
|
||
// 与 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:<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)');
|
||
}
|