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

206 lines
8.9 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 验证码服务 —— hero-sms.com API 客户端(零依赖,使用全局 fetch)
// 协议:经典 sms-activate 风格,所有请求为 GET https://hero-sms.com/stubs/handler_api.php?api_key=..&action=..
// 响应为纯文本(冒号分隔),如 ACCESS_NUMBER:<activationId>:<phone>、STATUS_OK:<code>、ACCESS_BALANCE:<n>
//
// 与 smsbower.js / 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, { log }) → setStatus status=8(取消,2 分钟内不可取消时静默处理)
// getBalance(cfg) → 余额数字
//
// 已实测(2026-07):base=https://hero-sms.com/stubs/handler_api.php
// India country=22OpenAI service="dr"getServicesList 返回 {code:"dr",name:"OpenAI"})。
// 配置字段:apiKey、baseUrl(默认 hero-sms 地址)、country(默认 22=印度)、
// service(默认 "dr"=OpenAI)、maxPrice、minPrice
const DEFAULT_BASE = 'https://hero-sms.com/stubs/handler_api.php';
const DEFAULT_COUNTRY = 22; // 印度
const DEFAULT_SERVICE = 'dr'; // OpenAI
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('未配置 hero-sms 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 的 ErrorBAD_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 },与 smsbower.acquireNumber 一致(orderId = activationId)。
// NO_NUMBERS(无库存,可回退重试)→ e.code='NO_OFFER_AVAILABLE'(与其它平台的库存错误标记一致),
// 并附 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;
if (cfg.operator) params.operator = cfg.operator; // 指定运营商(如 tim/claro/vivo),换号段规避风控
say('info', `向 hero-sms 请求号码(service=${service} country=${country}${cfg.operator ? ' operator=' + cfg.operator : ''}${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')) {
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 },与其它平台一致。
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;
}
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;
}
// ---------- 状态变更 ----------
// setStatus1=已就绪 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)。响应: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)');
}