Upload 17 files
Browse files- .dockerignore +8 -0
- Dockerfile +25 -0
- README.md +19 -10
- account.js +70 -0
- anthropic-adapter.js +169 -0
- chat.js +142 -0
- config.js +204 -0
- http.js +230 -0
- mail.js +1368 -0
- message-convert.js +120 -0
- openai-adapter.js +182 -0
- package.json +13 -0
- pool.js +516 -0
- protocol.js +287 -0
- server.js +211 -0
- stream-transform.js +281 -0
- ui.js +511 -0
.dockerignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
accounts
|
| 3 |
+
*.log
|
| 4 |
+
.git
|
| 5 |
+
debug-*
|
| 6 |
+
test-*
|
| 7 |
+
main.js
|
| 8 |
+
results.json
|
Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim
|
| 2 |
+
|
| 3 |
+
# HuggingFace Spaces 环境变量
|
| 4 |
+
ENV PORT=7860
|
| 5 |
+
ENV ACCOUNTS_DIR=/data/accounts
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
# 复制项目文件
|
| 10 |
+
COPY package.json ./
|
| 11 |
+
RUN npm install --production 2>/dev/null || true
|
| 12 |
+
|
| 13 |
+
COPY . .
|
| 14 |
+
|
| 15 |
+
# 创建数据目录并设置权限 (HF Spaces 以 uid=1000 运行)
|
| 16 |
+
RUN mkdir -p /data/accounts && \
|
| 17 |
+
chown -R 1000:1000 /data && \
|
| 18 |
+
chown -R 1000:1000 /app
|
| 19 |
+
|
| 20 |
+
USER 1000
|
| 21 |
+
|
| 22 |
+
# HuggingFace Spaces 要求暴露 7860 端口
|
| 23 |
+
EXPOSE 7860
|
| 24 |
+
|
| 25 |
+
CMD ["node", "server.js"]
|
README.md
CHANGED
|
@@ -1,10 +1,19 @@
|
|
| 1 |
-
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: ChatAIBot API Proxy
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# ChatAIBot API Proxy
|
| 11 |
+
|
| 12 |
+
OpenAI / Anthropic 兼容的 API 代理服务,自动管理账号池。
|
| 13 |
+
|
| 14 |
+
## 接口
|
| 15 |
+
|
| 16 |
+
- **OpenAI**: `POST /v1/chat/completions`
|
| 17 |
+
- **Anthropic**: `POST /v1/messages`
|
| 18 |
+
- **模型列表**: `GET /v1/models`
|
| 19 |
+
- **Dashboard**: `GET /`
|
account.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* account.js - 账号信息生成 (精简版)
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import crypto from 'crypto';
|
| 6 |
+
|
| 7 |
+
const FIRST_NAMES = [
|
| 8 |
+
"James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph",
|
| 9 |
+
"Thomas", "Charles", "Daniel", "Matthew", "Anthony", "Mark", "Steven", "Paul",
|
| 10 |
+
"Andrew", "Joshua", "Kenneth", "Kevin", "Jacob", "Benjamin", "Nathan", "Samuel",
|
| 11 |
+
"Ethan", "Noah", "Alexander", "Logan", "Lucas", "Mason", "Jack", "Henry",
|
| 12 |
+
"Mary", "Jennifer", "Elizabeth", "Jessica", "Sarah", "Lisa", "Ashley", "Emily",
|
| 13 |
+
"Emma", "Olivia", "Sophia", "Grace", "Isabella", "Charlotte", "Natalie",
|
| 14 |
+
"Carlos", "Miguel", "Sofia", "Valentina", "Hans", "Stefan", "Marco", "Luca",
|
| 15 |
+
];
|
| 16 |
+
|
| 17 |
+
const LAST_NAMES = [
|
| 18 |
+
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
|
| 19 |
+
"Rodriguez", "Martinez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore",
|
| 20 |
+
"Jackson", "Martin", "Lee", "Thompson", "White", "Harris", "Clark",
|
| 21 |
+
"Lewis", "Robinson", "Walker", "Young", "Allen", "King", "Wright",
|
| 22 |
+
"Hill", "Green", "Adams", "Nelson", "Baker", "Hall", "Rivera",
|
| 23 |
+
"Campbell", "Mitchell", "Carter", "Roberts", "Turner", "Phillips",
|
| 24 |
+
"Morgan", "Cooper", "Reed", "Bailey", "Bell", "Howard",
|
| 25 |
+
"Mueller", "Schmidt", "Rossi", "Ferrari", "Wang", "Li", "Kim", "Patel",
|
| 26 |
+
];
|
| 27 |
+
|
| 28 |
+
export function generatePassword(length = 14) {
|
| 29 |
+
const lowercase = 'abcdefghijklmnopqrstuvwxyz';
|
| 30 |
+
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
| 31 |
+
const digits = '0123456789';
|
| 32 |
+
const special = '!@#$%^&*';
|
| 33 |
+
|
| 34 |
+
const password = [
|
| 35 |
+
lowercase[Math.floor(Math.random() * lowercase.length)],
|
| 36 |
+
uppercase[Math.floor(Math.random() * uppercase.length)],
|
| 37 |
+
digits[Math.floor(Math.random() * digits.length)],
|
| 38 |
+
special[Math.floor(Math.random() * special.length)],
|
| 39 |
+
];
|
| 40 |
+
|
| 41 |
+
const allChars = lowercase + uppercase + digits + special;
|
| 42 |
+
for (let i = 4; i < length; i++) {
|
| 43 |
+
password.push(allChars[Math.floor(Math.random() * allChars.length)]);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Fisher-Yates shuffle
|
| 47 |
+
for (let i = password.length - 1; i > 0; i--) {
|
| 48 |
+
const j = Math.floor(Math.random() * (i + 1));
|
| 49 |
+
[password[i], password[j]] = [password[j], password[i]];
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
return password.join('');
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export function generateName() {
|
| 56 |
+
const firstName = FIRST_NAMES[Math.floor(Math.random() * FIRST_NAMES.length)];
|
| 57 |
+
const lastName = LAST_NAMES[Math.floor(Math.random() * LAST_NAMES.length)];
|
| 58 |
+
return { firstName, lastName };
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
export function generateAccountInfo() {
|
| 62 |
+
const { firstName, lastName } = generateName();
|
| 63 |
+
const password = generatePassword();
|
| 64 |
+
return {
|
| 65 |
+
password,
|
| 66 |
+
firstName,
|
| 67 |
+
lastName,
|
| 68 |
+
fullName: `${firstName} ${lastName}`,
|
| 69 |
+
};
|
| 70 |
+
}
|
anthropic-adapter.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* anthropic-adapter.js - Anthropic 兼容 API 适配器
|
| 3 |
+
*
|
| 4 |
+
* 处理 POST /v1/messages
|
| 5 |
+
* 将 Anthropic 格式请求转换为 chataibot.pro 协议
|
| 6 |
+
* 支持自动重试换号 (最多 3 次)
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import crypto from 'crypto';
|
| 10 |
+
import { createContext, sendMessageStreaming } from './chat.js';
|
| 11 |
+
import { anthropicToText, resolveModel } from './message-convert.js';
|
| 12 |
+
import { transformToAnthropicSSE, collectFullResponse } from './stream-transform.js';
|
| 13 |
+
|
| 14 |
+
const MAX_RETRY = 3;
|
| 15 |
+
|
| 16 |
+
function isRetryable(e) {
|
| 17 |
+
const code = e.statusCode || 0;
|
| 18 |
+
if (code === 401 || code === 403 || code === 429) return true;
|
| 19 |
+
const msg = (e.message || '').toLowerCase();
|
| 20 |
+
if (msg.includes('limit') || msg.includes('quota') || msg.includes('exhaust')
|
| 21 |
+
|| msg.includes('exceed') || msg.includes('too many') || msg.includes('no available')) return true;
|
| 22 |
+
return false;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function releaseOnError(pool, account, e) {
|
| 26 |
+
const code = e.statusCode || 0;
|
| 27 |
+
if (code === 401 || code === 403) {
|
| 28 |
+
pool.release(account, { sessionExpired: true });
|
| 29 |
+
} else if (code === 429) {
|
| 30 |
+
pool.release(account, { quotaExhausted: true });
|
| 31 |
+
} else {
|
| 32 |
+
pool.release(account, { success: false });
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* 处理 Anthropic messages 请求
|
| 38 |
+
*/
|
| 39 |
+
export async function handleMessages(body, res, pool) {
|
| 40 |
+
const requestId = 'msg_' + crypto.randomBytes(12).toString('hex');
|
| 41 |
+
|
| 42 |
+
if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
|
| 43 |
+
sendError(res, 400, 'messages is required and must be a non-empty array');
|
| 44 |
+
return;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const text = anthropicToText(body.system, body.messages);
|
| 48 |
+
const model = resolveModel(body.model);
|
| 49 |
+
const clientModel = body.model || 'claude-3-sonnet';
|
| 50 |
+
const stream = body.stream === true;
|
| 51 |
+
|
| 52 |
+
if (!text) {
|
| 53 |
+
sendError(res, 400, 'No valid message content found');
|
| 54 |
+
return;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// 流式请求
|
| 58 |
+
if (stream) {
|
| 59 |
+
let lastError;
|
| 60 |
+
for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
|
| 61 |
+
let account;
|
| 62 |
+
try {
|
| 63 |
+
account = await pool.acquire();
|
| 64 |
+
} catch (e) {
|
| 65 |
+
sendError(res, 503, 'No available account: ' + e.message);
|
| 66 |
+
return;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
try {
|
| 70 |
+
const title = text.substring(0, 100);
|
| 71 |
+
const ctx = await createContext(account.cookies, model, title);
|
| 72 |
+
if (ctx.cookies) account.cookies = ctx.cookies;
|
| 73 |
+
|
| 74 |
+
const result = await sendMessageStreaming(account.cookies, ctx.chatId, text, model);
|
| 75 |
+
if (result.cookies) account.cookies = result.cookies;
|
| 76 |
+
|
| 77 |
+
transformToAnthropicSSE(result.stream, res, clientModel, requestId, (errMsg) => {
|
| 78 |
+
const msg = (errMsg || '').toLowerCase();
|
| 79 |
+
if (msg.includes('limit') || msg.includes('quota') || msg.includes('exhaust') || msg.includes('exceed')) {
|
| 80 |
+
pool.release(account, { quotaExhausted: true });
|
| 81 |
+
} else {
|
| 82 |
+
pool.release(account, { success: false });
|
| 83 |
+
}
|
| 84 |
+
account = null;
|
| 85 |
+
});
|
| 86 |
+
result.stream.on('end', () => { if (account) pool.release(account, { success: true }); });
|
| 87 |
+
result.stream.on('error', (err) => {
|
| 88 |
+
if (!account) return;
|
| 89 |
+
const msg = (err?.message || '').toLowerCase();
|
| 90 |
+
if (msg.includes('limit') || msg.includes('quota') || msg.includes('exhaust')) {
|
| 91 |
+
pool.release(account, { quotaExhausted: true });
|
| 92 |
+
} else {
|
| 93 |
+
pool.release(account, { success: false });
|
| 94 |
+
}
|
| 95 |
+
});
|
| 96 |
+
return;
|
| 97 |
+
} catch (e) {
|
| 98 |
+
releaseOnError(pool, account, e);
|
| 99 |
+
lastError = e;
|
| 100 |
+
if (isRetryable(e) && attempt < MAX_RETRY) {
|
| 101 |
+
console.log(`[Anthropic] 请求失败 (${e.statusCode || e.message}), 换号重试 ${attempt + 1}/${MAX_RETRY}`);
|
| 102 |
+
continue;
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
if (!res.headersSent) {
|
| 107 |
+
sendError(res, 502, `Upstream error after ${MAX_RETRY} retries: ${lastError?.message}`);
|
| 108 |
+
}
|
| 109 |
+
return;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// 非流式请求
|
| 113 |
+
let lastError;
|
| 114 |
+
for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
|
| 115 |
+
let account;
|
| 116 |
+
try {
|
| 117 |
+
account = await pool.acquire();
|
| 118 |
+
} catch (e) {
|
| 119 |
+
sendError(res, 503, 'No available account: ' + e.message);
|
| 120 |
+
return;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
try {
|
| 124 |
+
const title = text.substring(0, 100);
|
| 125 |
+
const ctx = await createContext(account.cookies, model, title);
|
| 126 |
+
if (ctx.cookies) account.cookies = ctx.cookies;
|
| 127 |
+
|
| 128 |
+
const result = await sendMessageStreaming(account.cookies, ctx.chatId, text, model);
|
| 129 |
+
if (result.cookies) account.cookies = result.cookies;
|
| 130 |
+
|
| 131 |
+
const full = await collectFullResponse(result.stream);
|
| 132 |
+
pool.release(account, { success: true });
|
| 133 |
+
|
| 134 |
+
res.writeHead(200, {
|
| 135 |
+
'Content-Type': 'application/json',
|
| 136 |
+
'Access-Control-Allow-Origin': '*',
|
| 137 |
+
});
|
| 138 |
+
res.end(JSON.stringify({
|
| 139 |
+
id: requestId,
|
| 140 |
+
type: 'message',
|
| 141 |
+
role: 'assistant',
|
| 142 |
+
model: clientModel,
|
| 143 |
+
content: [{ type: 'text', text: full.text }],
|
| 144 |
+
stop_reason: 'end_turn',
|
| 145 |
+
stop_sequence: null,
|
| 146 |
+
usage: { input_tokens: 0, output_tokens: 0 },
|
| 147 |
+
}));
|
| 148 |
+
return;
|
| 149 |
+
} catch (e) {
|
| 150 |
+
releaseOnError(pool, account, e);
|
| 151 |
+
lastError = e;
|
| 152 |
+
if (isRetryable(e) && attempt < MAX_RETRY) {
|
| 153 |
+
console.log(`[Anthropic] 请求失败 (${e.statusCode || e.message}), 换号重试 ${attempt + 1}/${MAX_RETRY}`);
|
| 154 |
+
continue;
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
if (!res.headersSent) {
|
| 160 |
+
sendError(res, 502, `Upstream error after ${MAX_RETRY} retries: ${lastError?.message}`);
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
function sendError(res, status, message) {
|
| 165 |
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
| 166 |
+
res.end(JSON.stringify({
|
| 167 |
+
error: { message, type: 'invalid_request_error' },
|
| 168 |
+
}));
|
| 169 |
+
}
|
chat.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* chat.js - ChatAIBot.pro 聊天协议层
|
| 3 |
+
*
|
| 4 |
+
* 逆向自前端 JS chunks:
|
| 5 |
+
* POST /api/message/context → 创建聊天上下文
|
| 6 |
+
* POST /api/message/streaming → 流式聊天 (NDJSON SSE)
|
| 7 |
+
* GET /api/user/answers-count/v2 → 剩余额度
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import { post, get, requestStream } from './http.js';
|
| 11 |
+
import config from './config.js';
|
| 12 |
+
|
| 13 |
+
const API = config.siteBase;
|
| 14 |
+
|
| 15 |
+
const HEADERS = {
|
| 16 |
+
'Origin': config.siteBase,
|
| 17 |
+
'Referer': `${config.siteBase}/app/chat`,
|
| 18 |
+
'Accept-Language': 'en',
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* 创建聊天上下文
|
| 23 |
+
* @returns {{ chatId: number }}
|
| 24 |
+
*/
|
| 25 |
+
export async function createContext(cookies, model, title = 'API Chat') {
|
| 26 |
+
const resp = await post(`${API}/api/message/context`, {
|
| 27 |
+
title: title.substring(0, 150),
|
| 28 |
+
chatModel: model,
|
| 29 |
+
isInternational: true,
|
| 30 |
+
}, {
|
| 31 |
+
cookies,
|
| 32 |
+
headers: HEADERS,
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
if (!resp.ok) {
|
| 36 |
+
const body = resp.text();
|
| 37 |
+
throw Object.assign(new Error(`创建上下文失败 (${resp.status}): ${body.substring(0, 200)}`), {
|
| 38 |
+
statusCode: resp.status,
|
| 39 |
+
});
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
const data = resp.json();
|
| 43 |
+
return { chatId: data.id, cookies: resp.cookies };
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* 流式聊天 — 返回原始 NDJSON 流
|
| 48 |
+
* 响应流中每行是一个 JSON: {"type":"chunk","data":"..."} 等
|
| 49 |
+
*/
|
| 50 |
+
export async function sendMessageStreaming(cookies, chatId, text, model) {
|
| 51 |
+
const resp = await requestStream(`${API}/api/message/streaming`, {
|
| 52 |
+
method: 'POST',
|
| 53 |
+
body: {
|
| 54 |
+
text,
|
| 55 |
+
chatId,
|
| 56 |
+
withPotentialQuestions: false,
|
| 57 |
+
model,
|
| 58 |
+
from: 1,
|
| 59 |
+
},
|
| 60 |
+
cookies,
|
| 61 |
+
headers: {
|
| 62 |
+
...HEADERS,
|
| 63 |
+
'Accept': 'text/event-stream',
|
| 64 |
+
},
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
if (!resp.ok) {
|
| 68 |
+
// 需要消费流以获取错误信息
|
| 69 |
+
let errBody = '';
|
| 70 |
+
resp.stream.setEncoding('utf8');
|
| 71 |
+
for await (const chunk of resp.stream) errBody += chunk;
|
| 72 |
+
throw Object.assign(new Error(`流式请求失败 (${resp.status}): ${errBody.substring(0, 200)}`), {
|
| 73 |
+
statusCode: resp.status,
|
| 74 |
+
});
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
resp.stream.setEncoding('utf8');
|
| 78 |
+
return { stream: resp.stream, cookies: resp.cookies };
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* 非流式聊天
|
| 83 |
+
*/
|
| 84 |
+
export async function sendMessage(cookies, chatId, text, model) {
|
| 85 |
+
const resp = await post(`${API}/api/message`, {
|
| 86 |
+
text,
|
| 87 |
+
chatId,
|
| 88 |
+
withPotentialQuestions: true,
|
| 89 |
+
model,
|
| 90 |
+
from: 1,
|
| 91 |
+
isInternational: true,
|
| 92 |
+
}, {
|
| 93 |
+
cookies,
|
| 94 |
+
headers: HEADERS,
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
if (!resp.ok) {
|
| 98 |
+
throw Object.assign(new Error(`消息发送失败 (${resp.status}): ${resp.text().substring(0, 200)}`), {
|
| 99 |
+
statusCode: resp.status,
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
return { data: resp.json(), cookies: resp.cookies };
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/**
|
| 107 |
+
* 获取剩余额度
|
| 108 |
+
* @returns {{ remaining: number } | null}
|
| 109 |
+
*/
|
| 110 |
+
export async function getQuota(cookies) {
|
| 111 |
+
try {
|
| 112 |
+
const resp = await get(`${API}/api/user/answers-count/v2`, {
|
| 113 |
+
cookies,
|
| 114 |
+
headers: HEADERS,
|
| 115 |
+
});
|
| 116 |
+
if (!resp.ok) return null;
|
| 117 |
+
const data = resp.json();
|
| 118 |
+
// 可能返回 { answersCount, freeAnswersCount, ... }
|
| 119 |
+
return {
|
| 120 |
+
remaining: data.freeAnswersCount ?? data.answersCount ?? null,
|
| 121 |
+
raw: data,
|
| 122 |
+
};
|
| 123 |
+
} catch {
|
| 124 |
+
return null;
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
/**
|
| 129 |
+
* 获取用户信息 (验证 session 是否有效)
|
| 130 |
+
*/
|
| 131 |
+
export async function getUserInfo(cookies) {
|
| 132 |
+
try {
|
| 133 |
+
const resp = await get(`${API}/api/user`, {
|
| 134 |
+
cookies,
|
| 135 |
+
headers: HEADERS,
|
| 136 |
+
});
|
| 137 |
+
if (!resp.ok) return null;
|
| 138 |
+
return resp.json();
|
| 139 |
+
} catch {
|
| 140 |
+
return null;
|
| 141 |
+
}
|
| 142 |
+
}
|
config.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* config.js - ChatAIBot.pro 注册配置 (HuggingFace Spaces 版)
|
| 3 |
+
*
|
| 4 |
+
* 敏感信息从环境变量读取 (HF Secrets)
|
| 5 |
+
* 端口默认 7860 (HF Spaces 标准端口)
|
| 6 |
+
* 账号数据持久化到 /data/accounts (HF 持久存储)
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
const env = process.env;
|
| 10 |
+
|
| 11 |
+
export default {
|
| 12 |
+
// ==================== 站点配置 ====================
|
| 13 |
+
siteBase: 'https://chataibot.pro',
|
| 14 |
+
signupUrl: 'https://chataibot.pro/app/auth/sign-up',
|
| 15 |
+
loginUrl: 'https://chataibot.pro/app/auth/sign-in',
|
| 16 |
+
verifyUrl: 'https://chataibot.pro/app/verify',
|
| 17 |
+
|
| 18 |
+
// ==================== 邮箱配置 ====================
|
| 19 |
+
mailProvider: env.MAIL_PROVIDER || 'mailtm',
|
| 20 |
+
|
| 21 |
+
moemail: {
|
| 22 |
+
apiUrl: env.MOEMAIL_API_URL || '',
|
| 23 |
+
apiKey: env.MOEMAIL_API_KEY || '',
|
| 24 |
+
domain: env.MOEMAIL_DOMAIN || 'moemail.app',
|
| 25 |
+
prefix: env.MOEMAIL_PREFIX || 'ikun',
|
| 26 |
+
randomLength: 6,
|
| 27 |
+
duration: 0,
|
| 28 |
+
},
|
| 29 |
+
|
| 30 |
+
gptmail: {
|
| 31 |
+
apiBase: 'https://mail.chatgpt.org.uk',
|
| 32 |
+
apiKey: 'gpt-test',
|
| 33 |
+
fallbackKey: 'sk-LQ8yCnju',
|
| 34 |
+
},
|
| 35 |
+
|
| 36 |
+
duckmail: {
|
| 37 |
+
apiKey: env.DUCKMAIL_API_KEY || '',
|
| 38 |
+
domain: env.DUCKMAIL_DOMAIN || '',
|
| 39 |
+
},
|
| 40 |
+
|
| 41 |
+
catchall: {
|
| 42 |
+
domain: env.CATCHALL_DOMAIN || '',
|
| 43 |
+
prefix: 'ikun',
|
| 44 |
+
verifyMethod: 'manual',
|
| 45 |
+
workerUrl: '',
|
| 46 |
+
workerSecret: '',
|
| 47 |
+
},
|
| 48 |
+
|
| 49 |
+
custom: {
|
| 50 |
+
createUrl: env.CUSTOM_CREATE_URL || '',
|
| 51 |
+
createHeaders: {},
|
| 52 |
+
fetchUrl: env.CUSTOM_FETCH_URL || '',
|
| 53 |
+
fetchHeaders: {},
|
| 54 |
+
},
|
| 55 |
+
|
| 56 |
+
// ==================== 注册参数 ====================
|
| 57 |
+
count: 1,
|
| 58 |
+
concurrency: 1,
|
| 59 |
+
taskDelay: 3000,
|
| 60 |
+
retryCount: 1,
|
| 61 |
+
|
| 62 |
+
// ==================== 请求配置 ====================
|
| 63 |
+
timeout: 30000,
|
| 64 |
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
| 65 |
+
|
| 66 |
+
// ==================== 验证码发件人过滤 ====================
|
| 67 |
+
senderFilter: 'chataibot',
|
| 68 |
+
|
| 69 |
+
// ==================== 代理配置 ====================
|
| 70 |
+
proxyEnabled: false,
|
| 71 |
+
proxyUrl: '',
|
| 72 |
+
|
| 73 |
+
// ==================== 输出配置 ====================
|
| 74 |
+
// HuggingFace Spaces 持久存储在 /data 目录
|
| 75 |
+
outputDir: env.ACCOUNTS_DIR || '/data/accounts',
|
| 76 |
+
exportJsonPath: '/data/results.json',
|
| 77 |
+
|
| 78 |
+
// ==================== API 代理服务配置 ====================
|
| 79 |
+
server: {
|
| 80 |
+
port: parseInt(env.PORT) || 7860, // HF Spaces 标准端口
|
| 81 |
+
host: '0.0.0.0',
|
| 82 |
+
apiKey: env.API_KEY || '', // 访问密钥 (空=不校验)
|
| 83 |
+
},
|
| 84 |
+
|
| 85 |
+
// ==================== 账号池配置 ====================
|
| 86 |
+
pool: {
|
| 87 |
+
minAvailable: parseInt(env.MIN_AVAILABLE) || 5, // HF 环境降低,避免大量注册触发限流
|
| 88 |
+
autoRegister: env.AUTO_REGISTER !== 'false',
|
| 89 |
+
checkInterval: parseInt(env.CHECK_INTERVAL) || 300000,
|
| 90 |
+
},
|
| 91 |
+
|
| 92 |
+
// ==================== 模型映射 ====================
|
| 93 |
+
modelMapping: {
|
| 94 |
+
// ---- OpenAI ----
|
| 95 |
+
'gpt-3.5-turbo': 'gpt-3.5-turbo',
|
| 96 |
+
'gpt-4': 'gpt-4',
|
| 97 |
+
'gpt-4-turbo': 'gpt-4-turbo',
|
| 98 |
+
'gpt-4o': 'gpt-4o',
|
| 99 |
+
'gpt-4o-mini': 'gpt-4o-mini',
|
| 100 |
+
'gpt-4.1': 'gpt-4.1',
|
| 101 |
+
'gpt-4.1-mini': 'gpt-4.1-mini',
|
| 102 |
+
'gpt-4.1-nano': 'gpt-4.1-nano',
|
| 103 |
+
'gpt-5-pro': 'gpt-5-pro',
|
| 104 |
+
'gpt-5.1': 'gpt-5.1',
|
| 105 |
+
'gpt-5.1-high': 'gpt-5.1-high',
|
| 106 |
+
'gpt-5.2': 'gpt-5.2',
|
| 107 |
+
'gpt-5.2-high': 'gpt-5.2-high',
|
| 108 |
+
'gpt-5.4': 'gpt-5.4',
|
| 109 |
+
'gpt-5.4-high': 'gpt-5.4-high',
|
| 110 |
+
'gpt-5.4-pro': 'gpt-5.4-pro',
|
| 111 |
+
'o1': 'o1',
|
| 112 |
+
'o1-preview': 'o1-preview',
|
| 113 |
+
'o1-mini': 'o1-mini',
|
| 114 |
+
'o3': 'o3',
|
| 115 |
+
'o3-mini': 'o3-mini',
|
| 116 |
+
'o3-mini-high': 'o3-mini-high',
|
| 117 |
+
'o3-pro': 'o3-pro',
|
| 118 |
+
'o4-mini': 'o4-mini',
|
| 119 |
+
'o4-mini-high': 'o4-mini-high',
|
| 120 |
+
'o4-mini-deep-research': 'o4-mini-deep-research',
|
| 121 |
+
'gpt-4o-search-preview': 'gpt-4o-search-preview',
|
| 122 |
+
'gpt-4o-mini-search-preview': 'gpt-4o-mini-search-preview',
|
| 123 |
+
|
| 124 |
+
// ---- Anthropic ----
|
| 125 |
+
'claude-3-haiku': 'claude-3-haiku',
|
| 126 |
+
'claude-3-sonnet': 'claude-3-sonnet',
|
| 127 |
+
'claude-3-sonnet-high': 'claude-3-sonnet-high',
|
| 128 |
+
'claude-4.6-sonnet': 'claude-4.6-sonnet',
|
| 129 |
+
'claude-4.6-sonnet-high': 'claude-4.6-sonnet-high',
|
| 130 |
+
'claude-3-opus': 'claude-3-opus',
|
| 131 |
+
'claude-4.5-opus': 'claude-4.5-opus',
|
| 132 |
+
'claude-4.6-opus': 'claude-4.6-opus',
|
| 133 |
+
'claude-4.5-haiku': 'claude-4.5-haiku',
|
| 134 |
+
'claude-3-haiku-20240307': 'claude-3-haiku',
|
| 135 |
+
'claude-3-opus-20240229': 'claude-3-opus',
|
| 136 |
+
'claude-3-sonnet-20240229': 'claude-3-sonnet',
|
| 137 |
+
'claude-3-5-sonnet-20240620': 'claude-3-sonnet',
|
| 138 |
+
'claude-3.5-sonnet': 'claude-4.6-sonnet',
|
| 139 |
+
'claude-3-5-sonnet': 'claude-4.6-sonnet',
|
| 140 |
+
'claude-3-5-sonnet-20241022': 'claude-4.6-sonnet',
|
| 141 |
+
'claude-3.5-haiku': 'claude-4.5-haiku',
|
| 142 |
+
'claude-3-5-haiku': 'claude-4.5-haiku',
|
| 143 |
+
'claude-3-5-haiku-20241022': 'claude-4.5-haiku',
|
| 144 |
+
'claude-4-sonnet': 'claude-4.6-sonnet',
|
| 145 |
+
'claude-sonnet-4-20250514': 'claude-4.6-sonnet',
|
| 146 |
+
'claude-4-opus': 'claude-4.5-opus',
|
| 147 |
+
'claude-opus-4-20250514': 'claude-4.5-opus',
|
| 148 |
+
'claude-sonnet-4-6': 'claude-4.6-sonnet',
|
| 149 |
+
'claude-opus-4-6': 'claude-4.6-opus',
|
| 150 |
+
'claude-haiku-4-5': 'claude-4.5-haiku',
|
| 151 |
+
'claude-4.5-sonnet': 'claude-4.6-sonnet',
|
| 152 |
+
'claude-4.5-sonnet-high': 'claude-4.6-sonnet-high',
|
| 153 |
+
'claude-4.1-opus': 'claude-4.5-opus',
|
| 154 |
+
|
| 155 |
+
// ---- Google ----
|
| 156 |
+
'gemini-flash': 'gemini-flash',
|
| 157 |
+
'gemini-pro': 'gemini-pro',
|
| 158 |
+
'gemini-3-flash': 'gemini-3-flash',
|
| 159 |
+
'gemini-3-pro': 'gemini-3-pro',
|
| 160 |
+
'gemini-3.1-pro': 'gemini-3.1-pro',
|
| 161 |
+
'gemini-2-flash-search': 'gemini-2-flash-search',
|
| 162 |
+
'gemini-3-pro-search': 'gemini-3-pro-search',
|
| 163 |
+
'gemini-3-flash-search': 'gemini-3-flash-search',
|
| 164 |
+
'gemini-2-flash': 'gemini-flash',
|
| 165 |
+
'gemini-2.0-flash': 'gemini-flash',
|
| 166 |
+
'gemini-3.0-flash': 'gemini-3-flash',
|
| 167 |
+
'gemini-3.0-pro': 'gemini-3-pro',
|
| 168 |
+
'gemini-2.5-pro': 'gemini-pro',
|
| 169 |
+
'gemini-1.5-pro': 'gemini-pro',
|
| 170 |
+
'gemini-1.5-flash': 'gemini-flash',
|
| 171 |
+
|
| 172 |
+
// ---- 其他 ----
|
| 173 |
+
'deepseek': 'deepseek',
|
| 174 |
+
'deepseek-v3.2': 'deepseek-v3.2',
|
| 175 |
+
'deepseek-chat': 'deepseek',
|
| 176 |
+
'deepseek-v3': 'deepseek',
|
| 177 |
+
'deepseek-r1': 'deepseek',
|
| 178 |
+
'deepseek-reasoner': 'deepseek',
|
| 179 |
+
'deepseek-coder': 'deepseek',
|
| 180 |
+
'qwen3.5': 'qwen3.5',
|
| 181 |
+
'qwen3.5-plus': 'qwen3.5-plus',
|
| 182 |
+
'qwen3-max': 'qwen3-max',
|
| 183 |
+
'qwen3-thinking-2507': 'qwen3-thinking-2507',
|
| 184 |
+
'qwen3': 'qwen3-thinking-2507',
|
| 185 |
+
'qwen-max': 'qwen3-max',
|
| 186 |
+
'qwen-plus': 'qwen3.5-plus',
|
| 187 |
+
'qwen-turbo': 'qwen3.5',
|
| 188 |
+
'grok': 'grok',
|
| 189 |
+
'grok-2': 'grok',
|
| 190 |
+
'grok-3': 'grok',
|
| 191 |
+
'grok-3-mini': 'grok',
|
| 192 |
+
'perplexity': 'perplexity',
|
| 193 |
+
'perplexity-pro': 'perplexity-pro',
|
| 194 |
+
'pplx': 'perplexity',
|
| 195 |
+
'pplx-pro': 'perplexity-pro',
|
| 196 |
+
'llama-3': 'gpt-4o-mini',
|
| 197 |
+
'llama-3.1': 'gpt-4o',
|
| 198 |
+
'llama-3.2': 'gpt-4o',
|
| 199 |
+
'llama-4': 'gpt-4.1',
|
| 200 |
+
'mistral-large': 'gpt-4o',
|
| 201 |
+
'mistral-small': 'gpt-4o-mini',
|
| 202 |
+
'codestral': 'gpt-4o',
|
| 203 |
+
},
|
| 204 |
+
};
|
http.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* http.js - HTTP 请求工具模块
|
| 3 |
+
* 纯 Node.js 原生 https/http 实现,零依赖
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import https from 'https';
|
| 7 |
+
import http from 'http';
|
| 8 |
+
import { URL } from 'url';
|
| 9 |
+
import config from './config.js';
|
| 10 |
+
|
| 11 |
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* 通用 HTTP 请求 — 支持 cookie 管理、重定向跟踪、JSON 自动解析
|
| 15 |
+
*/
|
| 16 |
+
export async function request(url, options = {}) {
|
| 17 |
+
const {
|
| 18 |
+
method = 'GET',
|
| 19 |
+
headers = {},
|
| 20 |
+
body = null,
|
| 21 |
+
timeout = config.timeout || 30000,
|
| 22 |
+
followRedirect = true,
|
| 23 |
+
maxRedirects = 5,
|
| 24 |
+
cookies = null,
|
| 25 |
+
} = options;
|
| 26 |
+
|
| 27 |
+
const urlObj = new URL(url);
|
| 28 |
+
const isHttps = urlObj.protocol === 'https:';
|
| 29 |
+
const lib = isHttps ? https : http;
|
| 30 |
+
|
| 31 |
+
const finalHeaders = {
|
| 32 |
+
'User-Agent': config.userAgent,
|
| 33 |
+
'Accept': 'application/json, text/html, */*',
|
| 34 |
+
'Accept-Language': 'en-US,en;q=0.9,ru;q=0.8,zh-CN;q=0.7',
|
| 35 |
+
'Accept-Encoding': 'identity',
|
| 36 |
+
...headers,
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
// 注入 cookies
|
| 40 |
+
if (cookies && cookies.size > 0) {
|
| 41 |
+
finalHeaders['Cookie'] = [...cookies.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// 处理 body
|
| 45 |
+
let bodyStr = null;
|
| 46 |
+
if (body) {
|
| 47 |
+
bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
| 48 |
+
if (!finalHeaders['Content-Type']) {
|
| 49 |
+
finalHeaders['Content-Type'] = 'application/json';
|
| 50 |
+
}
|
| 51 |
+
finalHeaders['Content-Length'] = Buffer.byteLength(bodyStr);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
return new Promise((resolve, reject) => {
|
| 55 |
+
const reqOptions = {
|
| 56 |
+
hostname: urlObj.hostname,
|
| 57 |
+
port: urlObj.port || (isHttps ? 443 : 80),
|
| 58 |
+
path: urlObj.pathname + urlObj.search,
|
| 59 |
+
method,
|
| 60 |
+
headers: finalHeaders,
|
| 61 |
+
timeout,
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
const req = lib.request(reqOptions, (res) => {
|
| 65 |
+
// 收集 Set-Cookie
|
| 66 |
+
const resCookies = new Map(cookies || []);
|
| 67 |
+
const setCookieHeaders = res.headers['set-cookie'] || [];
|
| 68 |
+
for (const sc of setCookieHeaders) {
|
| 69 |
+
const [pair] = sc.split(';');
|
| 70 |
+
const [name, ...valueParts] = pair.split('=');
|
| 71 |
+
resCookies.set(name.trim(), valueParts.join('=').trim());
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// 处理重定向
|
| 75 |
+
if (followRedirect && [301, 302, 303, 307, 308].includes(res.statusCode) && maxRedirects > 0) {
|
| 76 |
+
const location = res.headers['location'];
|
| 77 |
+
if (location) {
|
| 78 |
+
const redirectUrl = location.startsWith('http') ? location : new URL(location, url).href;
|
| 79 |
+
res.resume(); // 丢弃 body
|
| 80 |
+
resolve(request(redirectUrl, {
|
| 81 |
+
...options,
|
| 82 |
+
maxRedirects: maxRedirects - 1,
|
| 83 |
+
cookies: resCookies,
|
| 84 |
+
method: [301, 302, 303].includes(res.statusCode) ? 'GET' : method,
|
| 85 |
+
body: [301, 302, 303].includes(res.statusCode) ? null : body,
|
| 86 |
+
}));
|
| 87 |
+
return;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
let data = '';
|
| 92 |
+
res.setEncoding('utf8');
|
| 93 |
+
res.on('data', chunk => data += chunk);
|
| 94 |
+
res.on('end', () => {
|
| 95 |
+
resolve({
|
| 96 |
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
| 97 |
+
status: res.statusCode,
|
| 98 |
+
statusText: res.statusMessage,
|
| 99 |
+
headers: res.headers,
|
| 100 |
+
cookies: resCookies,
|
| 101 |
+
url,
|
| 102 |
+
text: () => data,
|
| 103 |
+
json: () => {
|
| 104 |
+
try { return JSON.parse(data); }
|
| 105 |
+
catch { throw new Error(`JSON parse error: ${data.substring(0, 200)}`); }
|
| 106 |
+
},
|
| 107 |
+
html: () => data,
|
| 108 |
+
});
|
| 109 |
+
});
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
req.on('error', reject);
|
| 113 |
+
req.on('timeout', () => { req.destroy(); reject(new Error(`请求超时: ${url}`)); });
|
| 114 |
+
|
| 115 |
+
if (bodyStr) req.write(bodyStr);
|
| 116 |
+
req.end();
|
| 117 |
+
});
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/**
|
| 121 |
+
* GET 请求简写
|
| 122 |
+
*/
|
| 123 |
+
export async function get(url, options = {}) {
|
| 124 |
+
return request(url, { ...options, method: 'GET' });
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/**
|
| 128 |
+
* POST 请求简写
|
| 129 |
+
*/
|
| 130 |
+
export async function post(url, body, options = {}) {
|
| 131 |
+
return request(url, { ...options, method: 'POST', body });
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/**
|
| 135 |
+
* 带重试的请求
|
| 136 |
+
*/
|
| 137 |
+
export async function retryRequest(url, options = {}, maxRetries = 3) {
|
| 138 |
+
let lastError;
|
| 139 |
+
for (let i = 0; i < maxRetries; i++) {
|
| 140 |
+
try {
|
| 141 |
+
const resp = await request(url, options);
|
| 142 |
+
if (resp.ok || resp.status < 500) return resp;
|
| 143 |
+
lastError = new Error(`HTTP ${resp.status}: ${resp.text().substring(0, 200)}`);
|
| 144 |
+
} catch (e) {
|
| 145 |
+
lastError = e;
|
| 146 |
+
}
|
| 147 |
+
if (i < maxRetries - 1) {
|
| 148 |
+
const delay = 1000 * (i + 1) + Math.random() * 1000;
|
| 149 |
+
console.log(` [HTTP] 重试 ${i + 1}/${maxRetries} (${Math.round(delay)}ms)...`);
|
| 150 |
+
await sleep(delay);
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
throw lastError;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
export { sleep };
|
| 157 |
+
|
| 158 |
+
/**
|
| 159 |
+
* 流式 HTTP 请求 — 返回原始响应流 (不缓冲 body)
|
| 160 |
+
* 用于代理 chataibot SSE 流式响应
|
| 161 |
+
*/
|
| 162 |
+
export async function requestStream(url, options = {}) {
|
| 163 |
+
const {
|
| 164 |
+
method = 'GET',
|
| 165 |
+
headers = {},
|
| 166 |
+
body = null,
|
| 167 |
+
timeout = config.timeout || 120000,
|
| 168 |
+
cookies = null,
|
| 169 |
+
} = options;
|
| 170 |
+
|
| 171 |
+
const urlObj = new URL(url);
|
| 172 |
+
const isHttps = urlObj.protocol === 'https:';
|
| 173 |
+
const lib = isHttps ? https : http;
|
| 174 |
+
|
| 175 |
+
const finalHeaders = {
|
| 176 |
+
'User-Agent': config.userAgent,
|
| 177 |
+
'Accept': 'text/event-stream, application/json',
|
| 178 |
+
'Accept-Language': 'en',
|
| 179 |
+
'Accept-Encoding': 'identity',
|
| 180 |
+
...headers,
|
| 181 |
+
};
|
| 182 |
+
|
| 183 |
+
if (cookies && cookies.size > 0) {
|
| 184 |
+
finalHeaders['Cookie'] = [...cookies.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
let bodyStr = null;
|
| 188 |
+
if (body) {
|
| 189 |
+
bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
| 190 |
+
if (!finalHeaders['Content-Type']) {
|
| 191 |
+
finalHeaders['Content-Type'] = 'application/json';
|
| 192 |
+
}
|
| 193 |
+
finalHeaders['Content-Length'] = Buffer.byteLength(bodyStr);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
return new Promise((resolve, reject) => {
|
| 197 |
+
const reqOptions = {
|
| 198 |
+
hostname: urlObj.hostname,
|
| 199 |
+
port: urlObj.port || (isHttps ? 443 : 80),
|
| 200 |
+
path: urlObj.pathname + urlObj.search,
|
| 201 |
+
method,
|
| 202 |
+
headers: finalHeaders,
|
| 203 |
+
timeout,
|
| 204 |
+
};
|
| 205 |
+
|
| 206 |
+
const req = lib.request(reqOptions, (res) => {
|
| 207 |
+
const resCookies = new Map(cookies || []);
|
| 208 |
+
const setCookieHeaders = res.headers['set-cookie'] || [];
|
| 209 |
+
for (const sc of setCookieHeaders) {
|
| 210 |
+
const [pair] = sc.split(';');
|
| 211 |
+
const [name, ...valueParts] = pair.split('=');
|
| 212 |
+
resCookies.set(name.trim(), valueParts.join('=').trim());
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
resolve({
|
| 216 |
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
| 217 |
+
status: res.statusCode,
|
| 218 |
+
headers: res.headers,
|
| 219 |
+
cookies: resCookies,
|
| 220 |
+
stream: res,
|
| 221 |
+
});
|
| 222 |
+
});
|
| 223 |
+
|
| 224 |
+
req.on('error', reject);
|
| 225 |
+
req.on('timeout', () => { req.destroy(); reject(new Error(`流式请求超时: ${url}`)); });
|
| 226 |
+
|
| 227 |
+
if (bodyStr) req.write(bodyStr);
|
| 228 |
+
req.end();
|
| 229 |
+
});
|
| 230 |
+
}
|
mail.js
ADDED
|
@@ -0,0 +1,1368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* mail.js - 多渠道邮箱 Provider 系统
|
| 3 |
+
*
|
| 4 |
+
* 支持的邮箱渠道:
|
| 5 |
+
* moemail - MoeMail 自建域名邮箱(推荐,需要 API Key)
|
| 6 |
+
* gptmail - GPTMail 临时邮箱(mail.chatgpt.org.uk)
|
| 7 |
+
* guerrilla - GuerrillaMail 临时邮箱(免费,全自动)
|
| 8 |
+
* tempmail - TempMail.lol 临时邮箱(免费,全自动)
|
| 9 |
+
* mailtm - Mail.tm 临时邮箱(免费,全自动)
|
| 10 |
+
* duckmail - DuckMail 自建域名邮箱(支持私有域名)
|
| 11 |
+
* catchall - 自定义域名 Catch-All(Cloudflare/自建)
|
| 12 |
+
* custom - 自定义 HTTP 接口
|
| 13 |
+
* manual - 手动输入邮箱和验证码
|
| 14 |
+
*
|
| 15 |
+
* 适配 chataibot.pro 验证邮件:
|
| 16 |
+
* 发件人: xxx@send.chataibot.pro
|
| 17 |
+
* 主题: "Your verification code for Chat AI"
|
| 18 |
+
* 内容: "Your code: 528135"
|
| 19 |
+
* 验证链接: https://chataibot.pro/api/register/verify?email=xxx&token=528135
|
| 20 |
+
* token = 6 位数字验证码
|
| 21 |
+
*/
|
| 22 |
+
|
| 23 |
+
import https from 'https';
|
| 24 |
+
import crypto from 'crypto';
|
| 25 |
+
import { createInterface } from 'readline';
|
| 26 |
+
|
| 27 |
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
| 28 |
+
function prompt(question) {
|
| 29 |
+
return new Promise(resolve => {
|
| 30 |
+
rl.question(question, answer => resolve(answer.trim()));
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// ==================== 通用工具 ====================
|
| 35 |
+
|
| 36 |
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
| 37 |
+
|
| 38 |
+
function httpsGet(url, options = {}) {
|
| 39 |
+
return new Promise((resolve, reject) => {
|
| 40 |
+
const urlObj = new URL(url);
|
| 41 |
+
const req = https.request({
|
| 42 |
+
hostname: urlObj.hostname,
|
| 43 |
+
path: urlObj.pathname + urlObj.search,
|
| 44 |
+
method: 'GET',
|
| 45 |
+
headers: {
|
| 46 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
| 47 |
+
'Accept': 'application/json, text/plain, */*',
|
| 48 |
+
'Accept-Language': 'en-US,en;q=0.9',
|
| 49 |
+
...options.headers,
|
| 50 |
+
},
|
| 51 |
+
timeout: options.timeout || 30000,
|
| 52 |
+
}, (res) => {
|
| 53 |
+
let data = '';
|
| 54 |
+
res.on('data', chunk => data += chunk);
|
| 55 |
+
res.on('end', () => {
|
| 56 |
+
resolve({
|
| 57 |
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
| 58 |
+
status: res.statusCode,
|
| 59 |
+
json: () => JSON.parse(data),
|
| 60 |
+
text: () => data,
|
| 61 |
+
});
|
| 62 |
+
});
|
| 63 |
+
});
|
| 64 |
+
req.on('error', reject);
|
| 65 |
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
| 66 |
+
req.end();
|
| 67 |
+
});
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function httpsPost(url, body, options = {}) {
|
| 71 |
+
return new Promise((resolve, reject) => {
|
| 72 |
+
const urlObj = new URL(url);
|
| 73 |
+
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
| 74 |
+
const req = https.request({
|
| 75 |
+
hostname: urlObj.hostname,
|
| 76 |
+
path: urlObj.pathname + urlObj.search,
|
| 77 |
+
method: 'POST',
|
| 78 |
+
headers: {
|
| 79 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
| 80 |
+
'Accept': 'application/json, text/plain, */*',
|
| 81 |
+
'Content-Type': 'application/json',
|
| 82 |
+
'Content-Length': Buffer.byteLength(bodyStr),
|
| 83 |
+
...options.headers,
|
| 84 |
+
},
|
| 85 |
+
timeout: options.timeout || 30000,
|
| 86 |
+
}, (res) => {
|
| 87 |
+
let data = '';
|
| 88 |
+
res.on('data', chunk => data += chunk);
|
| 89 |
+
res.on('end', () => {
|
| 90 |
+
resolve({
|
| 91 |
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
| 92 |
+
status: res.statusCode,
|
| 93 |
+
json: () => JSON.parse(data),
|
| 94 |
+
text: () => data,
|
| 95 |
+
});
|
| 96 |
+
});
|
| 97 |
+
});
|
| 98 |
+
req.on('error', reject);
|
| 99 |
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
| 100 |
+
req.write(bodyStr);
|
| 101 |
+
req.end();
|
| 102 |
+
});
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
function httpsRequest(method, hostname, pathStr, body, extraHeaders = {}) {
|
| 106 |
+
return new Promise((resolve, reject) => {
|
| 107 |
+
const reqBody = body ? JSON.stringify(body) : null;
|
| 108 |
+
const req = https.request({
|
| 109 |
+
hostname,
|
| 110 |
+
path: pathStr,
|
| 111 |
+
method,
|
| 112 |
+
headers: {
|
| 113 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
| 114 |
+
'Accept': 'application/json',
|
| 115 |
+
...(reqBody ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(reqBody) } : {}),
|
| 116 |
+
...extraHeaders,
|
| 117 |
+
},
|
| 118 |
+
timeout: 15000,
|
| 119 |
+
}, (res) => {
|
| 120 |
+
let data = '';
|
| 121 |
+
res.on('data', c => data += c);
|
| 122 |
+
res.on('end', () => {
|
| 123 |
+
let parsed;
|
| 124 |
+
try { parsed = JSON.parse(data); } catch { parsed = data; }
|
| 125 |
+
resolve({
|
| 126 |
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
| 127 |
+
status: res.statusCode,
|
| 128 |
+
data: parsed,
|
| 129 |
+
});
|
| 130 |
+
});
|
| 131 |
+
});
|
| 132 |
+
req.on('error', reject);
|
| 133 |
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
| 134 |
+
if (reqBody) req.write(reqBody);
|
| 135 |
+
req.end();
|
| 136 |
+
});
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/**
|
| 140 |
+
* 从文本中提取验证码
|
| 141 |
+
* chataibot.pro 的 token 就是 6 位数字验证码
|
| 142 |
+
*/
|
| 143 |
+
export function extractVerificationCode(text) {
|
| 144 |
+
if (!text) return null;
|
| 145 |
+
if (typeof text !== 'string') text = String(text);
|
| 146 |
+
|
| 147 |
+
// chataibot.pro 特有格式: "Your code: 528135"
|
| 148 |
+
const codeMatch = text.match(/(?:Your\s+code|code)[::\s]*(\d{4,8})/i);
|
| 149 |
+
if (codeMatch) return codeMatch[1];
|
| 150 |
+
|
| 151 |
+
// URL 中的 token 参数 (token=528135)
|
| 152 |
+
const tokenMatch = text.match(/[?&]token=(\d{4,8})/);
|
| 153 |
+
if (tokenMatch) return tokenMatch[1];
|
| 154 |
+
|
| 155 |
+
// 通用验证码模式
|
| 156 |
+
const patterns = [
|
| 157 |
+
/验证码[::]\s*(\d{6})/,
|
| 158 |
+
/verification\s*code[::\s]*(\d{6})/i,
|
| 159 |
+
/(?:confirm|код|подтвержд)[^0-9]{0,30}(\d{4,8})/i,
|
| 160 |
+
];
|
| 161 |
+
for (const p of patterns) {
|
| 162 |
+
const m = text.match(p);
|
| 163 |
+
if (m) return m[1];
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// 最后兜底: 独立的6位数字,但排除 DKIM/邮件头中的 hex 序列
|
| 167 |
+
// 只在短文本(非原始 MIME)中使用,避免误匹配
|
| 168 |
+
if (text.length < 500) {
|
| 169 |
+
const m = text.match(/\b(\d{6})\b/);
|
| 170 |
+
if (m) return m[1];
|
| 171 |
+
}
|
| 172 |
+
return null;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// ==================== Provider 基类 ====================
|
| 176 |
+
|
| 177 |
+
class MailProvider {
|
| 178 |
+
constructor(options = {}) {
|
| 179 |
+
this.address = null;
|
| 180 |
+
this.sessionStartTime = null;
|
| 181 |
+
this.options = options;
|
| 182 |
+
}
|
| 183 |
+
async createInbox() { throw new Error('Not implemented'); }
|
| 184 |
+
async fetchVerificationCode(senderFilter, pollOptions) { throw new Error('Not implemented'); }
|
| 185 |
+
canAutoVerify() { return false; }
|
| 186 |
+
async cleanup() {}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// ==================== MoeMail Provider ====================
|
| 190 |
+
|
| 191 |
+
class MoeMailProvider extends MailProvider {
|
| 192 |
+
constructor(options = {}) {
|
| 193 |
+
super(options);
|
| 194 |
+
this.apiUrl = options.apiUrl || '';
|
| 195 |
+
this.apiKey = options.apiKey || '';
|
| 196 |
+
this.domain = options.domain || '';
|
| 197 |
+
this.prefix = options.prefix || '';
|
| 198 |
+
this.randomLength = options.randomLength || 6;
|
| 199 |
+
this.duration = options.duration || 0;
|
| 200 |
+
this.emailId = null;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
async createInbox() {
|
| 204 |
+
if (!this.apiUrl || !this.apiKey || !this.domain) {
|
| 205 |
+
throw new Error('MoeMail 未配置完整: 需要 apiUrl, apiKey, domain');
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
const randomPart = crypto.randomBytes(this.randomLength).toString('hex').substring(0, this.randomLength);
|
| 209 |
+
const name = this.prefix ? `${this.prefix}${randomPart}` : randomPart;
|
| 210 |
+
|
| 211 |
+
const resp = await httpsPost(`${this.apiUrl}/api/emails/generate`, {
|
| 212 |
+
name,
|
| 213 |
+
domain: this.domain,
|
| 214 |
+
expiryTime: this.duration,
|
| 215 |
+
}, {
|
| 216 |
+
headers: { 'X-API-Key': this.apiKey },
|
| 217 |
+
});
|
| 218 |
+
|
| 219 |
+
if (!resp.ok) {
|
| 220 |
+
throw new Error(`MoeMail 创建邮箱失败: ${resp.status} ${resp.text()}`);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
const data = resp.json();
|
| 224 |
+
this.emailId = data.id || data.data?.id;
|
| 225 |
+
this.address = data.address || data.data?.address || `${name}@${this.domain}`;
|
| 226 |
+
this.sessionStartTime = Date.now();
|
| 227 |
+
|
| 228 |
+
console.log(` [MoeMail] 邮箱: ${this.address}`);
|
| 229 |
+
return this.address;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
canAutoVerify() { return true; }
|
| 233 |
+
|
| 234 |
+
async fetchVerificationCode(senderFilter, pollOptions = {}) {
|
| 235 |
+
if (!this.emailId) return null;
|
| 236 |
+
|
| 237 |
+
const { initialDelay = 10000, maxAttempts = 15, pollInterval = 5000 } = pollOptions;
|
| 238 |
+
|
| 239 |
+
console.log(` [MoeMail] 等待邮件 (${initialDelay / 1000}s)...`);
|
| 240 |
+
await sleep(initialDelay);
|
| 241 |
+
|
| 242 |
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
| 243 |
+
try {
|
| 244 |
+
const resp = await httpsGet(`${this.apiUrl}/api/emails/${this.emailId}`, {
|
| 245 |
+
headers: { 'X-API-Key': this.apiKey },
|
| 246 |
+
});
|
| 247 |
+
if (!resp.ok) { await sleep(pollInterval); continue; }
|
| 248 |
+
|
| 249 |
+
const data = resp.json();
|
| 250 |
+
const messages = data.messages || data.data?.messages || [];
|
| 251 |
+
|
| 252 |
+
for (const msg of messages) {
|
| 253 |
+
const from = msg.from || msg.sender || '';
|
| 254 |
+
if (senderFilter && !from.toLowerCase().includes(senderFilter.toLowerCase())) continue;
|
| 255 |
+
|
| 256 |
+
const code = extractVerificationCode(msg.subject) ||
|
| 257 |
+
extractVerificationCode(msg.content) ||
|
| 258 |
+
extractVerificationCode(msg.html) ||
|
| 259 |
+
extractVerificationCode(msg.text);
|
| 260 |
+
if (code) {
|
| 261 |
+
console.log(` [MoeMail] 验证码: ${code}`);
|
| 262 |
+
return code;
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
console.log(` [MoeMail] 轮询 ${attempt}/${maxAttempts}: ${messages.length} 封邮件,未提取到验证码`);
|
| 267 |
+
} catch (e) {
|
| 268 |
+
console.log(` [MoeMail] 轮询 ${attempt}/${maxAttempts} 出错: ${e.message}`);
|
| 269 |
+
}
|
| 270 |
+
await sleep(pollInterval);
|
| 271 |
+
}
|
| 272 |
+
return null;
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
// ==================== GPTMail Provider ====================
|
| 277 |
+
|
| 278 |
+
const BAD_TLDS = ['.shop', '.top', '.xyz', '.buzz', '.click', '.icu', '.club', '.fun', '.site', '.online', '.store', '.live', '.rest', '.cc', '.tk', '.ml', '.ga', '.cf', '.gq', '.cn', '.pw'];
|
| 279 |
+
const GOOD_TLDS = ['.org', '.org.uk', '.co.uk', '.net', '.com'];
|
| 280 |
+
|
| 281 |
+
function isGoodDomain(domain) {
|
| 282 |
+
const lower = domain.toLowerCase();
|
| 283 |
+
if (BAD_TLDS.some(tld => lower.endsWith(tld))) return false;
|
| 284 |
+
const name = lower.split('.')[0];
|
| 285 |
+
if (name.length <= 7) {
|
| 286 |
+
const vowels = (name.match(/[aeiou]/g) || []).length;
|
| 287 |
+
if (vowels <= 1) return false;
|
| 288 |
+
}
|
| 289 |
+
return true;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
class GPTMailProvider extends MailProvider {
|
| 293 |
+
constructor(options = {}) {
|
| 294 |
+
super(options);
|
| 295 |
+
this.apiBase = options.apiBase || 'https://mail.chatgpt.org.uk';
|
| 296 |
+
this.apiKey = options.apiKey || 'gpt-test';
|
| 297 |
+
this.fallbackKey = options.fallbackKey || '';
|
| 298 |
+
this.usingFallback = false;
|
| 299 |
+
this.address = null;
|
| 300 |
+
this.token = null;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
_currentKey() {
|
| 304 |
+
return this.usingFallback ? this.fallbackKey : this.apiKey;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
_switchToFallback() {
|
| 308 |
+
if (this.fallbackKey && !this.usingFallback) {
|
| 309 |
+
this.usingFallback = true;
|
| 310 |
+
console.log(` [GPTMail] 主 Key 配额用完,切换备用 Key`);
|
| 311 |
+
return true;
|
| 312 |
+
}
|
| 313 |
+
return false;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
async _pickGoodEmail() {
|
| 317 |
+
const candidates = [];
|
| 318 |
+
const tasks = Array.from({ length: 8 }, () =>
|
| 319 |
+
httpsGet(`${this.apiBase}/api/generate-email`, {
|
| 320 |
+
headers: { 'X-API-Key': this._currentKey() },
|
| 321 |
+
}).catch(() => null)
|
| 322 |
+
);
|
| 323 |
+
const results = await Promise.allSettled(tasks);
|
| 324 |
+
let quotaHit = false;
|
| 325 |
+
|
| 326 |
+
for (const r of results) {
|
| 327 |
+
if (r.status !== 'fulfilled' || !r.value) continue;
|
| 328 |
+
const resp = r.value;
|
| 329 |
+
if (resp.status === 429 || !resp.ok) { quotaHit = true; continue; }
|
| 330 |
+
try {
|
| 331 |
+
const data = resp.json();
|
| 332 |
+
if (data.success && data.data?.email) {
|
| 333 |
+
candidates.push({ email: data.data.email, token: data.data.token });
|
| 334 |
+
}
|
| 335 |
+
} catch {}
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
if (candidates.length === 0 && quotaHit && this._switchToFallback()) {
|
| 339 |
+
const retryTasks = Array.from({ length: 8 }, () =>
|
| 340 |
+
httpsGet(`${this.apiBase}/api/generate-email`, {
|
| 341 |
+
headers: { 'X-API-Key': this._currentKey() },
|
| 342 |
+
}).catch(() => null)
|
| 343 |
+
);
|
| 344 |
+
const retryResults = await Promise.allSettled(retryTasks);
|
| 345 |
+
for (const r of retryResults) {
|
| 346 |
+
if (r.status !== 'fulfilled' || !r.value) continue;
|
| 347 |
+
try {
|
| 348 |
+
const data = r.value.json();
|
| 349 |
+
if (data.success && data.data?.email) {
|
| 350 |
+
candidates.push({ email: data.data.email, token: data.data.token });
|
| 351 |
+
}
|
| 352 |
+
} catch {}
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
candidates.sort((a, b) => {
|
| 357 |
+
const aDomain = a.email.split('@')[1];
|
| 358 |
+
const bDomain = b.email.split('@')[1];
|
| 359 |
+
const aG = isGoodDomain(aDomain), bG = isGoodDomain(bDomain);
|
| 360 |
+
if (aG && !bG) return -1;
|
| 361 |
+
if (!aG && bG) return 1;
|
| 362 |
+
const aP = GOOD_TLDS.findIndex(t => aDomain.endsWith(t));
|
| 363 |
+
const bP = GOOD_TLDS.findIndex(t => bDomain.endsWith(t));
|
| 364 |
+
return (aP >= 0 ? aP : 99) - (bP >= 0 ? bP : 99);
|
| 365 |
+
});
|
| 366 |
+
|
| 367 |
+
return candidates[0] || null;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
async createInbox() {
|
| 371 |
+
console.log(` [GPTMail] 创建临时邮箱...`);
|
| 372 |
+
const best = await this._pickGoodEmail();
|
| 373 |
+
|
| 374 |
+
if (best) {
|
| 375 |
+
this.address = best.email;
|
| 376 |
+
this.token = best.token;
|
| 377 |
+
this.sessionStartTime = Date.now();
|
| 378 |
+
const domain = best.email.split('@')[1];
|
| 379 |
+
console.log(` [GPTMail] 邮箱: ${this.address} (${isGoodDomain(domain) ? '优质' : '普通'}域名)`);
|
| 380 |
+
return this.address;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
throw new Error('GPTMail 创建邮箱失败: 无法获取可用邮箱');
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
canAutoVerify() { return true; }
|
| 387 |
+
|
| 388 |
+
async fetchVerificationCode(senderFilter, pollOptions = {}) {
|
| 389 |
+
const { initialDelay = 10000, maxAttempts = 20, pollInterval = 5000 } = pollOptions;
|
| 390 |
+
console.log(` [GPTMail] 等待邮件 (${initialDelay / 1000}s)...`);
|
| 391 |
+
await sleep(initialDelay);
|
| 392 |
+
|
| 393 |
+
let mailArrived = false;
|
| 394 |
+
|
| 395 |
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
| 396 |
+
try {
|
| 397 |
+
const resp = await httpsGet(
|
| 398 |
+
`${this.apiBase}/api/emails?email=${encodeURIComponent(this.address)}`,
|
| 399 |
+
{ headers: { 'X-API-Key': this._currentKey() } }
|
| 400 |
+
);
|
| 401 |
+
if (!resp.ok) {
|
| 402 |
+
console.log(` [GPTMail] 轮询 ${attempt}/${maxAttempts}: API ${resp.status}`);
|
| 403 |
+
await sleep(pollInterval);
|
| 404 |
+
continue;
|
| 405 |
+
}
|
| 406 |
+
const data = resp.json();
|
| 407 |
+
const messages = data.data?.emails || [];
|
| 408 |
+
|
| 409 |
+
for (const msg of messages) {
|
| 410 |
+
const from = msg.from_address || '';
|
| 411 |
+
if (senderFilter && !from.toLowerCase().includes(senderFilter.toLowerCase())) continue;
|
| 412 |
+
|
| 413 |
+
mailArrived = true;
|
| 414 |
+
|
| 415 |
+
// GPTMail 字段: content, html_content, subject
|
| 416 |
+
const text = [msg.subject, msg.content, msg.html_content].filter(Boolean).join(' ');
|
| 417 |
+
// 过滤掉只有空白/换行/<br> 的无效内容
|
| 418 |
+
const cleanText = text.replace(/<br\s*\/?>/gi, '').replace(/\s+/g, ' ').trim();
|
| 419 |
+
|
| 420 |
+
if (cleanText.length > 0) {
|
| 421 |
+
if (attempt <= 3) {
|
| 422 |
+
console.log(` [GPTMail] 邮件内容: ${cleanText.substring(0, 200)}`);
|
| 423 |
+
}
|
| 424 |
+
const code = extractVerificationCode(cleanText);
|
| 425 |
+
if (code) {
|
| 426 |
+
console.log(` [GPTMail] 验证码: ${code}`);
|
| 427 |
+
return code;
|
| 428 |
+
}
|
| 429 |
+
} else {
|
| 430 |
+
// 邮件到达了但内容为空 (GPTMail 解析失败),回退手动输入
|
| 431 |
+
console.log(` [GPTMail] 邮件已到达 (from: ${from}) 但内容解析为空 (raw_size: ${msg.raw_size || 0})`);
|
| 432 |
+
console.log(` [GPTMail] GPTMail 无法解析此邮件格式,请手动输入验证码`);
|
| 433 |
+
const manualCode = await prompt(' 请输入 6 位验证码 (查看邮箱或用其他方式获取) > ');
|
| 434 |
+
if (manualCode && manualCode.match(/^\d{4,8}$/)) {
|
| 435 |
+
return manualCode;
|
| 436 |
+
}
|
| 437 |
+
console.log(' 输入无效,继续轮询...');
|
| 438 |
+
}
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
if (!mailArrived && (attempt % 3 === 0)) {
|
| 442 |
+
console.log(` [GPTMail] 轮询 ${attempt}/${maxAttempts}: ${messages.length} 封邮件,等待验证邮件...`);
|
| 443 |
+
}
|
| 444 |
+
} catch (e) {
|
| 445 |
+
console.log(` [GPTMail] 轮询 ${attempt}/${maxAttempts} 出错: ${e.message}`);
|
| 446 |
+
}
|
| 447 |
+
await sleep(pollInterval);
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
// 超时也给手动输入机会
|
| 451 |
+
if (mailArrived) {
|
| 452 |
+
console.log(` [GPTMail] 轮询超时,邮件已到达但无法自动提取验证码`);
|
| 453 |
+
const manualCode = await prompt(' 请输入 6 位验证码 > ');
|
| 454 |
+
if (manualCode && manualCode.match(/^\d{4,8}$/)) return manualCode;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
return null;
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
// ==================== GuerrillaMail Provider ====================
|
| 462 |
+
|
| 463 |
+
class GuerrillaProvider extends MailProvider {
|
| 464 |
+
constructor(options = {}) {
|
| 465 |
+
super(options);
|
| 466 |
+
this.apiBase = 'https://api.guerrillamail.com/ajax.php';
|
| 467 |
+
this.sidToken = null;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
async createInbox() {
|
| 471 |
+
const resp = await httpsGet(`${this.apiBase}?f=get_email_address&lang=en`);
|
| 472 |
+
if (!resp.ok) throw new Error(`GuerrillaMail 初始化失败: ${resp.status}`);
|
| 473 |
+
|
| 474 |
+
const data = resp.json();
|
| 475 |
+
this.sidToken = data.sid_token;
|
| 476 |
+
this.address = data.email_addr;
|
| 477 |
+
this.sessionStartTime = Date.now();
|
| 478 |
+
|
| 479 |
+
console.log(` [Guerrilla] 邮箱: ${this.address}`);
|
| 480 |
+
return this.address;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
canAutoVerify() { return true; }
|
| 484 |
+
|
| 485 |
+
async fetchVerificationCode(senderFilter, pollOptions = {}) {
|
| 486 |
+
if (!this.sidToken) return null;
|
| 487 |
+
|
| 488 |
+
const { initialDelay = 15000, maxAttempts = 15, pollInterval = 4000 } = pollOptions;
|
| 489 |
+
|
| 490 |
+
console.log(` [Guerrilla] 等待邮件 (${initialDelay / 1000}s)...`);
|
| 491 |
+
await sleep(initialDelay);
|
| 492 |
+
|
| 493 |
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
| 494 |
+
try {
|
| 495 |
+
const resp = await httpsGet(
|
| 496 |
+
`${this.apiBase}?f=check_email&seq=0&sid_token=${this.sidToken}`
|
| 497 |
+
);
|
| 498 |
+
if (!resp.ok) { await sleep(pollInterval); continue; }
|
| 499 |
+
|
| 500 |
+
const data = resp.json();
|
| 501 |
+
const emails = data.list || [];
|
| 502 |
+
|
| 503 |
+
for (const email of emails) {
|
| 504 |
+
const mailFrom = email.mail_from || '';
|
| 505 |
+
if (senderFilter && !mailFrom.toLowerCase().includes(senderFilter.toLowerCase())) continue;
|
| 506 |
+
|
| 507 |
+
const code = extractVerificationCode(email.mail_excerpt || email.mail_subject);
|
| 508 |
+
if (code) {
|
| 509 |
+
console.log(` [Guerrilla] 验证码: ${code}`);
|
| 510 |
+
return code;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
// 获取完整邮件内容
|
| 514 |
+
if (email.mail_id) {
|
| 515 |
+
const detailResp = await httpsGet(
|
| 516 |
+
`${this.apiBase}?f=fetch_email&email_id=${email.mail_id}&sid_token=${this.sidToken}`
|
| 517 |
+
);
|
| 518 |
+
if (detailResp.ok) {
|
| 519 |
+
const detail = detailResp.json();
|
| 520 |
+
const bodyCode = extractVerificationCode(detail.mail_body);
|
| 521 |
+
if (bodyCode) {
|
| 522 |
+
console.log(` [Guerrilla] 验证码: ${bodyCode}`);
|
| 523 |
+
return bodyCode;
|
| 524 |
+
}
|
| 525 |
+
}
|
| 526 |
+
}
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
console.log(` [Guerrilla] 轮询 ${attempt}/${maxAttempts}: ${emails.length} 封邮件,未提取到验证码`);
|
| 530 |
+
} catch (e) {
|
| 531 |
+
console.log(` [Guerrilla] 轮询 ${attempt}/${maxAttempts} 出错: ${e.message}`);
|
| 532 |
+
}
|
| 533 |
+
await sleep(pollInterval);
|
| 534 |
+
}
|
| 535 |
+
return null;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
async cleanup() {
|
| 539 |
+
if (this.sidToken) {
|
| 540 |
+
try {
|
| 541 |
+
await httpsGet(`${this.apiBase}?f=forget_me&email_addr=${this.address}&sid_token=${this.sidToken}`);
|
| 542 |
+
} catch {}
|
| 543 |
+
}
|
| 544 |
+
}
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
// ==================== TempMail.lol Provider ====================
|
| 548 |
+
|
| 549 |
+
class TempMailProvider extends MailProvider {
|
| 550 |
+
constructor(options = {}) {
|
| 551 |
+
super(options);
|
| 552 |
+
this.apiBase = 'https://api.tempmail.lol';
|
| 553 |
+
this.token = null;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
async createInbox() {
|
| 557 |
+
const resp = await httpsGet(`${this.apiBase}/generate`);
|
| 558 |
+
if (!resp.ok) throw new Error(`TempMail 创建邮箱失败: ${resp.status}`);
|
| 559 |
+
|
| 560 |
+
const data = resp.json();
|
| 561 |
+
this.address = data.address;
|
| 562 |
+
this.token = data.token;
|
| 563 |
+
this.sessionStartTime = Date.now();
|
| 564 |
+
|
| 565 |
+
console.log(` [TempMail] 邮箱: ${this.address}`);
|
| 566 |
+
return this.address;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
canAutoVerify() { return true; }
|
| 570 |
+
|
| 571 |
+
async fetchVerificationCode(senderFilter, pollOptions = {}) {
|
| 572 |
+
if (!this.token) return null;
|
| 573 |
+
|
| 574 |
+
const { initialDelay = 15000, maxAttempts = 20, pollInterval = 5000 } = pollOptions;
|
| 575 |
+
|
| 576 |
+
console.log(` [TempMail] 等待邮件 (${initialDelay / 1000}s)...`);
|
| 577 |
+
await sleep(initialDelay);
|
| 578 |
+
|
| 579 |
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
| 580 |
+
try {
|
| 581 |
+
const resp = await httpsGet(`${this.apiBase}/auth/${this.token}`);
|
| 582 |
+
if (!resp.ok) { await sleep(pollInterval); continue; }
|
| 583 |
+
|
| 584 |
+
const data = resp.json();
|
| 585 |
+
const emails = data.email || [];
|
| 586 |
+
|
| 587 |
+
for (const email of emails) {
|
| 588 |
+
const from = email.from || '';
|
| 589 |
+
if (senderFilter && !from.toLowerCase().includes(senderFilter.toLowerCase())) continue;
|
| 590 |
+
|
| 591 |
+
const code = extractVerificationCode(email.subject) ||
|
| 592 |
+
extractVerificationCode(email.body) ||
|
| 593 |
+
extractVerificationCode(email.html);
|
| 594 |
+
if (code) {
|
| 595 |
+
console.log(` [TempMail] 验证码: ${code}`);
|
| 596 |
+
return code;
|
| 597 |
+
}
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
console.log(` [TempMail] 轮询 ${attempt}/${maxAttempts}: ${emails.length} 封邮件,未提取到验证码`);
|
| 601 |
+
} catch (e) {
|
| 602 |
+
console.log(` [TempMail] 轮询 ${attempt}/${maxAttempts} 出错: ${e.message}`);
|
| 603 |
+
}
|
| 604 |
+
await sleep(pollInterval);
|
| 605 |
+
}
|
| 606 |
+
return null;
|
| 607 |
+
}
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
// ==================== Mail.tm Provider ====================
|
| 611 |
+
|
| 612 |
+
class MailTmProvider extends MailProvider {
|
| 613 |
+
constructor(options = {}) {
|
| 614 |
+
super(options);
|
| 615 |
+
this.apiHost = 'api.mail.tm';
|
| 616 |
+
this.jwtToken = null;
|
| 617 |
+
this.accountId = null;
|
| 618 |
+
this.accountPassword = null;
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
async createInbox() {
|
| 622 |
+
// 1. 获取可用域名
|
| 623 |
+
const domainsResp = await httpsRequest('GET', this.apiHost, '/domains');
|
| 624 |
+
if (!domainsResp.ok) throw new Error(`Mail.tm 获取域名失败: ${domainsResp.status}`);
|
| 625 |
+
const domains = domainsResp.data['hydra:member'] || domainsResp.data || [];
|
| 626 |
+
if (!domains?.length) throw new Error('Mail.tm 无可用域名');
|
| 627 |
+
const domain = domains[0].domain;
|
| 628 |
+
|
| 629 |
+
// 2. 创建邮箱账号
|
| 630 |
+
const user = 'ik' + crypto.randomBytes(6).toString('hex');
|
| 631 |
+
const email = `${user}@${domain}`;
|
| 632 |
+
this.accountPassword = crypto.randomBytes(12).toString('base64url');
|
| 633 |
+
|
| 634 |
+
const createResp = await httpsRequest('POST', this.apiHost, '/accounts', {
|
| 635 |
+
address: email,
|
| 636 |
+
password: this.accountPassword,
|
| 637 |
+
});
|
| 638 |
+
if (createResp.status !== 201) throw new Error(`Mail.tm 创建账号失败: ${createResp.status}`);
|
| 639 |
+
this.accountId = createResp.data.id;
|
| 640 |
+
|
| 641 |
+
// 3. 登录获取 JWT
|
| 642 |
+
const loginResp = await httpsRequest('POST', this.apiHost, '/token', {
|
| 643 |
+
address: email,
|
| 644 |
+
password: this.accountPassword,
|
| 645 |
+
});
|
| 646 |
+
if (!loginResp.ok) throw new Error(`Mail.tm 登录失败: ${loginResp.status}`);
|
| 647 |
+
this.jwtToken = loginResp.data.token;
|
| 648 |
+
|
| 649 |
+
this.address = email;
|
| 650 |
+
this.sessionStartTime = Date.now();
|
| 651 |
+
|
| 652 |
+
console.log(` [Mail.tm] 邮箱: ${this.address}`);
|
| 653 |
+
return this.address;
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
canAutoVerify() { return true; }
|
| 657 |
+
|
| 658 |
+
async fetchVerificationCode(senderFilter, pollOptions = {}) {
|
| 659 |
+
if (!this.jwtToken) return null;
|
| 660 |
+
|
| 661 |
+
const { initialDelay = 15000, maxAttempts = 20, pollInterval = 5000 } = pollOptions;
|
| 662 |
+
|
| 663 |
+
console.log(` [Mail.tm] 等待邮件 (${initialDelay / 1000}s)...`);
|
| 664 |
+
await sleep(initialDelay);
|
| 665 |
+
|
| 666 |
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
| 667 |
+
try {
|
| 668 |
+
const resp = await httpsRequest('GET', this.apiHost, '/messages', null, {
|
| 669 |
+
'Authorization': `Bearer ${this.jwtToken}`,
|
| 670 |
+
});
|
| 671 |
+
if (!resp.ok) { await sleep(pollInterval); continue; }
|
| 672 |
+
|
| 673 |
+
const messages = resp.data['hydra:member'] || resp.data || [];
|
| 674 |
+
|
| 675 |
+
for (const msg of messages) {
|
| 676 |
+
const from = msg.from?.address || '';
|
| 677 |
+
if (senderFilter && !from.toLowerCase().includes(senderFilter.toLowerCase())) continue;
|
| 678 |
+
|
| 679 |
+
const code = extractVerificationCode(msg.subject || '');
|
| 680 |
+
if (code) {
|
| 681 |
+
console.log(` [Mail.tm] 验证码: ${code}`);
|
| 682 |
+
return code;
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
// 获取邮件详情
|
| 686 |
+
if (msg.id) {
|
| 687 |
+
const detailResp = await httpsRequest('GET', this.apiHost, `/messages/${msg.id}`, null, {
|
| 688 |
+
'Authorization': `Bearer ${this.jwtToken}`,
|
| 689 |
+
});
|
| 690 |
+
if (detailResp.ok) {
|
| 691 |
+
// Mail.tm html 字段可能是数组而非字符串
|
| 692 |
+
let html = detailResp.data.html || '';
|
| 693 |
+
if (Array.isArray(html)) html = html.join('');
|
| 694 |
+
const body = detailResp.data.text || html;
|
| 695 |
+
const bodyCode = extractVerificationCode(body);
|
| 696 |
+
if (bodyCode) {
|
| 697 |
+
console.log(` [Mail.tm] 验证码: ${bodyCode}`);
|
| 698 |
+
return bodyCode;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
// 如果详情解析失败,尝试 /sources 获取原始 MIME
|
| 702 |
+
const srcResp = await httpsRequest('GET', this.apiHost, `/sources/${msg.id}`, null, {
|
| 703 |
+
'Authorization': `Bearer ${this.jwtToken}`,
|
| 704 |
+
});
|
| 705 |
+
if (srcResp.ok) {
|
| 706 |
+
const rawMime = srcResp.data?.data || srcResp.raw || '';
|
| 707 |
+
const mimeCode = extractVerificationCode(rawMime);
|
| 708 |
+
if (mimeCode) {
|
| 709 |
+
console.log(` [Mail.tm] 验证码 (from MIME): ${mimeCode}`);
|
| 710 |
+
return mimeCode;
|
| 711 |
+
}
|
| 712 |
+
}
|
| 713 |
+
}
|
| 714 |
+
}
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
console.log(` [Mail.tm] 轮询 ${attempt}/${maxAttempts}: ${messages.length} 封邮件,未提取到验证码`);
|
| 718 |
+
} catch (e) {
|
| 719 |
+
console.log(` [Mail.tm] 轮询 ${attempt}/${maxAttempts} 出错: ${e.message}`);
|
| 720 |
+
}
|
| 721 |
+
await sleep(pollInterval);
|
| 722 |
+
}
|
| 723 |
+
return null;
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
async cleanup() {
|
| 727 |
+
if (this.jwtToken && this.accountId) {
|
| 728 |
+
try {
|
| 729 |
+
await httpsRequest('DELETE', this.apiHost, `/accounts/${this.accountId}`, null, {
|
| 730 |
+
'Authorization': `Bearer ${this.jwtToken}`,
|
| 731 |
+
});
|
| 732 |
+
} catch {}
|
| 733 |
+
}
|
| 734 |
+
}
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
// ==================== Dropmail Provider ====================
|
| 738 |
+
// 优势: 支持 raw 原始 MIME 邮件,可自行解析绕过服务端解析问题
|
| 739 |
+
|
| 740 |
+
class DropmailProvider extends MailProvider {
|
| 741 |
+
constructor(options = {}) {
|
| 742 |
+
super(options);
|
| 743 |
+
this.apiUrl = 'https://dropmail.me/api/graphql/web-test-2';
|
| 744 |
+
this.sessionId = null;
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
async _gql(query) {
|
| 748 |
+
const resp = await httpsPost(this.apiUrl, { query });
|
| 749 |
+
if (!resp.ok) throw new Error(`Dropmail API ${resp.status}`);
|
| 750 |
+
return resp.json();
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
async createInbox() {
|
| 754 |
+
const data = await this._gql('mutation{introduceSession{id,expiresAt,addresses{address}}}');
|
| 755 |
+
const session = data.data.introduceSession;
|
| 756 |
+
this.sessionId = session.id;
|
| 757 |
+
this.address = session.addresses[0].address;
|
| 758 |
+
this.sessionStartTime = Date.now();
|
| 759 |
+
|
| 760 |
+
console.log(` [Dropmail] 邮箱: ${this.address}`);
|
| 761 |
+
return this.address;
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
canAutoVerify() { return true; }
|
| 765 |
+
|
| 766 |
+
async fetchVerificationCode(senderFilter, pollOptions = {}) {
|
| 767 |
+
if (!this.sessionId) return null;
|
| 768 |
+
|
| 769 |
+
const { initialDelay = 10000, maxAttempts = 15, pollInterval = 5000 } = pollOptions;
|
| 770 |
+
|
| 771 |
+
console.log(` [Dropmail] 等待邮件 (${initialDelay / 1000}s)...`);
|
| 772 |
+
await sleep(initialDelay);
|
| 773 |
+
|
| 774 |
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
| 775 |
+
try {
|
| 776 |
+
const sid = this.sessionId.replace(/"/g, '\\"');
|
| 777 |
+
const data = await this._gql(
|
| 778 |
+
`query{session(id:"${sid}"){mails{rawSize,fromAddr,headerSubject,text,html,raw}}}`
|
| 779 |
+
);
|
| 780 |
+
const mails = data.data?.session?.mails || [];
|
| 781 |
+
|
| 782 |
+
for (const mail of mails) {
|
| 783 |
+
const from = mail.fromAddr || '';
|
| 784 |
+
if (senderFilter && !from.toLowerCase().includes(senderFilter.toLowerCase())) continue;
|
| 785 |
+
|
| 786 |
+
// 优先从解析好的字段提取
|
| 787 |
+
const parsed = [mail.headerSubject, mail.text, mail.html].filter(Boolean).join(' ');
|
| 788 |
+
const cleanParsed = parsed.replace(/<br\s*\/?>/gi, '').replace(/\s+/g, ' ').trim();
|
| 789 |
+
if (cleanParsed.length > 5) {
|
| 790 |
+
const code = extractVerificationCode(cleanParsed);
|
| 791 |
+
if (code) {
|
| 792 |
+
console.log(` [Dropmail] 验证码: ${code}`);
|
| 793 |
+
return code;
|
| 794 |
+
}
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
// 解析好的字段为空时,从 raw MIME 原文自行解析
|
| 798 |
+
if (mail.raw && mail.rawSize > 100) {
|
| 799 |
+
console.log(` [Dropmail] 解析好的字段为空,尝试 raw MIME 解析 (${mail.rawSize} bytes)...`);
|
| 800 |
+
const code = this._parseRawMime(mail.raw);
|
| 801 |
+
if (code) {
|
| 802 |
+
console.log(` [Dropmail] 验证码 (from raw): ${code}`);
|
| 803 |
+
return code;
|
| 804 |
+
}
|
| 805 |
+
// raw 解析也失败,说明邮件内容确实为空(临时邮箱被反制)
|
| 806 |
+
console.log(` [Dropmail] 邮件内容为空(chataibot 对临时邮箱发送空内容)`);
|
| 807 |
+
console.log(` [Dropmail] 请手动输入验证码`);
|
| 808 |
+
const manualCode = await prompt(' 请输入 6 位验证码 > ');
|
| 809 |
+
if (manualCode && manualCode.match(/^\d{4,8}$/)) return manualCode;
|
| 810 |
+
console.log(' 输入无效,继续轮询...');
|
| 811 |
+
}
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
console.log(` [Dropmail] 轮询 ${attempt}/${maxAttempts}: ${mails.length} 封邮件`);
|
| 815 |
+
} catch (e) {
|
| 816 |
+
console.log(` [Dropmail] 轮询 ${attempt}/${maxAttempts} 出错: ${e.message}`);
|
| 817 |
+
}
|
| 818 |
+
await sleep(pollInterval);
|
| 819 |
+
}
|
| 820 |
+
return null;
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
/** 手动解析 raw MIME 邮件提取��证码 */
|
| 824 |
+
_parseRawMime(raw) {
|
| 825 |
+
// 提取所有 MIME 部分
|
| 826 |
+
const boundaryMatch = raw.match(/boundary="?([^"\r\n]+)"?/);
|
| 827 |
+
if (!boundaryMatch) {
|
| 828 |
+
// 无 boundary,直接整体提取
|
| 829 |
+
return extractVerificationCode(this._decodeMimePart(raw));
|
| 830 |
+
}
|
| 831 |
+
const boundary = boundaryMatch[1];
|
| 832 |
+
const parts = raw.split('--' + boundary);
|
| 833 |
+
|
| 834 |
+
for (const part of parts) {
|
| 835 |
+
// 跳过 preamble 和 epilogue
|
| 836 |
+
if (part.startsWith('--')) continue;
|
| 837 |
+
|
| 838 |
+
const decoded = this._decodeMimePart(part);
|
| 839 |
+
if (decoded) {
|
| 840 |
+
const code = extractVerificationCode(decoded);
|
| 841 |
+
if (code) return code;
|
| 842 |
+
}
|
| 843 |
+
}
|
| 844 |
+
return null;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
/** 解码单个 MIME 部分 (处理 base64 / quoted-printable) */
|
| 848 |
+
_decodeMimePart(part) {
|
| 849 |
+
// 分离 header 和 body
|
| 850 |
+
const headerEnd = part.indexOf('\r\n\r\n');
|
| 851 |
+
if (headerEnd < 0) return part;
|
| 852 |
+
|
| 853 |
+
const header = part.substring(0, headerEnd).toLowerCase();
|
| 854 |
+
let body = part.substring(headerEnd + 4);
|
| 855 |
+
|
| 856 |
+
// 检查 Content-Transfer-Encoding
|
| 857 |
+
if (header.includes('base64')) {
|
| 858 |
+
try {
|
| 859 |
+
// 去除换行后 base64 解码
|
| 860 |
+
const cleaned = body.replace(/[\r\n\s]/g, '');
|
| 861 |
+
body = Buffer.from(cleaned, 'base64').toString('utf-8');
|
| 862 |
+
} catch {}
|
| 863 |
+
} else if (header.includes('quoted-printable')) {
|
| 864 |
+
body = body
|
| 865 |
+
.replace(/=\r?\n/g, '') // soft line break
|
| 866 |
+
.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
return body;
|
| 870 |
+
}
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
// ==================== TempMailIO Provider ====================
|
| 874 |
+
// temp-mail.io - 免费临时邮箱,无需注册
|
| 875 |
+
|
| 876 |
+
class TempMailIOProvider extends MailProvider {
|
| 877 |
+
constructor(options = {}) {
|
| 878 |
+
super(options);
|
| 879 |
+
this.apiBase = 'https://api.internal.temp-mail.io/api/v3';
|
| 880 |
+
this.token = null;
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
async createInbox() {
|
| 884 |
+
const resp = await httpsPost(`${this.apiBase}/email/new`, {});
|
| 885 |
+
if (!resp.ok) throw new Error(`temp-mail.io 创建邮箱失败: ${resp.status}`);
|
| 886 |
+
|
| 887 |
+
const data = resp.json();
|
| 888 |
+
this.address = data.email;
|
| 889 |
+
this.token = data.token;
|
| 890 |
+
this.sessionStartTime = Date.now();
|
| 891 |
+
|
| 892 |
+
console.log(` [TempMailIO] 邮箱: ${this.address}`);
|
| 893 |
+
return this.address;
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
canAutoVerify() { return true; }
|
| 897 |
+
|
| 898 |
+
async fetchVerificationCode(senderFilter, pollOptions = {}) {
|
| 899 |
+
if (!this.address) return null;
|
| 900 |
+
|
| 901 |
+
const { initialDelay = 10000, maxAttempts = 15, pollInterval = 5000 } = pollOptions;
|
| 902 |
+
|
| 903 |
+
console.log(` [TempMailIO] 等待邮件 (${initialDelay / 1000}s)...`);
|
| 904 |
+
await sleep(initialDelay);
|
| 905 |
+
|
| 906 |
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
| 907 |
+
try {
|
| 908 |
+
const resp = await httpsGet(`${this.apiBase}/email/${encodeURIComponent(this.address)}/messages`);
|
| 909 |
+
if (!resp.ok) { await sleep(pollInterval); continue; }
|
| 910 |
+
|
| 911 |
+
const messages = resp.json();
|
| 912 |
+
|
| 913 |
+
for (const msg of (Array.isArray(messages) ? messages : [])) {
|
| 914 |
+
const from = msg.from || '';
|
| 915 |
+
if (senderFilter && !from.toLowerCase().includes(senderFilter.toLowerCase())) continue;
|
| 916 |
+
|
| 917 |
+
const text = [msg.subject, msg.body_text, msg.body_html].filter(Boolean).join(' ');
|
| 918 |
+
const clean = text.replace(/<br\s*\/?>/gi, '').replace(/\s+/g, ' ').trim();
|
| 919 |
+
|
| 920 |
+
if (clean.length > 5) {
|
| 921 |
+
const code = extractVerificationCode(clean);
|
| 922 |
+
if (code) {
|
| 923 |
+
console.log(` [TempMailIO] 验证码: ${code}`);
|
| 924 |
+
return code;
|
| 925 |
+
}
|
| 926 |
+
}
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
console.log(` [TempMailIO] 轮询 ${attempt}/${maxAttempts}: ${Array.isArray(messages) ? messages.length : 0} 封邮件`);
|
| 930 |
+
} catch (e) {
|
| 931 |
+
console.log(` [TempMailIO] 轮询 ${attempt}/${maxAttempts} 出错: ${e.message}`);
|
| 932 |
+
}
|
| 933 |
+
await sleep(pollInterval);
|
| 934 |
+
}
|
| 935 |
+
return null;
|
| 936 |
+
}
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
// ==================== LinShiYou Provider ====================
|
| 940 |
+
// linshiyou.com (临时邮) — PHP session 驱动,支持 @linshiyou.com 和 @youxiang.dev
|
| 941 |
+
// 已验证可以收到 chataibot.pro 完整验证邮件 (含验证码)
|
| 942 |
+
|
| 943 |
+
class LinShiYouProvider extends MailProvider {
|
| 944 |
+
constructor(options = {}) {
|
| 945 |
+
super(options);
|
| 946 |
+
this.baseUrl = 'https://linshiyou.com';
|
| 947 |
+
this.domain = options.domain || '@linshiyou.com'; // 或 @youxiang.dev
|
| 948 |
+
this.cookie = null;
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
/** 发起带 cookie 的 GET 请求 */
|
| 952 |
+
_get(path) {
|
| 953 |
+
return new Promise((resolve, reject) => {
|
| 954 |
+
const url = new URL(this.baseUrl + path);
|
| 955 |
+
const req = https.request({
|
| 956 |
+
hostname: url.hostname,
|
| 957 |
+
path: url.pathname + url.search,
|
| 958 |
+
method: 'GET',
|
| 959 |
+
headers: {
|
| 960 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
| 961 |
+
'Accept': 'text/html, */*; q=0.01',
|
| 962 |
+
'X-Requested-With': 'XMLHttpRequest',
|
| 963 |
+
...(this.cookie ? { 'Cookie': this.cookie } : {}),
|
| 964 |
+
},
|
| 965 |
+
timeout: 15000,
|
| 966 |
+
}, (res) => {
|
| 967 |
+
// 保存 session cookie
|
| 968 |
+
const setCookies = res.headers['set-cookie'];
|
| 969 |
+
if (setCookies) {
|
| 970 |
+
const phpsessid = setCookies.find(c => c.includes('PHPSESSID'));
|
| 971 |
+
if (phpsessid) {
|
| 972 |
+
this.cookie = phpsessid.split(';')[0];
|
| 973 |
+
}
|
| 974 |
+
}
|
| 975 |
+
let data = '';
|
| 976 |
+
res.on('data', c => data += c);
|
| 977 |
+
res.on('end', () => resolve({ status: res.statusCode, data, ok: res.statusCode >= 200 && res.statusCode < 300 }));
|
| 978 |
+
});
|
| 979 |
+
req.on('error', reject);
|
| 980 |
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
| 981 |
+
req.end();
|
| 982 |
+
});
|
| 983 |
+
}
|
| 984 |
+
|
| 985 |
+
async createInbox() {
|
| 986 |
+
const prefix = 'ikun' + crypto.randomBytes(4).toString('hex');
|
| 987 |
+
const email = prefix + this.domain;
|
| 988 |
+
|
| 989 |
+
// 先访问主页获取 session
|
| 990 |
+
await this._get('/');
|
| 991 |
+
|
| 992 |
+
// 设置邮箱地址
|
| 993 |
+
const resp = await this._get('/user.php?user=' + encodeURIComponent(email));
|
| 994 |
+
if (!resp.ok) throw new Error(`LinShiYou 创建邮箱失败: ${resp.status}`);
|
| 995 |
+
|
| 996 |
+
this.address = resp.data.trim() || email;
|
| 997 |
+
this.sessionStartTime = Date.now();
|
| 998 |
+
|
| 999 |
+
console.log(` [LinShiYou] 邮箱: ${this.address}`);
|
| 1000 |
+
return this.address;
|
| 1001 |
+
}
|
| 1002 |
+
|
| 1003 |
+
canAutoVerify() { return true; }
|
| 1004 |
+
|
| 1005 |
+
async fetchVerificationCode(senderFilter, pollOptions = {}) {
|
| 1006 |
+
if (!this.cookie) return null;
|
| 1007 |
+
|
| 1008 |
+
const { initialDelay = 10000, maxAttempts = 20, pollInterval = 5000 } = pollOptions;
|
| 1009 |
+
|
| 1010 |
+
console.log(` [LinShiYou] 等待邮件 (${initialDelay / 1000}s)...`);
|
| 1011 |
+
await sleep(initialDelay);
|
| 1012 |
+
|
| 1013 |
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
| 1014 |
+
try {
|
| 1015 |
+
// 获取未读邮件
|
| 1016 |
+
const resp = await this._get('/mail.php?unseen=1');
|
| 1017 |
+
if (!resp.ok) { await sleep(pollInterval); continue; }
|
| 1018 |
+
|
| 1019 |
+
const html = resp.data.trim();
|
| 1020 |
+
|
| 1021 |
+
// "DIE" 表示 session 过期
|
| 1022 |
+
if (html === 'DIE') {
|
| 1023 |
+
console.log(` [LinShiYou] Session 过期`);
|
| 1024 |
+
return null;
|
| 1025 |
+
}
|
| 1026 |
+
|
| 1027 |
+
if (html.length > 0) {
|
| 1028 |
+
// 从 HTML 中提取验证码
|
| 1029 |
+
const code = extractVerificationCode(html);
|
| 1030 |
+
if (code) {
|
| 1031 |
+
console.log(` [LinShiYou] 验证码: ${code}`);
|
| 1032 |
+
return code;
|
| 1033 |
+
}
|
| 1034 |
+
}
|
| 1035 |
+
|
| 1036 |
+
// 也检查全部邮件
|
| 1037 |
+
if (attempt % 3 === 0) {
|
| 1038 |
+
const allResp = await this._get('/mail.php');
|
| 1039 |
+
if (allResp.ok && allResp.data.trim().length > 0 && allResp.data.trim() !== 'DIE') {
|
| 1040 |
+
const code = extractVerificationCode(allResp.data);
|
| 1041 |
+
if (code) {
|
| 1042 |
+
console.log(` [LinShiYou] 验证码: ${code}`);
|
| 1043 |
+
return code;
|
| 1044 |
+
}
|
| 1045 |
+
}
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
console.log(` [LinShiYou] 轮询 ${attempt}/${maxAttempts}: 等待验证邮件...`);
|
| 1049 |
+
} catch (e) {
|
| 1050 |
+
console.log(` [LinShiYou] 轮询 ${attempt}/${maxAttempts} 出错: ${e.message}`);
|
| 1051 |
+
}
|
| 1052 |
+
await sleep(pollInterval);
|
| 1053 |
+
}
|
| 1054 |
+
return null;
|
| 1055 |
+
}
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
// ==================== DuckMail Provider ====================
|
| 1059 |
+
|
| 1060 |
+
class DuckMailProvider extends MailProvider {
|
| 1061 |
+
constructor(options = {}) {
|
| 1062 |
+
super(options);
|
| 1063 |
+
this.apiBase = 'https://api.duckmail.sbs';
|
| 1064 |
+
this.apiKey = options.apiKey || '';
|
| 1065 |
+
this.domain = options.domain || '';
|
| 1066 |
+
this.password = options.password || 'Reg' + crypto.randomBytes(6).toString('hex');
|
| 1067 |
+
this.token = null;
|
| 1068 |
+
this.accountId = null;
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
async _get(path) {
|
| 1072 |
+
const auth = this.token || this.apiKey;
|
| 1073 |
+
return httpsGet(`${this.apiBase}${path}`, {
|
| 1074 |
+
headers: auth ? { 'Authorization': `Bearer ${auth}` } : {},
|
| 1075 |
+
});
|
| 1076 |
+
}
|
| 1077 |
+
|
| 1078 |
+
async _post(path, body) {
|
| 1079 |
+
const auth = this.token || this.apiKey;
|
| 1080 |
+
return httpsPost(`${this.apiBase}${path}`, body, {
|
| 1081 |
+
headers: auth ? { 'Authorization': `Bearer ${auth}` } : {},
|
| 1082 |
+
});
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
async createInbox() {
|
| 1086 |
+
// 获取域名
|
| 1087 |
+
let domain = this.domain;
|
| 1088 |
+
if (!domain) {
|
| 1089 |
+
const resp = await this._get('/domains');
|
| 1090 |
+
if (resp.ok) {
|
| 1091 |
+
const data = resp.json();
|
| 1092 |
+
const domains = data['hydra:member'] || [];
|
| 1093 |
+
const privateDomains = domains.filter(d => d.ownerId && d.isVerified);
|
| 1094 |
+
const publicDomains = domains.filter(d => !d.ownerId && d.isVerified);
|
| 1095 |
+
domain = (privateDomains[0] || publicDomains[0])?.domain;
|
| 1096 |
+
}
|
| 1097 |
+
if (!domain) throw new Error('DuckMail: 无可用域名');
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
const username = crypto.randomBytes(4).toString('hex');
|
| 1101 |
+
const address = `${username}@${domain}`;
|
| 1102 |
+
|
| 1103 |
+
const resp = await this._post('/accounts', { address, password: this.password });
|
| 1104 |
+
if (!resp.ok) throw new Error(`DuckMail 创建邮箱失败: ${resp.status}`);
|
| 1105 |
+
|
| 1106 |
+
const data = resp.json();
|
| 1107 |
+
this.accountId = data.id;
|
| 1108 |
+
this.address = data.address || address;
|
| 1109 |
+
|
| 1110 |
+
// 获取 token
|
| 1111 |
+
const tokenResp = await this._post('/token', { address: this.address, password: this.password });
|
| 1112 |
+
if (tokenResp.ok) {
|
| 1113 |
+
this.token = tokenResp.json().token;
|
| 1114 |
+
}
|
| 1115 |
+
|
| 1116 |
+
this.sessionStartTime = Date.now();
|
| 1117 |
+
console.log(` [DuckMail] 邮箱: ${this.address}`);
|
| 1118 |
+
return this.address;
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
canAutoVerify() { return true; }
|
| 1122 |
+
|
| 1123 |
+
async fetchVerificationCode(senderFilter, pollOptions = {}) {
|
| 1124 |
+
const { initialDelay = 15000, maxAttempts = 20, pollInterval = 5000 } = pollOptions;
|
| 1125 |
+
|
| 1126 |
+
console.log(` [DuckMail] 等待邮件 (${initialDelay / 1000}s)...`);
|
| 1127 |
+
await sleep(initialDelay);
|
| 1128 |
+
|
| 1129 |
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
| 1130 |
+
try {
|
| 1131 |
+
const resp = await this._get('/messages');
|
| 1132 |
+
if (!resp.ok) { await sleep(pollInterval); continue; }
|
| 1133 |
+
|
| 1134 |
+
const data = resp.json();
|
| 1135 |
+
const messages = data['hydra:member'] || [];
|
| 1136 |
+
|
| 1137 |
+
for (const msg of messages) {
|
| 1138 |
+
const from = msg.from?.address || '';
|
| 1139 |
+
if (senderFilter && !from.toLowerCase().includes(senderFilter.toLowerCase())) continue;
|
| 1140 |
+
|
| 1141 |
+
// 获取邮件详情
|
| 1142 |
+
const detailResp = await this._get(`/messages/${msg.id}`);
|
| 1143 |
+
if (!detailResp.ok) continue;
|
| 1144 |
+
|
| 1145 |
+
const detail = detailResp.json();
|
| 1146 |
+
const body = detail.text || (Array.isArray(detail.html) ? detail.html.join('') : detail.html) || '';
|
| 1147 |
+
|
| 1148 |
+
const code = extractVerificationCode(detail.subject) || extractVerificationCode(body);
|
| 1149 |
+
if (code) {
|
| 1150 |
+
console.log(` [DuckMail] 验证码: ${code}`);
|
| 1151 |
+
return code;
|
| 1152 |
+
}
|
| 1153 |
+
}
|
| 1154 |
+
|
| 1155 |
+
console.log(` [DuckMail] 轮询 ${attempt}/${maxAttempts}: ${messages.length} 封邮件,未提取到验证码`);
|
| 1156 |
+
} catch (e) {
|
| 1157 |
+
console.log(` [DuckMail] 轮询 ${attempt}/${maxAttempts} 出错: ${e.message}`);
|
| 1158 |
+
}
|
| 1159 |
+
await sleep(pollInterval);
|
| 1160 |
+
}
|
| 1161 |
+
return null;
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
async cleanup() {
|
| 1165 |
+
if (this.accountId && this.token) {
|
| 1166 |
+
try { await this._get(`/accounts/${this.accountId}`); } catch {}
|
| 1167 |
+
}
|
| 1168 |
+
}
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
// ==================== Catch-All 域名邮箱 Provider ====================
|
| 1172 |
+
|
| 1173 |
+
class CatchAllProvider extends MailProvider {
|
| 1174 |
+
constructor(options = {}) {
|
| 1175 |
+
super(options);
|
| 1176 |
+
this.domain = options.domain || '';
|
| 1177 |
+
this.prefix = options.prefix || 'ikun';
|
| 1178 |
+
this.verifyMethod = options.verifyMethod || 'manual';
|
| 1179 |
+
this.workerUrl = options.workerUrl || '';
|
| 1180 |
+
this.workerSecret = options.workerSecret || '';
|
| 1181 |
+
}
|
| 1182 |
+
|
| 1183 |
+
async createInbox() {
|
| 1184 |
+
if (!this.domain) throw new Error('Catch-All 未配置域名');
|
| 1185 |
+
|
| 1186 |
+
const randomPart = crypto.randomBytes(4).toString('hex');
|
| 1187 |
+
this.address = `${this.prefix}_${randomPart}@${this.domain}`;
|
| 1188 |
+
this.sessionStartTime = Date.now();
|
| 1189 |
+
|
| 1190 |
+
console.log(` [CatchAll] 邮箱: ${this.address}`);
|
| 1191 |
+
return this.address;
|
| 1192 |
+
}
|
| 1193 |
+
|
| 1194 |
+
canAutoVerify() { return this.verifyMethod === 'cloudflare_worker' && !!this.workerUrl; }
|
| 1195 |
+
|
| 1196 |
+
async fetchVerificationCode(senderFilter, pollOptions = {}) {
|
| 1197 |
+
if (!this.canAutoVerify()) return null;
|
| 1198 |
+
|
| 1199 |
+
const { initialDelay = 15000, maxAttempts = 20, pollInterval = 5000 } = pollOptions;
|
| 1200 |
+
|
| 1201 |
+
console.log(` [CatchAll] 等待邮件 (${initialDelay / 1000}s)...`);
|
| 1202 |
+
await sleep(initialDelay);
|
| 1203 |
+
|
| 1204 |
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
| 1205 |
+
try {
|
| 1206 |
+
const params = `to=${encodeURIComponent(this.address)}&from=${encodeURIComponent(senderFilter || '')}&after=${this.sessionStartTime || 0}`;
|
| 1207 |
+
const headers = {};
|
| 1208 |
+
if (this.workerSecret) headers['X-Auth-Secret'] = this.workerSecret;
|
| 1209 |
+
|
| 1210 |
+
const resp = await httpsGet(`${this.workerUrl}/emails?${params}`, { headers });
|
| 1211 |
+
if (!resp.ok) { await sleep(pollInterval); continue; }
|
| 1212 |
+
|
| 1213 |
+
const data = resp.json();
|
| 1214 |
+
const emails = Array.isArray(data.emails || data.messages || data) ? (data.emails || data.messages || data) : [];
|
| 1215 |
+
|
| 1216 |
+
for (const email of emails) {
|
| 1217 |
+
const code = extractVerificationCode(email.subject) ||
|
| 1218 |
+
extractVerificationCode(email.text) ||
|
| 1219 |
+
extractVerificationCode(email.html) ||
|
| 1220 |
+
extractVerificationCode(email.body);
|
| 1221 |
+
if (code) {
|
| 1222 |
+
console.log(` [CatchAll] 验证码: ${code}`);
|
| 1223 |
+
return code;
|
| 1224 |
+
}
|
| 1225 |
+
}
|
| 1226 |
+
|
| 1227 |
+
console.log(` [CatchAll] 轮询 ${attempt}/${maxAttempts}: 未提取到验证码`);
|
| 1228 |
+
} catch (e) {
|
| 1229 |
+
console.log(` [CatchAll] 轮询 ${attempt}/${maxAttempts} 出错: ${e.message}`);
|
| 1230 |
+
}
|
| 1231 |
+
await sleep(pollInterval);
|
| 1232 |
+
}
|
| 1233 |
+
return null;
|
| 1234 |
+
}
|
| 1235 |
+
}
|
| 1236 |
+
|
| 1237 |
+
// ==================== 自定义 HTTP Provider ====================
|
| 1238 |
+
|
| 1239 |
+
class CustomProvider extends MailProvider {
|
| 1240 |
+
constructor(options = {}) {
|
| 1241 |
+
super(options);
|
| 1242 |
+
this.createUrl = options.createUrl || '';
|
| 1243 |
+
this.createHeaders = options.createHeaders || {};
|
| 1244 |
+
this.fetchUrl = options.fetchUrl || '';
|
| 1245 |
+
this.fetchHeaders = options.fetchHeaders || {};
|
| 1246 |
+
this.emailId = null;
|
| 1247 |
+
}
|
| 1248 |
+
|
| 1249 |
+
async createInbox() {
|
| 1250 |
+
if (!this.createUrl) throw new Error('Custom Provider 未配置 createUrl');
|
| 1251 |
+
|
| 1252 |
+
const resp = await httpsPost(this.createUrl, {}, { headers: this.createHeaders });
|
| 1253 |
+
if (!resp.ok) throw new Error(`Custom 创建邮箱失败: ${resp.status}`);
|
| 1254 |
+
|
| 1255 |
+
const data = resp.json();
|
| 1256 |
+
this.address = data.address || data.email || data.data?.address;
|
| 1257 |
+
this.emailId = data.id || data.data?.id;
|
| 1258 |
+
this.sessionStartTime = Date.now();
|
| 1259 |
+
|
| 1260 |
+
if (!this.address) throw new Error('Custom API 未返回邮箱地址');
|
| 1261 |
+
console.log(` [Custom] 邮箱: ${this.address}`);
|
| 1262 |
+
return this.address;
|
| 1263 |
+
}
|
| 1264 |
+
|
| 1265 |
+
canAutoVerify() { return !!this.fetchUrl; }
|
| 1266 |
+
|
| 1267 |
+
async fetchVerificationCode(senderFilter, pollOptions = {}) {
|
| 1268 |
+
if (!this.fetchUrl) return null;
|
| 1269 |
+
|
| 1270 |
+
const { initialDelay = 15000, maxAttempts = 15, pollInterval = 5000 } = pollOptions;
|
| 1271 |
+
|
| 1272 |
+
console.log(` [Custom] 等待邮件 (${initialDelay / 1000}s)...`);
|
| 1273 |
+
await sleep(initialDelay);
|
| 1274 |
+
|
| 1275 |
+
const url = this.fetchUrl.replace('{id}', this.emailId || '').replace('{address}', this.address || '');
|
| 1276 |
+
|
| 1277 |
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
| 1278 |
+
try {
|
| 1279 |
+
const resp = await httpsGet(url, { headers: this.fetchHeaders });
|
| 1280 |
+
if (!resp.ok) { await sleep(pollInterval); continue; }
|
| 1281 |
+
|
| 1282 |
+
const data = resp.json();
|
| 1283 |
+
const messages = data.messages || data.emails || data.data?.messages || [];
|
| 1284 |
+
|
| 1285 |
+
for (const msg of (Array.isArray(messages) ? messages : [])) {
|
| 1286 |
+
const from = msg.from || msg.sender || '';
|
| 1287 |
+
if (senderFilter && !from.toLowerCase().includes(senderFilter.toLowerCase())) continue;
|
| 1288 |
+
|
| 1289 |
+
const code = extractVerificationCode(msg.subject) ||
|
| 1290 |
+
extractVerificationCode(msg.body) ||
|
| 1291 |
+
extractVerificationCode(msg.text) ||
|
| 1292 |
+
extractVerificationCode(msg.content) ||
|
| 1293 |
+
extractVerificationCode(msg.html);
|
| 1294 |
+
if (code) {
|
| 1295 |
+
console.log(` [Custom] 验证码: ${code}`);
|
| 1296 |
+
return code;
|
| 1297 |
+
}
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
console.log(` [Custom] 轮询 ${attempt}/${maxAttempts}: 未提取到验证码`);
|
| 1301 |
+
} catch (e) {
|
| 1302 |
+
console.log(` [Custom] 轮询 ${attempt}/${maxAttempts} 出错: ${e.message}`);
|
| 1303 |
+
}
|
| 1304 |
+
await sleep(pollInterval);
|
| 1305 |
+
}
|
| 1306 |
+
return null;
|
| 1307 |
+
}
|
| 1308 |
+
}
|
| 1309 |
+
|
| 1310 |
+
// ==================== 手动输入 ====================
|
| 1311 |
+
|
| 1312 |
+
class ManualMailProvider extends MailProvider {
|
| 1313 |
+
constructor() {
|
| 1314 |
+
super();
|
| 1315 |
+
}
|
| 1316 |
+
|
| 1317 |
+
async createInbox() {
|
| 1318 |
+
this.address = await prompt(' 请输入要注册的邮箱地址 > ');
|
| 1319 |
+
return this.address;
|
| 1320 |
+
}
|
| 1321 |
+
|
| 1322 |
+
async fetchVerificationCode() {
|
| 1323 |
+
console.log(`\n ┌──────────────────────────────────────────────────┐`);
|
| 1324 |
+
console.log(` │ 等待验证码 - 请检查邮箱 │`);
|
| 1325 |
+
console.log(` │ 邮箱: ${(this.address || '').substring(0, 42).padEnd(42)}│`);
|
| 1326 |
+
console.log(` │ 发件人: xxx@send.chataibot.pro │`);
|
| 1327 |
+
console.log(` │ 格式: "Your code: XXXXXX" (6位数字) │`);
|
| 1328 |
+
console.log(` └──────────────────────────────────────────────────┘\n`);
|
| 1329 |
+
const code = await prompt(' 请输入 6 位验证码 > ');
|
| 1330 |
+
return code;
|
| 1331 |
+
}
|
| 1332 |
+
}
|
| 1333 |
+
|
| 1334 |
+
// ==================== Provider 注册表与工厂 ====================
|
| 1335 |
+
|
| 1336 |
+
const PROVIDERS = {
|
| 1337 |
+
moemail: MoeMailProvider,
|
| 1338 |
+
linshiyou: LinShiYouProvider,
|
| 1339 |
+
gptmail: GPTMailProvider,
|
| 1340 |
+
guerrilla: GuerrillaProvider,
|
| 1341 |
+
tempmail: TempMailProvider,
|
| 1342 |
+
tempmailio: TempMailIOProvider,
|
| 1343 |
+
mailtm: MailTmProvider,
|
| 1344 |
+
dropmail: DropmailProvider,
|
| 1345 |
+
duckmail: DuckMailProvider,
|
| 1346 |
+
catchall: CatchAllProvider,
|
| 1347 |
+
custom: CustomProvider,
|
| 1348 |
+
manual: ManualMailProvider,
|
| 1349 |
+
};
|
| 1350 |
+
|
| 1351 |
+
export function createMailProvider(providerName, options = {}) {
|
| 1352 |
+
const ProviderClass = PROVIDERS[providerName];
|
| 1353 |
+
if (!ProviderClass) {
|
| 1354 |
+
const available = Object.keys(PROVIDERS).join(', ');
|
| 1355 |
+
throw new Error(`未知邮箱 Provider: ${providerName}\n可用渠道: ${available}`);
|
| 1356 |
+
}
|
| 1357 |
+
return new ProviderClass(options);
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
+
export function listProviders() {
|
| 1361 |
+
return Object.keys(PROVIDERS);
|
| 1362 |
+
}
|
| 1363 |
+
|
| 1364 |
+
export function closeMail() {
|
| 1365 |
+
try { rl.close(); } catch {}
|
| 1366 |
+
}
|
| 1367 |
+
|
| 1368 |
+
export { extractVerificationCode as extractCode };
|
message-convert.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* message-convert.js - 消息格式转换
|
| 3 |
+
*
|
| 4 |
+
* 将 OpenAI / Anthropic 的 messages 数组转换为
|
| 5 |
+
* chataibot.pro 接受的单一 text 字符串
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import config from './config.js';
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* 从 content 字段提取纯文本
|
| 12 |
+
* OpenAI/Anthropic 的 content 可能是字符串或多模态数组
|
| 13 |
+
*/
|
| 14 |
+
function extractText(content) {
|
| 15 |
+
if (typeof content === 'string') return content;
|
| 16 |
+
if (Array.isArray(content)) {
|
| 17 |
+
return content
|
| 18 |
+
.filter(part => part.type === 'text')
|
| 19 |
+
.map(part => part.text)
|
| 20 |
+
.join('\n');
|
| 21 |
+
}
|
| 22 |
+
return String(content || '');
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* OpenAI messages → chataibot text
|
| 27 |
+
* @param {Array} messages - [{ role: 'system'|'user'|'assistant', content }]
|
| 28 |
+
*/
|
| 29 |
+
export function openaiToText(messages) {
|
| 30 |
+
if (!messages || !messages.length) return '';
|
| 31 |
+
|
| 32 |
+
const system = messages.filter(m => m.role === 'system').map(m => extractText(m.content)).join('\n');
|
| 33 |
+
const conversation = messages.filter(m => m.role !== 'system');
|
| 34 |
+
|
| 35 |
+
// 单轮: 只有一条 user 消息
|
| 36 |
+
if (conversation.length === 1 && conversation[0].role === 'user') {
|
| 37 |
+
const userText = extractText(conversation[0].content);
|
| 38 |
+
return system ? `${system}\n\n${userText}` : userText;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// 多轮: 格式化为带角色标签的文本
|
| 42 |
+
let text = '';
|
| 43 |
+
if (system) text += `[System] ${system}\n\n`;
|
| 44 |
+
|
| 45 |
+
for (const msg of conversation) {
|
| 46 |
+
const role = msg.role === 'assistant' ? 'Assistant' : 'User';
|
| 47 |
+
text += `[${role}] ${extractText(msg.content)}\n\n`;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
return text.trim();
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* Anthropic messages → chataibot text
|
| 55 |
+
* @param {string|undefined} system - system prompt (Anthropic 单独字段)
|
| 56 |
+
* @param {Array} messages - [{ role: 'user'|'assistant', content }]
|
| 57 |
+
*/
|
| 58 |
+
export function anthropicToText(system, messages) {
|
| 59 |
+
if (!messages || !messages.length) return system || '';
|
| 60 |
+
|
| 61 |
+
// 单轮
|
| 62 |
+
if (messages.length === 1 && messages[0].role === 'user') {
|
| 63 |
+
const userText = extractText(messages[0].content);
|
| 64 |
+
return system ? `${system}\n\n${userText}` : userText;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// 多轮
|
| 68 |
+
let text = '';
|
| 69 |
+
if (system) text += `[System] ${system}\n\n`;
|
| 70 |
+
|
| 71 |
+
for (const msg of messages) {
|
| 72 |
+
const role = msg.role === 'assistant' ? 'Assistant' : 'User';
|
| 73 |
+
text += `[${role}] ${extractText(msg.content)}\n\n`;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
return text.trim();
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* 解析并映射模型名
|
| 81 |
+
*/
|
| 82 |
+
export function resolveModel(requestModel) {
|
| 83 |
+
if (!requestModel) return 'gpt-4o';
|
| 84 |
+
// 精确匹配
|
| 85 |
+
if (config.modelMapping[requestModel]) return config.modelMapping[requestModel];
|
| 86 |
+
// 尝试去掉版本后缀匹配 (如 claude-3-sonnet-20240229 → claude-3-sonnet)
|
| 87 |
+
const base = requestModel.replace(/-\d{8}$/, '');
|
| 88 |
+
if (config.modelMapping[base]) return config.modelMapping[base];
|
| 89 |
+
// 原样返回让 chataibot 自己判断
|
| 90 |
+
return requestModel;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/**
|
| 94 |
+
* 根据模型名判断所属厂商 (与官网分组一致)
|
| 95 |
+
*/
|
| 96 |
+
function getOwner(model) {
|
| 97 |
+
if (model.startsWith('gpt-') || model.startsWith('o1') || model.startsWith('o3') || model.startsWith('o4')) return 'OpenAI';
|
| 98 |
+
if (model.startsWith('claude-')) return 'Anthropic';
|
| 99 |
+
if (model.startsWith('gemini-')) return 'Google';
|
| 100 |
+
return '其他';
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/**
|
| 104 |
+
* 获取可用模型列表 (去重,只列 chataibot 原生模型名)
|
| 105 |
+
*/
|
| 106 |
+
export function getModelList() {
|
| 107 |
+
const seen = new Set();
|
| 108 |
+
const list = [];
|
| 109 |
+
for (const target of Object.values(config.modelMapping)) {
|
| 110 |
+
if (seen.has(target)) continue;
|
| 111 |
+
seen.add(target);
|
| 112 |
+
list.push({
|
| 113 |
+
id: target,
|
| 114 |
+
object: 'model',
|
| 115 |
+
created: 1700000000,
|
| 116 |
+
owned_by: getOwner(target),
|
| 117 |
+
});
|
| 118 |
+
}
|
| 119 |
+
return list;
|
| 120 |
+
}
|
openai-adapter.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* openai-adapter.js - OpenAI 兼容 API 适配器
|
| 3 |
+
*
|
| 4 |
+
* 处理 POST /v1/chat/completions
|
| 5 |
+
* 将 OpenAI 格式请求转换为 chataibot.pro 协议
|
| 6 |
+
* 支持自动重试换号 (最多 3 次)
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import crypto from 'crypto';
|
| 10 |
+
import { createContext, sendMessageStreaming } from './chat.js';
|
| 11 |
+
import { openaiToText, resolveModel } from './message-convert.js';
|
| 12 |
+
import { transformToOpenAISSE, collectFullResponse } from './stream-transform.js';
|
| 13 |
+
|
| 14 |
+
const MAX_RETRY = 3;
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* 判断错误是否可重试 (换号)
|
| 18 |
+
*/
|
| 19 |
+
function isRetryable(e) {
|
| 20 |
+
const code = e.statusCode || 0;
|
| 21 |
+
// 401/403 session 过期, 429 额度耗尽, 额度相关的 streaming error
|
| 22 |
+
if (code === 401 || code === 403 || code === 429) return true;
|
| 23 |
+
const msg = (e.message || '').toLowerCase();
|
| 24 |
+
if (msg.includes('limit') || msg.includes('quota') || msg.includes('exhaust')
|
| 25 |
+
|| msg.includes('exceed') || msg.includes('too many') || msg.includes('no available')) return true;
|
| 26 |
+
return false;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/**
|
| 30 |
+
* 根据错误类型释放账号
|
| 31 |
+
*/
|
| 32 |
+
function releaseOnError(pool, account, e) {
|
| 33 |
+
const code = e.statusCode || 0;
|
| 34 |
+
if (code === 401 || code === 403) {
|
| 35 |
+
pool.release(account, { sessionExpired: true });
|
| 36 |
+
} else if (code === 429) {
|
| 37 |
+
pool.release(account, { quotaExhausted: true });
|
| 38 |
+
} else {
|
| 39 |
+
pool.release(account, { success: false });
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* 处理 OpenAI chat completions 请求
|
| 45 |
+
*/
|
| 46 |
+
export async function handleChatCompletions(body, res, pool) {
|
| 47 |
+
const requestId = 'chatcmpl-' + crypto.randomBytes(12).toString('hex');
|
| 48 |
+
|
| 49 |
+
// 验证参数
|
| 50 |
+
if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
|
| 51 |
+
sendError(res, 400, 'messages is required and must be a non-empty array');
|
| 52 |
+
return;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const text = openaiToText(body.messages);
|
| 56 |
+
const model = resolveModel(body.model);
|
| 57 |
+
const clientModel = body.model || 'gpt-4o';
|
| 58 |
+
const stream = body.stream === true;
|
| 59 |
+
|
| 60 |
+
if (!text) {
|
| 61 |
+
sendError(res, 400, 'No valid message content found');
|
| 62 |
+
return;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// 流式请求: 尝试获取流,如果获取阶段就报错则换号重试
|
| 66 |
+
if (stream) {
|
| 67 |
+
let lastError;
|
| 68 |
+
for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
|
| 69 |
+
let account;
|
| 70 |
+
try {
|
| 71 |
+
account = await pool.acquire();
|
| 72 |
+
} catch (e) {
|
| 73 |
+
sendError(res, 503, 'No available account: ' + e.message);
|
| 74 |
+
return;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
try {
|
| 78 |
+
const title = text.substring(0, 100);
|
| 79 |
+
const ctx = await createContext(account.cookies, model, title);
|
| 80 |
+
if (ctx.cookies) account.cookies = ctx.cookies;
|
| 81 |
+
|
| 82 |
+
const result = await sendMessageStreaming(account.cookies, ctx.chatId, text, model);
|
| 83 |
+
if (result.cookies) account.cookies = result.cookies;
|
| 84 |
+
|
| 85 |
+
// 流获取成功 — 开始写 SSE (此后无法重试)
|
| 86 |
+
transformToOpenAISSE(result.stream, res, clientModel, requestId, (errMsg) => {
|
| 87 |
+
// streamingError 回调 — 检测额度耗尽
|
| 88 |
+
const msg = (errMsg || '').toLowerCase();
|
| 89 |
+
if (msg.includes('limit') || msg.includes('quota') || msg.includes('exhaust') || msg.includes('exceed')) {
|
| 90 |
+
pool.release(account, { quotaExhausted: true });
|
| 91 |
+
} else {
|
| 92 |
+
pool.release(account, { success: false });
|
| 93 |
+
}
|
| 94 |
+
account = null; // 标记已释放
|
| 95 |
+
});
|
| 96 |
+
result.stream.on('end', () => { if (account) pool.release(account, { success: true }); });
|
| 97 |
+
result.stream.on('error', (err) => {
|
| 98 |
+
if (!account) return;
|
| 99 |
+
const msg = (err?.message || '').toLowerCase();
|
| 100 |
+
if (msg.includes('limit') || msg.includes('quota') || msg.includes('exhaust')) {
|
| 101 |
+
pool.release(account, { quotaExhausted: true });
|
| 102 |
+
} else {
|
| 103 |
+
pool.release(account, { success: false });
|
| 104 |
+
}
|
| 105 |
+
});
|
| 106 |
+
return; // 成功启动流,退出
|
| 107 |
+
} catch (e) {
|
| 108 |
+
releaseOnError(pool, account, e);
|
| 109 |
+
lastError = e;
|
| 110 |
+
if (isRetryable(e) && attempt < MAX_RETRY) {
|
| 111 |
+
console.log(`[OpenAI] 请求失败 (${e.statusCode || e.message}), 换号重试 ${attempt + 1}/${MAX_RETRY}`);
|
| 112 |
+
continue;
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
// 所有重试都失败
|
| 117 |
+
if (!res.headersSent) {
|
| 118 |
+
sendError(res, 502, `Upstream error after ${MAX_RETRY} retries: ${lastError?.message}`);
|
| 119 |
+
}
|
| 120 |
+
return;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// 非流式请求: 完整重试 (包括 stream 收集阶段)
|
| 124 |
+
let lastError;
|
| 125 |
+
for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
|
| 126 |
+
let account;
|
| 127 |
+
try {
|
| 128 |
+
account = await pool.acquire();
|
| 129 |
+
} catch (e) {
|
| 130 |
+
sendError(res, 503, 'No available account: ' + e.message);
|
| 131 |
+
return;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
try {
|
| 135 |
+
const title = text.substring(0, 100);
|
| 136 |
+
const ctx = await createContext(account.cookies, model, title);
|
| 137 |
+
if (ctx.cookies) account.cookies = ctx.cookies;
|
| 138 |
+
|
| 139 |
+
const result = await sendMessageStreaming(account.cookies, ctx.chatId, text, model);
|
| 140 |
+
if (result.cookies) account.cookies = result.cookies;
|
| 141 |
+
|
| 142 |
+
const full = await collectFullResponse(result.stream);
|
| 143 |
+
pool.release(account, { success: true });
|
| 144 |
+
|
| 145 |
+
res.writeHead(200, {
|
| 146 |
+
'Content-Type': 'application/json',
|
| 147 |
+
'Access-Control-Allow-Origin': '*',
|
| 148 |
+
});
|
| 149 |
+
res.end(JSON.stringify({
|
| 150 |
+
id: requestId,
|
| 151 |
+
object: 'chat.completion',
|
| 152 |
+
created: Math.floor(Date.now() / 1000),
|
| 153 |
+
model: clientModel,
|
| 154 |
+
choices: [{
|
| 155 |
+
index: 0,
|
| 156 |
+
message: { role: 'assistant', content: full.text },
|
| 157 |
+
finish_reason: 'stop',
|
| 158 |
+
}],
|
| 159 |
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
| 160 |
+
}));
|
| 161 |
+
return; // 成功
|
| 162 |
+
} catch (e) {
|
| 163 |
+
releaseOnError(pool, account, e);
|
| 164 |
+
lastError = e;
|
| 165 |
+
if (isRetryable(e) && attempt < MAX_RETRY) {
|
| 166 |
+
console.log(`[OpenAI] 请求失败 (${e.statusCode || e.message}), 换号重试 ${attempt + 1}/${MAX_RETRY}`);
|
| 167 |
+
continue;
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
if (!res.headersSent) {
|
| 173 |
+
sendError(res, 502, `Upstream error after ${MAX_RETRY} retries: ${lastError?.message}`);
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
function sendError(res, status, message) {
|
| 178 |
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
| 179 |
+
res.end(JSON.stringify({
|
| 180 |
+
error: { message, type: 'invalid_request_error', code: status },
|
| 181 |
+
}));
|
| 182 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "chataibot-api-proxy",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "ChatAIBot API Proxy - HuggingFace Spaces Edition",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node server.js"
|
| 8 |
+
},
|
| 9 |
+
"engines": {
|
| 10 |
+
"node": ">=18.0.0"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {}
|
| 13 |
+
}
|
pool.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* pool.js - 账号池管理器
|
| 3 |
+
*
|
| 4 |
+
* 管理 chataibot.pro 账号的生命周期:
|
| 5 |
+
* 加载 → 验证 session → 分配 → 使用 → 归还 → 轮换
|
| 6 |
+
* 额度耗尽 → 自动注册新账号补充
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import fs from 'fs';
|
| 10 |
+
import path from 'path';
|
| 11 |
+
import { post, sleep } from './http.js';
|
| 12 |
+
import { getUserInfo, getQuota } from './chat.js';
|
| 13 |
+
import { register } from './protocol.js';
|
| 14 |
+
import { generateAccountInfo } from './account.js';
|
| 15 |
+
import { createMailProvider, closeMail } from './mail.js';
|
| 16 |
+
import config from './config.js';
|
| 17 |
+
|
| 18 |
+
const State = {
|
| 19 |
+
ACTIVE: 'active',
|
| 20 |
+
IN_USE: 'in_use',
|
| 21 |
+
NEEDS_LOGIN: 'needs_login',
|
| 22 |
+
EXHAUSTED: 'exhausted',
|
| 23 |
+
DEAD: 'dead',
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
export class AccountPool {
|
| 27 |
+
constructor() {
|
| 28 |
+
this.accounts = [];
|
| 29 |
+
this._registeringCount = 0; // 当前正在注册的数量
|
| 30 |
+
this._maxConcurrentRegister = 3; // HF 共享 IP,降低并发避免 Mail.tm 429
|
| 31 |
+
this._timer = null;
|
| 32 |
+
this._logs = []; // 注册日志环形缓冲
|
| 33 |
+
this._maxLogs = 200; // 最多保留 200 条
|
| 34 |
+
this._logId = 0; // 自增 ID,方便前端增量拉取
|
| 35 |
+
this._mail429Count = 0; // 连续 429 计数,用于指数退避
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* 添加一条注册日志
|
| 40 |
+
*/
|
| 41 |
+
_addLog(message, level = 'info') {
|
| 42 |
+
this._logId++;
|
| 43 |
+
const entry = { id: this._logId, time: new Date().toISOString(), message, level };
|
| 44 |
+
this._logs.push(entry);
|
| 45 |
+
if (this._logs.length > this._maxLogs) {
|
| 46 |
+
this._logs.splice(0, this._logs.length - this._maxLogs);
|
| 47 |
+
}
|
| 48 |
+
console.log(`[Pool] ${message}`);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/**
|
| 52 |
+
* 获取日志 (支持增量: since 参数为上次拿到的最大 id)
|
| 53 |
+
*/
|
| 54 |
+
getLogs(since = 0) {
|
| 55 |
+
return this._logs.filter(l => l.id > since);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* 初始化: 从 accounts/ 目录加载已有账号
|
| 60 |
+
*/
|
| 61 |
+
async init() {
|
| 62 |
+
const dir = path.resolve(config.outputDir);
|
| 63 |
+
if (!fs.existsSync(dir)) {
|
| 64 |
+
fs.mkdirSync(dir, { recursive: true });
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
| 68 |
+
this._addLog(`加载 ${files.length} 个账号文件...`);
|
| 69 |
+
|
| 70 |
+
for (const file of files) {
|
| 71 |
+
try {
|
| 72 |
+
const filePath = path.join(dir, file);
|
| 73 |
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
| 74 |
+
if (!data.email || !data.password) continue;
|
| 75 |
+
|
| 76 |
+
const cookies = new Map();
|
| 77 |
+
if (data.cookies) {
|
| 78 |
+
for (const [k, v] of Object.entries(data.cookies)) {
|
| 79 |
+
cookies.set(k, v);
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
this.accounts.push({
|
| 84 |
+
email: data.email,
|
| 85 |
+
password: data.password,
|
| 86 |
+
cookies,
|
| 87 |
+
state: State.ACTIVE,
|
| 88 |
+
remainingQuota: null,
|
| 89 |
+
lastUsedAt: 0,
|
| 90 |
+
lastCheckedAt: 0,
|
| 91 |
+
errorCount: 0,
|
| 92 |
+
filePath,
|
| 93 |
+
});
|
| 94 |
+
} catch {}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// 并行验证所有账号 session
|
| 98 |
+
this._addLog(`验证 ${this.accounts.length} 个账号 session...`);
|
| 99 |
+
const checks = this.accounts.map(async (acc) => {
|
| 100 |
+
const user = await getUserInfo(acc.cookies);
|
| 101 |
+
if (user) {
|
| 102 |
+
acc.state = State.ACTIVE;
|
| 103 |
+
acc.lastCheckedAt = Date.now();
|
| 104 |
+
const quota = await getQuota(acc.cookies);
|
| 105 |
+
if (quota) acc.remainingQuota = quota.remaining;
|
| 106 |
+
} else {
|
| 107 |
+
acc.state = State.NEEDS_LOGIN;
|
| 108 |
+
}
|
| 109 |
+
});
|
| 110 |
+
await Promise.allSettled(checks);
|
| 111 |
+
|
| 112 |
+
const active = this.accounts.filter(a => a.state === State.ACTIVE).length;
|
| 113 |
+
const needsLogin = this.accounts.filter(a => a.state === State.NEEDS_LOGIN).length;
|
| 114 |
+
this._addLog(`就绪: ${active} 可用, ${needsLogin} 需重新登录`);
|
| 115 |
+
|
| 116 |
+
// 尝试重新登录
|
| 117 |
+
for (const acc of this.accounts.filter(a => a.state === State.NEEDS_LOGIN)) {
|
| 118 |
+
await this._refreshSession(acc);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// 将 dead 账号文件移到 dead/ 子目录,避免下次还加载
|
| 122 |
+
this._archiveDeadAccounts();
|
| 123 |
+
|
| 124 |
+
// 启动后台任务
|
| 125 |
+
this._startBackgroundTasks();
|
| 126 |
+
|
| 127 |
+
// 启动后主动补充池到 minAvailable
|
| 128 |
+
const activeAfterInit = this.accounts.filter(a => a.state === State.ACTIVE).length;
|
| 129 |
+
if (activeAfterInit < config.pool.minAvailable && config.pool.autoRegister) {
|
| 130 |
+
const needed = config.pool.minAvailable - activeAfterInit;
|
| 131 |
+
this._addLog(`可用账号不足 (${activeAfterInit}/${config.pool.minAvailable}),自动注册 ${needed} 个...`);
|
| 132 |
+
await this._registerBatch(needed);
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
/**
|
| 137 |
+
* 获取一个可用账号
|
| 138 |
+
* @param {string} model - chataibot 模型名,用于估算消耗
|
| 139 |
+
*/
|
| 140 |
+
async acquire() {
|
| 141 |
+
// 优先选有额度的 (quota > 0 或 quota 未知)
|
| 142 |
+
let candidates = this.accounts.filter(a =>
|
| 143 |
+
a.state === State.ACTIVE && (a.remainingQuota === null || a.remainingQuota > 0)
|
| 144 |
+
);
|
| 145 |
+
|
| 146 |
+
// 放宽: 所有 ACTIVE 的 (quota 可能未刷新)
|
| 147 |
+
if (candidates.length === 0) {
|
| 148 |
+
candidates = this.accounts.filter(a => a.state === State.ACTIVE);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
if (candidates.length === 0) {
|
| 152 |
+
// 尝试刷新 NEEDS_LOGIN 的
|
| 153 |
+
for (const acc of this.accounts.filter(a => a.state === State.NEEDS_LOGIN)) {
|
| 154 |
+
const ok = await this._refreshSession(acc);
|
| 155 |
+
if (ok) { candidates = [acc]; break; }
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
if (candidates.length === 0 && config.pool.autoRegister) {
|
| 160 |
+
// 自动注册新账号
|
| 161 |
+
const acc = await this._registerNew();
|
| 162 |
+
if (acc) candidates = [acc];
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
if (candidates.length === 0) {
|
| 166 |
+
throw new Error('No available account in pool');
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// 排序: quota 高的优先,未知的按最久未用
|
| 170 |
+
candidates.sort((a, b) => {
|
| 171 |
+
if (a.remainingQuota != null && b.remainingQuota != null) {
|
| 172 |
+
return b.remainingQuota - a.remainingQuota;
|
| 173 |
+
}
|
| 174 |
+
if (a.remainingQuota != null) return -1;
|
| 175 |
+
if (b.remainingQuota != null) return 1;
|
| 176 |
+
return a.lastUsedAt - b.lastUsedAt;
|
| 177 |
+
});
|
| 178 |
+
|
| 179 |
+
const account = candidates[0];
|
| 180 |
+
account.state = State.IN_USE;
|
| 181 |
+
account.lastUsedAt = Date.now();
|
| 182 |
+
return account;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
/**
|
| 186 |
+
* 归还账号
|
| 187 |
+
*/
|
| 188 |
+
release(account, { success = true, quotaExhausted = false, sessionExpired = false } = {}) {
|
| 189 |
+
if (sessionExpired) {
|
| 190 |
+
account.state = State.NEEDS_LOGIN;
|
| 191 |
+
account.errorCount++;
|
| 192 |
+
} else if (quotaExhausted) {
|
| 193 |
+
account.state = State.EXHAUSTED;
|
| 194 |
+
account.remainingQuota = 0;
|
| 195 |
+
} else if (success) {
|
| 196 |
+
account.state = State.ACTIVE;
|
| 197 |
+
account.errorCount = 0;
|
| 198 |
+
// 异步查询真实剩余 requests 数量 (不同模型扣费不同)
|
| 199 |
+
this._refreshQuota(account);
|
| 200 |
+
} else {
|
| 201 |
+
account.errorCount++;
|
| 202 |
+
account.state = account.errorCount >= 5 ? State.DEAD : State.ACTIVE;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
// 补充池
|
| 206 |
+
const active = this.accounts.filter(a => a.state === State.ACTIVE).length;
|
| 207 |
+
if (active < config.pool.minAvailable && config.pool.autoRegister) {
|
| 208 |
+
const needed = config.pool.minAvailable - active;
|
| 209 |
+
this._registerBatch(needed).catch(() => {});
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/**
|
| 214 |
+
* 获取池状态
|
| 215 |
+
*/
|
| 216 |
+
getStatus() {
|
| 217 |
+
const counts = {};
|
| 218 |
+
for (const s of Object.values(State)) counts[s] = 0;
|
| 219 |
+
for (const a of this.accounts) counts[a.state]++;
|
| 220 |
+
|
| 221 |
+
return {
|
| 222 |
+
total: this.accounts.length,
|
| 223 |
+
...counts,
|
| 224 |
+
accounts: this.accounts.map(a => ({
|
| 225 |
+
email: a.email,
|
| 226 |
+
state: a.state,
|
| 227 |
+
remaining: a.remainingQuota,
|
| 228 |
+
lastUsed: a.lastUsedAt ? new Date(a.lastUsedAt).toISOString() : null,
|
| 229 |
+
})),
|
| 230 |
+
};
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/**
|
| 234 |
+
* 刷新账号 session (重新登录)
|
| 235 |
+
*/
|
| 236 |
+
async _refreshSession(account) {
|
| 237 |
+
try {
|
| 238 |
+
this._addLog(`刷新 session: ${account.email}`);
|
| 239 |
+
const resp = await post(`${config.siteBase}/api/login`, {
|
| 240 |
+
email: account.email,
|
| 241 |
+
password: account.password,
|
| 242 |
+
}, {
|
| 243 |
+
headers: {
|
| 244 |
+
'Origin': config.siteBase,
|
| 245 |
+
'Referer': `${config.siteBase}/app/auth/sign-in`,
|
| 246 |
+
'Accept-Language': 'en',
|
| 247 |
+
},
|
| 248 |
+
});
|
| 249 |
+
|
| 250 |
+
if (resp.ok) {
|
| 251 |
+
account.cookies = resp.cookies;
|
| 252 |
+
account.state = State.ACTIVE;
|
| 253 |
+
account.errorCount = 0;
|
| 254 |
+
this._saveCookies(account);
|
| 255 |
+
this._addLog(`登录成功: ${account.email}`, 'success');
|
| 256 |
+
return true;
|
| 257 |
+
}
|
| 258 |
+
this._addLog(`登录失败 (${resp.status}): ${account.email}`, 'error');
|
| 259 |
+
account.errorCount++;
|
| 260 |
+
// 401 多次失败才标 DEAD,可能是密码错/账号被封
|
| 261 |
+
if (resp.status === 401 && account.errorCount >= 2) {
|
| 262 |
+
account.state = State.DEAD;
|
| 263 |
+
} else if (resp.status >= 500) {
|
| 264 |
+
// 服务器错误保持 NEEDS_LOGIN,下次再试
|
| 265 |
+
account.state = State.NEEDS_LOGIN;
|
| 266 |
+
} else {
|
| 267 |
+
account.state = account.errorCount >= 3 ? State.DEAD : State.NEEDS_LOGIN;
|
| 268 |
+
}
|
| 269 |
+
return false;
|
| 270 |
+
} catch (e) {
|
| 271 |
+
this._addLog(`登录出错: ${account.email} - ${e.message}`, 'error');
|
| 272 |
+
account.errorCount++;
|
| 273 |
+
// 网络错误保持 NEEDS_LOGIN,下次再试
|
| 274 |
+
account.state = account.errorCount >= 5 ? State.DEAD : State.NEEDS_LOGIN;
|
| 275 |
+
return false;
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/**
|
| 280 |
+
* 批量并行注册多个账号
|
| 281 |
+
* @param {number} count - 注册数量
|
| 282 |
+
* @param {string} providerOverride - 指定邮箱提供商 (空=使用默认)
|
| 283 |
+
*/
|
| 284 |
+
async _registerBatch(count, providerOverride = '') {
|
| 285 |
+
const tasks = [];
|
| 286 |
+
for (let i = 0; i < count; i++) {
|
| 287 |
+
tasks.push(this._registerNew(providerOverride));
|
| 288 |
+
// HF 共享 IP: 加大间隔避免 Mail.tm 429
|
| 289 |
+
if (i < count - 1) await sleep(3000 + Math.random() * 2000);
|
| 290 |
+
}
|
| 291 |
+
const results = await Promise.allSettled(tasks);
|
| 292 |
+
const success = results.filter(r => r.status === 'fulfilled' && r.value).length;
|
| 293 |
+
this._addLog(`批量注册完成: ${success}/${count} 成功`, success > 0 ? 'success' : 'error');
|
| 294 |
+
return success;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
/**
|
| 298 |
+
* 手动触发注册 (供 Dashboard 调用)
|
| 299 |
+
* @param {number} count - 注册数量
|
| 300 |
+
* @param {string} provider - 邮箱提供商 (可选, 覆盖默认)
|
| 301 |
+
* @param {number} concurrency - 并行数 (可选, 覆盖默认)
|
| 302 |
+
* @returns {{ registering: number, queued: number }}
|
| 303 |
+
*/
|
| 304 |
+
manualRegister(count = 5, provider = '', concurrency = 0) {
|
| 305 |
+
count = Math.min(Math.max(1, count), 50);
|
| 306 |
+
// 临时调整并发数
|
| 307 |
+
const origMax = this._maxConcurrentRegister;
|
| 308 |
+
if (concurrency > 0) {
|
| 309 |
+
this._maxConcurrentRegister = Math.min(Math.max(1, concurrency), 20);
|
| 310 |
+
}
|
| 311 |
+
const available = this._maxConcurrentRegister - this._registeringCount;
|
| 312 |
+
const actual = Math.min(count, available);
|
| 313 |
+
if (actual <= 0) {
|
| 314 |
+
this._maxConcurrentRegister = origMax;
|
| 315 |
+
return { registering: this._registeringCount, queued: 0 };
|
| 316 |
+
}
|
| 317 |
+
this._registerBatch(actual, provider || '').catch(() => {}).finally(() => {
|
| 318 |
+
this._maxConcurrentRegister = origMax;
|
| 319 |
+
});
|
| 320 |
+
return { registering: this._registeringCount + actual, queued: actual };
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
/**
|
| 324 |
+
* 自动注册新账号 (支持并行)
|
| 325 |
+
* @param {string} providerOverride - 指定邮箱提供商 (空=使用默认)
|
| 326 |
+
*/
|
| 327 |
+
async _registerNew(providerOverride = '') {
|
| 328 |
+
if (this._registeringCount >= this._maxConcurrentRegister) return null;
|
| 329 |
+
this._registeringCount++;
|
| 330 |
+
|
| 331 |
+
try {
|
| 332 |
+
// 确定使用哪个邮箱 provider
|
| 333 |
+
let providerName = providerOverride || config.mailProvider;
|
| 334 |
+
if (providerName === 'manual') providerName = 'mailtm';
|
| 335 |
+
let providerOpts = config[providerName] || {};
|
| 336 |
+
// 检查 provider 是否配置完整
|
| 337 |
+
if (providerName === 'moemail' && !providerOpts.apiUrl) providerName = 'mailtm';
|
| 338 |
+
if (providerName === 'duckmail' && !providerOpts.apiKey) providerName = 'mailtm';
|
| 339 |
+
if (providerName === 'catchall' && !providerOpts.domain) providerName = 'mailtm';
|
| 340 |
+
if (providerName === 'custom' && !providerOpts.createUrl) providerName = 'mailtm';
|
| 341 |
+
providerOpts = config[providerName] || {};
|
| 342 |
+
const mailProvider = createMailProvider(providerName, providerOpts);
|
| 343 |
+
this._addLog(`开始注册新账号 (${providerName})...`);
|
| 344 |
+
|
| 345 |
+
// 重试逻辑 (HF 环境: 增加重试次数到 4 次)
|
| 346 |
+
let lastError;
|
| 347 |
+
for (let attempt = 1; attempt <= 4; attempt++) {
|
| 348 |
+
try {
|
| 349 |
+
// 429 指数退避: 根据连续失败次数等待
|
| 350 |
+
if (this._mail429Count > 0) {
|
| 351 |
+
const backoff = Math.min(60000, 5000 * Math.pow(2, this._mail429Count - 1));
|
| 352 |
+
this._addLog(`Mail.tm 限流退避中,等待 ${Math.round(backoff/1000)}s...`, 'warn');
|
| 353 |
+
await sleep(backoff);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
this._addLog(`[${attempt}/4] 创建临时邮箱 (${providerName})...`);
|
| 357 |
+
await mailProvider.createInbox();
|
| 358 |
+
this._mail429Count = 0; // 成功,重置计数
|
| 359 |
+
this._addLog(`[${attempt}/4] 邮箱就绪: ${mailProvider.address}`);
|
| 360 |
+
const accountInfo = generateAccountInfo();
|
| 361 |
+
accountInfo.email = mailProvider.address;
|
| 362 |
+
|
| 363 |
+
this._addLog(`[${attempt}/4] 提交注册请求...`);
|
| 364 |
+
const result = await register(accountInfo, mailProvider);
|
| 365 |
+
await mailProvider.cleanup();
|
| 366 |
+
|
| 367 |
+
if (result.success) {
|
| 368 |
+
const cookies = result.cookies || new Map();
|
| 369 |
+
const entry = {
|
| 370 |
+
email: accountInfo.email,
|
| 371 |
+
password: accountInfo.password,
|
| 372 |
+
cookies,
|
| 373 |
+
state: State.ACTIVE,
|
| 374 |
+
remainingQuota: null,
|
| 375 |
+
lastUsedAt: 0,
|
| 376 |
+
lastCheckedAt: Date.now(),
|
| 377 |
+
errorCount: 0,
|
| 378 |
+
filePath: path.resolve(config.outputDir, `chataibot-${accountInfo.email.replace('@', '_at_')}.json`),
|
| 379 |
+
};
|
| 380 |
+
|
| 381 |
+
// 保存到文件
|
| 382 |
+
const saveData = {
|
| 383 |
+
site: 'chataibot.pro',
|
| 384 |
+
email: entry.email,
|
| 385 |
+
password: entry.password,
|
| 386 |
+
cookies: Object.fromEntries(cookies),
|
| 387 |
+
registeredAt: new Date().toISOString(),
|
| 388 |
+
};
|
| 389 |
+
fs.writeFileSync(entry.filePath, JSON.stringify(saveData, null, 2));
|
| 390 |
+
|
| 391 |
+
this.accounts.push(entry);
|
| 392 |
+
this._addLog(`新账号就绪: ${entry.email}`, 'success');
|
| 393 |
+
return entry;
|
| 394 |
+
}
|
| 395 |
+
lastError = result.error;
|
| 396 |
+
this._addLog(`[${attempt}/4] 注册失败: ${result.error}`, 'error');
|
| 397 |
+
} catch (e) {
|
| 398 |
+
lastError = e.message;
|
| 399 |
+
// 检测 429 限流
|
| 400 |
+
if (e.message && e.message.includes('429')) {
|
| 401 |
+
this._mail429Count++;
|
| 402 |
+
this._addLog(`[${attempt}/4] Mail.tm 429 限流 (连续 ${this._mail429Count} 次)`, 'warn');
|
| 403 |
+
} else {
|
| 404 |
+
this._addLog(`[${attempt}/4] 注册出错: ${e.message}`, 'error');
|
| 405 |
+
}
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
if (attempt < 4) {
|
| 409 |
+
const retryDelay = 5000 + Math.random() * 5000;
|
| 410 |
+
this._addLog(`等待 ${Math.round(retryDelay/1000)}s 后重试...`);
|
| 411 |
+
await sleep(retryDelay);
|
| 412 |
+
// 重新创建邮箱 provider
|
| 413 |
+
try { await mailProvider.cleanup(); } catch {}
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
this._addLog(`注册最终失败: ${lastError}`, 'error');
|
| 418 |
+
return null;
|
| 419 |
+
} finally {
|
| 420 |
+
this._registeringCount--;
|
| 421 |
+
}
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
/**
|
| 425 |
+
* 异步刷新账号的真实剩余 requests
|
| 426 |
+
*/
|
| 427 |
+
async _refreshQuota(account) {
|
| 428 |
+
try {
|
| 429 |
+
const quota = await getQuota(account.cookies);
|
| 430 |
+
if (quota) {
|
| 431 |
+
account.remainingQuota = quota.remaining;
|
| 432 |
+
account.lastCheckedAt = Date.now();
|
| 433 |
+
if (quota.remaining <= 0) {
|
| 434 |
+
account.state = State.EXHAUSTED;
|
| 435 |
+
this._addLog(`额度耗尽: ${account.email} (${quota.remaining} requests)`, 'warn');
|
| 436 |
+
}
|
| 437 |
+
}
|
| 438 |
+
} catch {}
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
/**
|
| 442 |
+
* 将 dead 账号文件移到 dead/ 子目录
|
| 443 |
+
*/
|
| 444 |
+
_archiveDeadAccounts() {
|
| 445 |
+
const deadAccounts = this.accounts.filter(a => a.state === State.DEAD);
|
| 446 |
+
if (deadAccounts.length === 0) return;
|
| 447 |
+
|
| 448 |
+
const deadDir = path.resolve(config.outputDir, 'dead');
|
| 449 |
+
if (!fs.existsSync(deadDir)) fs.mkdirSync(deadDir, { recursive: true });
|
| 450 |
+
|
| 451 |
+
for (const acc of deadAccounts) {
|
| 452 |
+
if (!acc.filePath || !fs.existsSync(acc.filePath)) continue;
|
| 453 |
+
try {
|
| 454 |
+
const dest = path.join(deadDir, path.basename(acc.filePath));
|
| 455 |
+
fs.renameSync(acc.filePath, dest);
|
| 456 |
+
this._addLog(`归档 dead 账号: ${acc.email}`);
|
| 457 |
+
} catch {}
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
// 从池中移除 dead 账号
|
| 461 |
+
this.accounts = this.accounts.filter(a => a.state !== State.DEAD);
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
/**
|
| 465 |
+
* 保存更新后的 cookies 到 JSON 文件
|
| 466 |
+
*/
|
| 467 |
+
_saveCookies(account) {
|
| 468 |
+
if (!account.filePath) return;
|
| 469 |
+
try {
|
| 470 |
+
const data = JSON.parse(fs.readFileSync(account.filePath, 'utf8'));
|
| 471 |
+
data.cookies = Object.fromEntries(account.cookies);
|
| 472 |
+
fs.writeFileSync(account.filePath, JSON.stringify(data, null, 2));
|
| 473 |
+
} catch {}
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
/**
|
| 477 |
+
* 后台定时任务
|
| 478 |
+
*/
|
| 479 |
+
_startBackgroundTasks() {
|
| 480 |
+
const interval = config.pool.checkInterval || 300000;
|
| 481 |
+
this._timer = setInterval(async () => {
|
| 482 |
+
// 检查额度
|
| 483 |
+
for (const acc of this.accounts.filter(a => a.state === State.ACTIVE || a.state === State.EXHAUSTED)) {
|
| 484 |
+
if (Date.now() - acc.lastCheckedAt < interval) continue;
|
| 485 |
+
const quota = await getQuota(acc.cookies);
|
| 486 |
+
if (quota) {
|
| 487 |
+
acc.remainingQuota = quota.remaining;
|
| 488 |
+
acc.lastCheckedAt = Date.now();
|
| 489 |
+
if (acc.state === State.EXHAUSTED && quota.remaining > 0) {
|
| 490 |
+
acc.state = State.ACTIVE;
|
| 491 |
+
this._addLog(`额度恢复: ${acc.email} (${quota.remaining})`, 'success');
|
| 492 |
+
}
|
| 493 |
+
}
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
// 刷新需要登录的
|
| 497 |
+
for (const acc of this.accounts.filter(a => a.state === State.NEEDS_LOGIN)) {
|
| 498 |
+
await this._refreshSession(acc);
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
// 补充池
|
| 502 |
+
const active = this.accounts.filter(a => a.state === State.ACTIVE).length;
|
| 503 |
+
if (active < config.pool.minAvailable && config.pool.autoRegister) {
|
| 504 |
+
const needed = config.pool.minAvailable - active;
|
| 505 |
+
await this._registerBatch(needed);
|
| 506 |
+
}
|
| 507 |
+
}, interval);
|
| 508 |
+
|
| 509 |
+
// 不阻止进程退出
|
| 510 |
+
if (this._timer.unref) this._timer.unref();
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
destroy() {
|
| 514 |
+
if (this._timer) clearInterval(this._timer);
|
| 515 |
+
}
|
| 516 |
+
}
|
protocol.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* protocol.js - ChatAIBot.pro 纯 HTTP 协议注册模块
|
| 3 |
+
*
|
| 4 |
+
* 已确认的 API 端点 (Express + Apache/Ubuntu):
|
| 5 |
+
* POST /api/register → 201 {"success":true} (注册, 返回 connect.sid session cookie)
|
| 6 |
+
* POST /api/register/verify → 200 (验证, 需要 {email, token})
|
| 7 |
+
* POST /api/login → 200 (登录, 需要 {email, password})
|
| 8 |
+
* POST /api/logout → 200 OK
|
| 9 |
+
*
|
| 10 |
+
* 注册流程 (逆向自前端 sign-up chunk + module 40933):
|
| 11 |
+
* 1. POST /api/register { email, password, isAdvertisingAccepted, mainSiteUrl,
|
| 12 |
+
* utmSource, utmCampaign, connectBusiness, yandexClientId }
|
| 13 |
+
* Headers: Content-Type: application/json, Accept-Language: en
|
| 14 |
+
* → 201 {"success":true} + Set-Cookie: connect.sid
|
| 15 |
+
* 2. 邮件中包含 6 位数字验证码 (token)
|
| 16 |
+
* 格式: "Your code: 528135"
|
| 17 |
+
* 3. POST /api/register/verify { email, token, connectBusiness, syncToken }
|
| 18 |
+
* → 验证完成,账号激活
|
| 19 |
+
* 4. POST /api/login { email, password }
|
| 20 |
+
* → 获取 session
|
| 21 |
+
*/
|
| 22 |
+
|
| 23 |
+
import { request, get, post, sleep } from './http.js';
|
| 24 |
+
import config from './config.js';
|
| 25 |
+
|
| 26 |
+
const API_BASE = config.siteBase;
|
| 27 |
+
|
| 28 |
+
// ==================== 工具函数 ====================
|
| 29 |
+
|
| 30 |
+
function humanDelay(baseMs) {
|
| 31 |
+
return Math.floor(baseMs * (0.8 + Math.random() * 1.7));
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// ==================== 注册步骤 ====================
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Step 1: 注册账号
|
| 38 |
+
* POST /api/register
|
| 39 |
+
* 逆向自前端 sign-up-d2a668c82094de73.js + module 40933
|
| 40 |
+
* 浏览器发送的完整字段和 headers
|
| 41 |
+
*/
|
| 42 |
+
async function stepRegister(account) {
|
| 43 |
+
console.log(' [Step 1] 提交注册...');
|
| 44 |
+
|
| 45 |
+
const resp = await post(`${API_BASE}/api/register`, {
|
| 46 |
+
email: account.email,
|
| 47 |
+
password: account.password,
|
| 48 |
+
isAdvertisingAccepted: false,
|
| 49 |
+
mainSiteUrl: `${config.siteBase}/api`,
|
| 50 |
+
utmSource: '',
|
| 51 |
+
utmCampaign: '',
|
| 52 |
+
connectBusiness: '',
|
| 53 |
+
yandexClientId: '',
|
| 54 |
+
}, {
|
| 55 |
+
headers: {
|
| 56 |
+
'Origin': config.siteBase,
|
| 57 |
+
'Referer': config.signupUrl,
|
| 58 |
+
'Accept-Language': 'en',
|
| 59 |
+
},
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
const body = resp.text();
|
| 63 |
+
console.log(` 状态: ${resp.status}`);
|
| 64 |
+
console.log(` 响应: ${body.substring(0, 200)}`);
|
| 65 |
+
|
| 66 |
+
if (resp.status === 201 || resp.ok) {
|
| 67 |
+
const sessionCookie = resp.cookies.get('connect.sid');
|
| 68 |
+
console.log(` Session: ${sessionCookie ? sessionCookie.substring(0, 50) + '...' : '无'}`);
|
| 69 |
+
return { success: true, cookies: resp.cookies };
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
let errorMsg = body;
|
| 73 |
+
try {
|
| 74 |
+
const json = JSON.parse(body);
|
| 75 |
+
errorMsg = json.message || json.error || body;
|
| 76 |
+
} catch {}
|
| 77 |
+
|
| 78 |
+
throw new Error(`注册失败 (${resp.status}): ${errorMsg}`);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* Step 2: 从邮箱获取 6 位验证码
|
| 83 |
+
* chataibot.pro 的验证码就是 token,是一个 6 位数字
|
| 84 |
+
*/
|
| 85 |
+
async function stepGetVerifyToken(mailProvider, senderFilter) {
|
| 86 |
+
console.log(' [Step 2] 等待验证邮件...');
|
| 87 |
+
|
| 88 |
+
const pollOptions = {
|
| 89 |
+
initialDelay: 8000,
|
| 90 |
+
maxAttempts: 15,
|
| 91 |
+
pollInterval: 5000,
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
const code = await mailProvider.fetchVerificationCode(senderFilter, pollOptions);
|
| 95 |
+
if (code) {
|
| 96 |
+
console.log(` 验证码: ${code}`);
|
| 97 |
+
return code;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
return null;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/**
|
| 104 |
+
* Step 3: 提交验证码
|
| 105 |
+
* POST /api/register/verify { email, token, connectBusiness, syncToken }
|
| 106 |
+
* 逆向自前端: verify 还发送 connectBusiness 和 syncToken
|
| 107 |
+
*/
|
| 108 |
+
async function stepVerify(email, token, cookies) {
|
| 109 |
+
console.log(' [Step 3] 提交验证...');
|
| 110 |
+
|
| 111 |
+
const resp = await post(`${API_BASE}/api/register/verify`, {
|
| 112 |
+
email,
|
| 113 |
+
token,
|
| 114 |
+
connectBusiness: '',
|
| 115 |
+
syncToken: '',
|
| 116 |
+
}, {
|
| 117 |
+
cookies,
|
| 118 |
+
headers: {
|
| 119 |
+
'Origin': config.siteBase,
|
| 120 |
+
'Referer': `${config.siteBase}/app/verify`,
|
| 121 |
+
'Accept-Language': 'en',
|
| 122 |
+
},
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
const body = resp.text();
|
| 126 |
+
console.log(` 状态: ${resp.status}`);
|
| 127 |
+
console.log(` 响应: ${body.substring(0, 300)}`);
|
| 128 |
+
|
| 129 |
+
if (resp.ok) {
|
| 130 |
+
let data;
|
| 131 |
+
try { data = JSON.parse(body); } catch { data = { raw: body }; }
|
| 132 |
+
return { success: true, data, cookies: resp.cookies };
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
throw new Error(`验证失败 (${resp.status}): ${body.substring(0, 200)}`);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
/**
|
| 139 |
+
* Step 4: 登录获取 session
|
| 140 |
+
* POST /api/login
|
| 141 |
+
*/
|
| 142 |
+
async function stepLogin(email, password) {
|
| 143 |
+
console.log(' [Step 4] 登录...');
|
| 144 |
+
|
| 145 |
+
await sleep(humanDelay(1000));
|
| 146 |
+
|
| 147 |
+
const resp = await post(`${API_BASE}/api/login`, {
|
| 148 |
+
email,
|
| 149 |
+
password,
|
| 150 |
+
}, {
|
| 151 |
+
headers: {
|
| 152 |
+
'Origin': config.siteBase,
|
| 153 |
+
'Referer': `${config.siteBase}/app/auth/sign-in`,
|
| 154 |
+
'Accept-Language': 'en',
|
| 155 |
+
},
|
| 156 |
+
});
|
| 157 |
+
|
| 158 |
+
const body = resp.text();
|
| 159 |
+
console.log(` 状态: ${resp.status}`);
|
| 160 |
+
console.log(` 响应: ${body.substring(0, 300)}`);
|
| 161 |
+
|
| 162 |
+
if (resp.ok) {
|
| 163 |
+
let data;
|
| 164 |
+
try { data = JSON.parse(body); } catch { data = { raw: body }; }
|
| 165 |
+
return {
|
| 166 |
+
success: true,
|
| 167 |
+
data,
|
| 168 |
+
cookies: resp.cookies,
|
| 169 |
+
sessionCookie: resp.cookies.get('connect.sid'),
|
| 170 |
+
};
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
console.log(' 登录失败(可能需要先验证邮箱)');
|
| 174 |
+
return { success: false, status: resp.status, body };
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// ==================== 主注册流程 ====================
|
| 178 |
+
|
| 179 |
+
/**
|
| 180 |
+
* 纯协议注册 ChatAIBot.pro 账号
|
| 181 |
+
*
|
| 182 |
+
* @param {object} account - { email, password, firstName, lastName, fullName }
|
| 183 |
+
* @param {object} mailProvider - 邮箱 provider 实例
|
| 184 |
+
* @returns {object} { success, account, session, error }
|
| 185 |
+
*/
|
| 186 |
+
export async function register(account, mailProvider) {
|
| 187 |
+
console.log(`\n ══════════════════════════════════════════════════`);
|
| 188 |
+
console.log(` 注册: ${account.email}`);
|
| 189 |
+
console.log(` ══════════════════════════════════════════════════\n`);
|
| 190 |
+
|
| 191 |
+
try {
|
| 192 |
+
// Step 1: 注册
|
| 193 |
+
await sleep(humanDelay(500));
|
| 194 |
+
const regResult = await stepRegister(account);
|
| 195 |
+
|
| 196 |
+
// Step 2: 获取 6 位验证码
|
| 197 |
+
const senderFilter = config.senderFilter || 'chataibot';
|
| 198 |
+
const token = await stepGetVerifyToken(mailProvider, senderFilter);
|
| 199 |
+
if (!token) {
|
| 200 |
+
console.log('\n [警告] 未获取到验证码,账号已创建但未验证');
|
| 201 |
+
console.log(' 请手动检查邮箱完成验证');
|
| 202 |
+
return {
|
| 203 |
+
success: true,
|
| 204 |
+
verified: false,
|
| 205 |
+
account,
|
| 206 |
+
session: { cookies: regResult.cookies },
|
| 207 |
+
cookies: regResult.cookies,
|
| 208 |
+
};
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Step 3: 提交验证
|
| 212 |
+
await sleep(humanDelay(1000));
|
| 213 |
+
const verifyResult = await stepVerify(account.email, token, regResult.cookies);
|
| 214 |
+
|
| 215 |
+
// Step 4: 登录
|
| 216 |
+
await sleep(humanDelay(1500));
|
| 217 |
+
const loginResult = await stepLogin(account.email, account.password);
|
| 218 |
+
|
| 219 |
+
return {
|
| 220 |
+
success: true,
|
| 221 |
+
verified: true,
|
| 222 |
+
account,
|
| 223 |
+
session: loginResult.data || verifyResult.data,
|
| 224 |
+
cookies: loginResult.cookies || verifyResult.cookies || regResult.cookies,
|
| 225 |
+
loginSuccess: loginResult.success,
|
| 226 |
+
};
|
| 227 |
+
} catch (e) {
|
| 228 |
+
console.log(`\n [错误] ${e.message}`);
|
| 229 |
+
return {
|
| 230 |
+
success: false,
|
| 231 |
+
account,
|
| 232 |
+
error: e.message,
|
| 233 |
+
};
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
/**
|
| 238 |
+
* 探测 API (保留供调试用)
|
| 239 |
+
*/
|
| 240 |
+
export async function probe() {
|
| 241 |
+
console.log('\n ┌──────────────────────────────────────────────────┐');
|
| 242 |
+
console.log(' │ ChatAIBot.pro API 探测 │');
|
| 243 |
+
console.log(' └──────────────────────────────────────────────────┘\n');
|
| 244 |
+
|
| 245 |
+
const endpoints = [
|
| 246 |
+
['GET', '/api/register', null],
|
| 247 |
+
['POST', '/api/register', { email: 'probe@test.com', password: 'Probe123!' }],
|
| 248 |
+
['POST', '/api/register/verify', { email: 'probe@test.com', token: 'test' }],
|
| 249 |
+
['POST', '/api/login', { email: 'probe@test.com', password: 'Probe123!' }],
|
| 250 |
+
['POST', '/api/logout', {}],
|
| 251 |
+
];
|
| 252 |
+
|
| 253 |
+
const results = [];
|
| 254 |
+
|
| 255 |
+
for (const [method, path, body] of endpoints) {
|
| 256 |
+
try {
|
| 257 |
+
const opts = {
|
| 258 |
+
headers: {
|
| 259 |
+
'Origin': config.siteBase,
|
| 260 |
+
'Referer': config.signupUrl,
|
| 261 |
+
},
|
| 262 |
+
followRedirect: false,
|
| 263 |
+
timeout: 10000,
|
| 264 |
+
};
|
| 265 |
+
|
| 266 |
+
let resp;
|
| 267 |
+
if (method === 'POST') {
|
| 268 |
+
resp = await post(`${API_BASE}${path}`, body, opts);
|
| 269 |
+
} else {
|
| 270 |
+
resp = await get(`${API_BASE}${path}`, opts);
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
const text = resp.text();
|
| 274 |
+
const status = resp.status;
|
| 275 |
+
|
| 276 |
+
console.log(` ${status} ${method} ${path}`);
|
| 277 |
+
console.log(` ${text.substring(0, 200)}`);
|
| 278 |
+
console.log(` Cookies: ${[...resp.cookies.keys()].join(', ') || '无'}`);
|
| 279 |
+
|
| 280 |
+
results.push({ method, path, status, body: text.substring(0, 200) });
|
| 281 |
+
} catch (e) {
|
| 282 |
+
results.push({ method, path, status: 0, error: e.message });
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
return results;
|
| 287 |
+
}
|
server.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* server.js - API 代理服务主入口
|
| 3 |
+
*
|
| 4 |
+
* 对外暴露 OpenAI / Anthropic 兼容的 API 端点
|
| 5 |
+
* 对内通过账号池轮换使用 chataibot.pro 试用额度
|
| 6 |
+
*
|
| 7 |
+
* 启动: node server.js
|
| 8 |
+
* 端口: config.server.port (默认 9090)
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
import http from 'http';
|
| 12 |
+
import config from './config.js';
|
| 13 |
+
import { AccountPool } from './pool.js';
|
| 14 |
+
import { handleChatCompletions } from './openai-adapter.js';
|
| 15 |
+
import { handleMessages } from './anthropic-adapter.js';
|
| 16 |
+
import { getModelList } from './message-convert.js';
|
| 17 |
+
import { getDashboardHTML } from './ui.js';
|
| 18 |
+
import { listProviders } from './mail.js';
|
| 19 |
+
|
| 20 |
+
// ==================== 初始化 ====================
|
| 21 |
+
|
| 22 |
+
const pool = new AccountPool();
|
| 23 |
+
// 不阻塞: 先启动 HTTP 服务器,再后台初始化池
|
| 24 |
+
// HF Spaces 健康检查有超时限制,必须快速响应
|
| 25 |
+
let poolReady = false;
|
| 26 |
+
pool.init().then(() => { poolReady = true; }).catch(e => {
|
| 27 |
+
console.error('[Pool] 初始化失败:', e.message);
|
| 28 |
+
poolReady = true; // 即使失败也标记为完成,允许手动注册
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
// ==================== HTTP 服务器 ====================
|
| 32 |
+
|
| 33 |
+
function parseBody(req) {
|
| 34 |
+
return new Promise((resolve, reject) => {
|
| 35 |
+
let data = '';
|
| 36 |
+
req.on('data', chunk => data += chunk);
|
| 37 |
+
req.on('end', () => {
|
| 38 |
+
try { resolve(JSON.parse(data)); }
|
| 39 |
+
catch { reject(new Error('Invalid JSON body')); }
|
| 40 |
+
});
|
| 41 |
+
req.on('error', reject);
|
| 42 |
+
});
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function sendJson(res, status, data) {
|
| 46 |
+
res.writeHead(status, {
|
| 47 |
+
'Content-Type': 'application/json',
|
| 48 |
+
'Access-Control-Allow-Origin': '*',
|
| 49 |
+
});
|
| 50 |
+
res.end(JSON.stringify(data));
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
function sendError(res, status, message) {
|
| 54 |
+
sendJson(res, status, {
|
| 55 |
+
error: { message, type: 'invalid_request_error', code: status },
|
| 56 |
+
});
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
function sendHtml(res, html) {
|
| 60 |
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });
|
| 61 |
+
res.end(html);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function checkAuth(req) {
|
| 65 |
+
if (!config.server.apiKey) return true;
|
| 66 |
+
// Bearer token
|
| 67 |
+
const auth = req.headers['authorization'] || '';
|
| 68 |
+
if (auth.startsWith('Bearer ') && auth.slice(7) === config.server.apiKey) return true;
|
| 69 |
+
// x-api-key header (Anthropic 风格)
|
| 70 |
+
if (req.headers['x-api-key'] === config.server.apiKey) return true;
|
| 71 |
+
return false;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
const server = http.createServer(async (req, res) => {
|
| 75 |
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
| 76 |
+
const pathname = url.pathname;
|
| 77 |
+
const method = req.method;
|
| 78 |
+
|
| 79 |
+
// CORS 预检
|
| 80 |
+
if (method === 'OPTIONS') {
|
| 81 |
+
res.writeHead(204, {
|
| 82 |
+
'Access-Control-Allow-Origin': '*',
|
| 83 |
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
| 84 |
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version',
|
| 85 |
+
'Access-Control-Max-Age': '86400',
|
| 86 |
+
});
|
| 87 |
+
res.end();
|
| 88 |
+
return;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// 健康检查 (不需要认证)
|
| 92 |
+
if (method === 'GET' && pathname === '/health') {
|
| 93 |
+
sendJson(res, 200, { status: 'ok', uptime: process.uptime(), poolReady });
|
| 94 |
+
return;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// Dashboard 控制台
|
| 98 |
+
if (method === 'GET' && (pathname === '/' || pathname === '/ui')) {
|
| 99 |
+
sendHtml(res, getDashboardHTML());
|
| 100 |
+
return;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// 模型列表 (Dashboard 需要免认证访问)
|
| 104 |
+
if (method === 'GET' && pathname === '/v1/models') {
|
| 105 |
+
sendJson(res, 200, { object: 'list', data: getModelList() });
|
| 106 |
+
return;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// 账号池状态 (Dashboard 需要免认证访问)
|
| 110 |
+
if (method === 'GET' && pathname === '/pool/status') {
|
| 111 |
+
sendJson(res, 200, pool.getStatus());
|
| 112 |
+
return;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// 注册日志 (Dashboard 需要免认证访问)
|
| 116 |
+
if (method === 'GET' && pathname === '/pool/logs') {
|
| 117 |
+
const since = parseInt(url.searchParams.get('since')) || 0;
|
| 118 |
+
sendJson(res, 200, { logs: pool.getLogs(since) });
|
| 119 |
+
return;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// Dashboard 配置信息 (可用邮箱提供商列表等)
|
| 123 |
+
if (method === 'GET' && pathname === '/pool/config') {
|
| 124 |
+
sendJson(res, 200, {
|
| 125 |
+
providers: listProviders(),
|
| 126 |
+
currentProvider: config.mailProvider,
|
| 127 |
+
maxConcurrent: pool._maxConcurrentRegister,
|
| 128 |
+
});
|
| 129 |
+
return;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// 手动注册账号 (Dashboard 调用)
|
| 133 |
+
if (method === 'POST' && pathname === '/pool/register') {
|
| 134 |
+
try {
|
| 135 |
+
const body = await parseBody(req);
|
| 136 |
+
const count = Math.min(Math.max(1, body.count || 5), 50);
|
| 137 |
+
const provider = body.provider || '';
|
| 138 |
+
const concurrency = parseInt(body.concurrency) || 0;
|
| 139 |
+
const result = pool.manualRegister(count, provider, concurrency);
|
| 140 |
+
const provLabel = provider || config.mailProvider;
|
| 141 |
+
sendJson(res, 200, { ok: true, message: `\u5DF2\u63D0\u4EA4 ${result.queued} \u4E2A\u6CE8\u518C\u4EFB\u52A1 (${provLabel})`, ...result });
|
| 142 |
+
} catch (e) {
|
| 143 |
+
sendJson(res, 200, { ok: false, message: e.message });
|
| 144 |
+
}
|
| 145 |
+
return;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
// 认证校验
|
| 149 |
+
if (!checkAuth(req)) {
|
| 150 |
+
sendError(res, 401, 'Invalid API key');
|
| 151 |
+
return;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
try {
|
| 155 |
+
// ==================== 路由 ====================
|
| 156 |
+
|
| 157 |
+
// OpenAI: POST /v1/chat/completions
|
| 158 |
+
if (method === 'POST' && pathname === '/v1/chat/completions') {
|
| 159 |
+
const body = await parseBody(req);
|
| 160 |
+
await handleChatCompletions(body, res, pool);
|
| 161 |
+
return;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// Anthropic: POST /v1/messages
|
| 165 |
+
if (method === 'POST' && pathname === '/v1/messages') {
|
| 166 |
+
const body = await parseBody(req);
|
| 167 |
+
await handleMessages(body, res, pool);
|
| 168 |
+
return;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// 404
|
| 172 |
+
sendError(res, 404, `Not found: ${method} ${pathname}`);
|
| 173 |
+
} catch (e) {
|
| 174 |
+
console.error(`[Server] 请求错误: ${e.message}`);
|
| 175 |
+
if (!res.headersSent) {
|
| 176 |
+
sendError(res, 500, 'Internal server error');
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
// ==================== 启动 ====================
|
| 182 |
+
|
| 183 |
+
const { port, host } = config.server;
|
| 184 |
+
server.listen(port, host, () => {
|
| 185 |
+
console.log(`\n ┌──────────────────────────────────────────┐`);
|
| 186 |
+
console.log(` │ ChatAIBot API Proxy │`);
|
| 187 |
+
console.log(` │ http://${host}:${port} │`);
|
| 188 |
+
console.log(` ├──────────────────────────────────────────┤`);
|
| 189 |
+
console.log(` │ Dashboard: http://${host}:${port}/ │`);
|
| 190 |
+
console.log(` │ OpenAI: POST /v1/chat/completions │`);
|
| 191 |
+
console.log(` │ Anthropic: POST /v1/messages │`);
|
| 192 |
+
console.log(` │ Models: GET /v1/models │`);
|
| 193 |
+
console.log(` │ Pool: GET /pool/status │`);
|
| 194 |
+
console.log(` │ Health: GET /health │`);
|
| 195 |
+
console.log(` ├──────────────────────────────────────────┤`);
|
| 196 |
+
console.log(` │ Auth: ${config.server.apiKey ? 'Bearer ' + config.server.apiKey.substring(0, 8) + '...' : '\u65E0 (\u5F00\u653E\u8BBF\u95EE)'}${' '.repeat(Math.max(0, 24 - (config.server.apiKey ? 16 : 13)))}│`);
|
| 197 |
+
console.log(` │ Pool: \u521D\u59CB\u5316\u4E2D... │`);
|
| 198 |
+
console.log(` \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n`);
|
| 199 |
+
});
|
| 200 |
+
|
| 201 |
+
// 优雅退出
|
| 202 |
+
process.on('SIGINT', () => {
|
| 203 |
+
console.log('\n[Server] 正在关闭...');
|
| 204 |
+
pool.destroy();
|
| 205 |
+
server.close(() => process.exit(0));
|
| 206 |
+
});
|
| 207 |
+
|
| 208 |
+
process.on('SIGTERM', () => {
|
| 209 |
+
pool.destroy();
|
| 210 |
+
server.close(() => process.exit(0));
|
| 211 |
+
});
|
stream-transform.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* stream-transform.js - chataibot JSON 流 → OpenAI/Anthropic SSE 转换器
|
| 3 |
+
*
|
| 4 |
+
* chataibot.pro 流式响应格式 (紧凑 JSON 对象流,无换行分隔):
|
| 5 |
+
* {"type":"botType","data":"gpt-4o"}{"type":"chunk","data":"Hello"}...
|
| 6 |
+
* {"type":"finalResult","data":{"mainText":"...","questions":[...]}}
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* JSON 对象流解析器 — 通过花括号计数提取完整 JSON 对象
|
| 11 |
+
* chataibot 返回的不是 NDJSON(无换行),而是紧凑的 JSON 对象序列
|
| 12 |
+
*/
|
| 13 |
+
function createJsonStreamParser(onObject) {
|
| 14 |
+
let buffer = '';
|
| 15 |
+
let depth = 0;
|
| 16 |
+
let inString = false;
|
| 17 |
+
let escape = false;
|
| 18 |
+
let objStart = -1;
|
| 19 |
+
|
| 20 |
+
return {
|
| 21 |
+
feed(chunk) {
|
| 22 |
+
buffer += chunk;
|
| 23 |
+
for (let i = 0; i < buffer.length; i++) {
|
| 24 |
+
const ch = buffer[i];
|
| 25 |
+
|
| 26 |
+
if (escape) { escape = false; continue; }
|
| 27 |
+
if (ch === '\\' && inString) { escape = true; continue; }
|
| 28 |
+
if (ch === '"') { inString = !inString; continue; }
|
| 29 |
+
if (inString) continue;
|
| 30 |
+
|
| 31 |
+
if (ch === '{') {
|
| 32 |
+
if (depth === 0) objStart = i;
|
| 33 |
+
depth++;
|
| 34 |
+
} else if (ch === '}') {
|
| 35 |
+
depth--;
|
| 36 |
+
if (depth === 0 && objStart >= 0) {
|
| 37 |
+
const jsonStr = buffer.substring(objStart, i + 1);
|
| 38 |
+
try { onObject(JSON.parse(jsonStr)); } catch {}
|
| 39 |
+
objStart = -1;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// 清理已消费的部分
|
| 45 |
+
if (objStart >= 0) {
|
| 46 |
+
buffer = buffer.substring(objStart);
|
| 47 |
+
objStart = 0;
|
| 48 |
+
} else if (depth === 0) {
|
| 49 |
+
buffer = '';
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
flush() {
|
| 53 |
+
if (buffer.trim()) {
|
| 54 |
+
try { onObject(JSON.parse(buffer.trim())); } catch {}
|
| 55 |
+
}
|
| 56 |
+
buffer = '';
|
| 57 |
+
},
|
| 58 |
+
};
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* chataibot NDJSON → OpenAI SSE 格式
|
| 63 |
+
* @param {Function} onStreamError - 可选回调,流中出现 streamingError 时调用
|
| 64 |
+
*/
|
| 65 |
+
export function transformToOpenAISSE(upstreamStream, res, model, requestId, onStreamError) {
|
| 66 |
+
res.writeHead(200, {
|
| 67 |
+
'Content-Type': 'text/event-stream',
|
| 68 |
+
'Cache-Control': 'no-cache',
|
| 69 |
+
'Connection': 'keep-alive',
|
| 70 |
+
'Access-Control-Allow-Origin': '*',
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
const ts = Math.floor(Date.now() / 1000);
|
| 74 |
+
|
| 75 |
+
function writeChunk(delta, finishReason = null) {
|
| 76 |
+
const obj = {
|
| 77 |
+
id: requestId,
|
| 78 |
+
object: 'chat.completion.chunk',
|
| 79 |
+
created: ts,
|
| 80 |
+
model,
|
| 81 |
+
choices: [{ index: 0, delta, finish_reason: finishReason }],
|
| 82 |
+
};
|
| 83 |
+
res.write(`data: ${JSON.stringify(obj)}\n\n`);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// 先发送 role chunk
|
| 87 |
+
writeChunk({ role: 'assistant', content: '' });
|
| 88 |
+
|
| 89 |
+
const parser = createJsonStreamParser((obj) => {
|
| 90 |
+
switch (obj.type) {
|
| 91 |
+
case 'chunk':
|
| 92 |
+
writeChunk({ content: obj.data });
|
| 93 |
+
break;
|
| 94 |
+
|
| 95 |
+
case 'reasoningContent':
|
| 96 |
+
// 兼容 OpenAI o-series reasoning 输出
|
| 97 |
+
writeChunk({ reasoning_content: obj.data });
|
| 98 |
+
break;
|
| 99 |
+
|
| 100 |
+
case 'finalResult':
|
| 101 |
+
writeChunk({}, 'stop');
|
| 102 |
+
res.write('data: [DONE]\n\n');
|
| 103 |
+
res.end();
|
| 104 |
+
break;
|
| 105 |
+
|
| 106 |
+
case 'streamingError':
|
| 107 |
+
if (onStreamError) onStreamError(obj.data);
|
| 108 |
+
writeChunk({}, 'stop');
|
| 109 |
+
res.write('data: [DONE]\n\n');
|
| 110 |
+
res.end();
|
| 111 |
+
break;
|
| 112 |
+
}
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
upstreamStream.on('data', (chunk) => parser.feed(chunk));
|
| 116 |
+
upstreamStream.on('end', () => {
|
| 117 |
+
parser.flush();
|
| 118 |
+
if (!res.writableEnded) {
|
| 119 |
+
writeChunk({}, 'stop');
|
| 120 |
+
res.write('data: [DONE]\n\n');
|
| 121 |
+
res.end();
|
| 122 |
+
}
|
| 123 |
+
});
|
| 124 |
+
upstreamStream.on('error', () => {
|
| 125 |
+
if (!res.writableEnded) {
|
| 126 |
+
res.write('data: [DONE]\n\n');
|
| 127 |
+
res.end();
|
| 128 |
+
}
|
| 129 |
+
});
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/**
|
| 133 |
+
* chataibot NDJSON → Anthropic SSE 格式
|
| 134 |
+
* @param {Function} onStreamError - 可选回调,流中出现 streamingError 时调用
|
| 135 |
+
*/
|
| 136 |
+
export function transformToAnthropicSSE(upstreamStream, res, model, requestId, onStreamError) {
|
| 137 |
+
res.writeHead(200, {
|
| 138 |
+
'Content-Type': 'text/event-stream',
|
| 139 |
+
'Cache-Control': 'no-cache',
|
| 140 |
+
'Connection': 'keep-alive',
|
| 141 |
+
'Access-Control-Allow-Origin': '*',
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
function writeEvent(event, data) {
|
| 145 |
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
let headerSent = false;
|
| 149 |
+
let blockIndex = 0;
|
| 150 |
+
|
| 151 |
+
function ensureHeader() {
|
| 152 |
+
if (headerSent) return;
|
| 153 |
+
headerSent = true;
|
| 154 |
+
|
| 155 |
+
writeEvent('message_start', {
|
| 156 |
+
type: 'message_start',
|
| 157 |
+
message: {
|
| 158 |
+
id: requestId,
|
| 159 |
+
type: 'message',
|
| 160 |
+
role: 'assistant',
|
| 161 |
+
model,
|
| 162 |
+
content: [],
|
| 163 |
+
stop_reason: null,
|
| 164 |
+
usage: { input_tokens: 0, output_tokens: 0 },
|
| 165 |
+
},
|
| 166 |
+
});
|
| 167 |
+
|
| 168 |
+
writeEvent('content_block_start', {
|
| 169 |
+
type: 'content_block_start',
|
| 170 |
+
index: blockIndex,
|
| 171 |
+
content_block: { type: 'text', text: '' },
|
| 172 |
+
});
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
const parser = createJsonStreamParser((obj) => {
|
| 176 |
+
switch (obj.type) {
|
| 177 |
+
case 'chunk':
|
| 178 |
+
ensureHeader();
|
| 179 |
+
writeEvent('content_block_delta', {
|
| 180 |
+
type: 'content_block_delta',
|
| 181 |
+
index: blockIndex,
|
| 182 |
+
delta: { type: 'text_delta', text: obj.data },
|
| 183 |
+
});
|
| 184 |
+
break;
|
| 185 |
+
|
| 186 |
+
case 'reasoningContent':
|
| 187 |
+
ensureHeader();
|
| 188 |
+
writeEvent('content_block_delta', {
|
| 189 |
+
type: 'content_block_delta',
|
| 190 |
+
index: blockIndex,
|
| 191 |
+
delta: { type: 'text_delta', text: obj.data },
|
| 192 |
+
});
|
| 193 |
+
break;
|
| 194 |
+
|
| 195 |
+
case 'finalResult':
|
| 196 |
+
ensureHeader();
|
| 197 |
+
writeEvent('content_block_stop', {
|
| 198 |
+
type: 'content_block_stop',
|
| 199 |
+
index: blockIndex,
|
| 200 |
+
});
|
| 201 |
+
writeEvent('message_delta', {
|
| 202 |
+
type: 'message_delta',
|
| 203 |
+
delta: { stop_reason: 'end_turn' },
|
| 204 |
+
usage: { output_tokens: 0 },
|
| 205 |
+
});
|
| 206 |
+
writeEvent('message_stop', { type: 'message_stop' });
|
| 207 |
+
res.end();
|
| 208 |
+
break;
|
| 209 |
+
|
| 210 |
+
case 'streamingError':
|
| 211 |
+
if (onStreamError) onStreamError(obj.data);
|
| 212 |
+
ensureHeader();
|
| 213 |
+
writeEvent('content_block_stop', { type: 'content_block_stop', index: blockIndex });
|
| 214 |
+
writeEvent('message_delta', {
|
| 215 |
+
type: 'message_delta',
|
| 216 |
+
delta: { stop_reason: 'end_turn' },
|
| 217 |
+
usage: { output_tokens: 0 },
|
| 218 |
+
});
|
| 219 |
+
writeEvent('message_stop', { type: 'message_stop' });
|
| 220 |
+
res.end();
|
| 221 |
+
break;
|
| 222 |
+
}
|
| 223 |
+
});
|
| 224 |
+
|
| 225 |
+
upstreamStream.on('data', (chunk) => parser.feed(chunk));
|
| 226 |
+
upstreamStream.on('end', () => {
|
| 227 |
+
parser.flush();
|
| 228 |
+
if (!res.writableEnded) {
|
| 229 |
+
ensureHeader();
|
| 230 |
+
writeEvent('content_block_stop', { type: 'content_block_stop', index: blockIndex });
|
| 231 |
+
writeEvent('message_delta', {
|
| 232 |
+
type: 'message_delta',
|
| 233 |
+
delta: { stop_reason: 'end_turn' },
|
| 234 |
+
usage: { output_tokens: 0 },
|
| 235 |
+
});
|
| 236 |
+
writeEvent('message_stop', { type: 'message_stop' });
|
| 237 |
+
res.end();
|
| 238 |
+
}
|
| 239 |
+
});
|
| 240 |
+
upstreamStream.on('error', () => {
|
| 241 |
+
if (!res.writableEnded) res.end();
|
| 242 |
+
});
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/**
|
| 246 |
+
* 消费 NDJSON 流,收集完整响应 (用于非流式请求)
|
| 247 |
+
*/
|
| 248 |
+
export function collectFullResponse(upstreamStream) {
|
| 249 |
+
return new Promise((resolve, reject) => {
|
| 250 |
+
let text = '';
|
| 251 |
+
let reasoning = '';
|
| 252 |
+
let actualModel = '';
|
| 253 |
+
|
| 254 |
+
const parser = createJsonStreamParser((obj) => {
|
| 255 |
+
switch (obj.type) {
|
| 256 |
+
case 'botType':
|
| 257 |
+
actualModel = obj.data;
|
| 258 |
+
break;
|
| 259 |
+
case 'chunk':
|
| 260 |
+
text += obj.data;
|
| 261 |
+
break;
|
| 262 |
+
case 'reasoningContent':
|
| 263 |
+
reasoning += obj.data;
|
| 264 |
+
break;
|
| 265 |
+
case 'finalResult':
|
| 266 |
+
if (obj.data?.mainText) text = obj.data.mainText;
|
| 267 |
+
break;
|
| 268 |
+
case 'streamingError':
|
| 269 |
+
reject(new Error(obj.data || 'Streaming error'));
|
| 270 |
+
break;
|
| 271 |
+
}
|
| 272 |
+
});
|
| 273 |
+
|
| 274 |
+
upstreamStream.on('data', (chunk) => parser.feed(chunk));
|
| 275 |
+
upstreamStream.on('end', () => {
|
| 276 |
+
parser.flush();
|
| 277 |
+
resolve({ text, reasoning, model: actualModel });
|
| 278 |
+
});
|
| 279 |
+
upstreamStream.on('error', reject);
|
| 280 |
+
});
|
| 281 |
+
}
|
ui.js
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* ui.js - Dashboard 控制台页面
|
| 3 |
+
*
|
| 4 |
+
* 内嵌完整的 CSS + JavaScript,无外部依赖
|
| 5 |
+
* 通过 GET / 或 GET /ui 访问
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
export function getDashboardHTML() {
|
| 9 |
+
return `<!DOCTYPE html>
|
| 10 |
+
<html lang="zh-CN">
|
| 11 |
+
<head>
|
| 12 |
+
<meta charset="UTF-8">
|
| 13 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 14 |
+
<title>ChatAIBot API Proxy</title>
|
| 15 |
+
<style>
|
| 16 |
+
:root {
|
| 17 |
+
--bg: #f0f2f5;
|
| 18 |
+
--bg-card: #ffffff;
|
| 19 |
+
--text: #1a1a2e;
|
| 20 |
+
--text-sec: #6b7280;
|
| 21 |
+
--border: #e5e7eb;
|
| 22 |
+
--radius: 12px;
|
| 23 |
+
--shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
|
| 24 |
+
--shadow-md: 0 4px 12px rgba(0,0,0,.08);
|
| 25 |
+
--c-active: #10b981;
|
| 26 |
+
--c-active-bg: #ecfdf5;
|
| 27 |
+
--c-inuse: #3b82f6;
|
| 28 |
+
--c-inuse-bg: #eff6ff;
|
| 29 |
+
--c-login: #f59e0b;
|
| 30 |
+
--c-login-bg: #fffbeb;
|
| 31 |
+
--c-exhaust: #9ca3af;
|
| 32 |
+
--c-exhaust-bg: #f3f4f6;
|
| 33 |
+
--c-dead: #ef4444;
|
| 34 |
+
--c-dead-bg: #fef2f2;
|
| 35 |
+
--c-total: #8b5cf6;
|
| 36 |
+
--c-total-bg: #f5f3ff;
|
| 37 |
+
}
|
| 38 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 39 |
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
|
| 40 |
+
|
| 41 |
+
.header { display: flex; justify-content: space-between; align-items: center; padding: 16px 28px; background: var(--bg-card); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 10; }
|
| 42 |
+
.logo { font-size: 1.2rem; font-weight: 700; letter-spacing: -.5px; }
|
| 43 |
+
.logo-sub { font-weight: 400; color: var(--text-sec); margin-left: 6px; font-size: .85rem; }
|
| 44 |
+
.header-right { display: flex; align-items: center; gap: 16px; font-size: .85rem; color: var(--text-sec); }
|
| 45 |
+
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--c-active); display: inline-block; }
|
| 46 |
+
.dot.loading { animation: pulse .6s ease-in-out infinite; }
|
| 47 |
+
@keyframes pulse { 0%,100% { opacity:1 } 50% { opacity:.3 } }
|
| 48 |
+
|
| 49 |
+
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
| 50 |
+
|
| 51 |
+
.stats { display: grid; grid-template-columns: repeat(6, 1fr); gap: 16px; margin-bottom: 28px; }
|
| 52 |
+
.card { background: var(--bg-card); border-radius: var(--radius); padding: 20px; box-shadow: var(--shadow); border-left: 4px solid transparent; text-align: center; transition: transform .15s, box-shadow .15s; cursor: default; }
|
| 53 |
+
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
|
| 54 |
+
.card-total { border-left-color: var(--c-total); }
|
| 55 |
+
.card-total .card-val { color: var(--c-total); }
|
| 56 |
+
.card-active { border-left-color: var(--c-active); }
|
| 57 |
+
.card-active .card-val { color: var(--c-active); }
|
| 58 |
+
.card-inuse { border-left-color: var(--c-inuse); }
|
| 59 |
+
.card-inuse .card-val { color: var(--c-inuse); }
|
| 60 |
+
.card-login { border-left-color: var(--c-login); }
|
| 61 |
+
.card-login .card-val { color: var(--c-login); }
|
| 62 |
+
.card-exhaust { border-left-color: var(--c-exhaust); }
|
| 63 |
+
.card-exhaust .card-val { color: var(--c-exhaust); }
|
| 64 |
+
.card-dead { border-left-color: var(--c-dead); }
|
| 65 |
+
.card-dead .card-val { color: var(--c-dead); }
|
| 66 |
+
.card-val { font-size: 2rem; font-weight: 700; line-height: 1; }
|
| 67 |
+
.card-label { font-size: .82rem; color: var(--text-sec); margin-top: 8px; }
|
| 68 |
+
|
| 69 |
+
.section { background: var(--bg-card); border-radius: var(--radius); box-shadow: var(--shadow); margin-bottom: 24px; overflow: hidden; }
|
| 70 |
+
.section-head { padding: 18px 24px; font-size: .95rem; font-weight: 600; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
| 71 |
+
|
| 72 |
+
.model-groups { padding: 20px 24px; }
|
| 73 |
+
.model-group { margin-bottom: 18px; }
|
| 74 |
+
.model-group:last-child { margin-bottom: 0; }
|
| 75 |
+
.mg-title { font-size: .85rem; font-weight: 600; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
|
| 76 |
+
.mg-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
| 77 |
+
.mg-count { font-weight: 400; color: var(--text-sec); font-size: .78rem; }
|
| 78 |
+
.mg-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
| 79 |
+
.chip { display: inline-block; padding: 4px 14px; border-radius: 20px; font-size: .78rem; font-family: "SF Mono", "Cascadia Code", Consolas, monospace; background: #f8f9fa; border: 1px solid var(--border); transition: background .15s; cursor: default; }
|
| 80 |
+
.chip:hover { background: #eef0f2; }
|
| 81 |
+
|
| 82 |
+
.table-wrap { overflow-x: auto; }
|
| 83 |
+
table { width: 100%; border-collapse: collapse; }
|
| 84 |
+
th { background: #f9fafb; text-align: left; padding: 12px 20px; font-weight: 600; font-size: .82rem; color: var(--text-sec); border-bottom: 2px solid var(--border); white-space: nowrap; }
|
| 85 |
+
td { padding: 11px 20px; border-bottom: 1px solid var(--border); font-size: .88rem; }
|
| 86 |
+
tr:last-child td { border-bottom: none; }
|
| 87 |
+
tr:hover td { background: #fafbfc; }
|
| 88 |
+
.email { font-family: monospace; font-size: .82rem; }
|
| 89 |
+
|
| 90 |
+
.badge { display: inline-block; padding: 2px 12px; border-radius: 12px; font-size: .75rem; font-weight: 500; }
|
| 91 |
+
.badge-active { background: var(--c-active-bg); color: var(--c-active); }
|
| 92 |
+
.badge-in_use { background: var(--c-inuse-bg); color: var(--c-inuse); }
|
| 93 |
+
.badge-needs_login { background: var(--c-login-bg); color: var(--c-login); }
|
| 94 |
+
.badge-exhausted { background: var(--c-exhaust-bg); color: var(--c-exhaust); }
|
| 95 |
+
.badge-dead { background: var(--c-dead-bg); color: var(--c-dead); }
|
| 96 |
+
|
| 97 |
+
.empty { text-align: center; padding: 40px; color: var(--text-sec); font-size: .9rem; }
|
| 98 |
+
|
| 99 |
+
/* Register bar */
|
| 100 |
+
.reg-bar { display: flex; align-items: center; gap: 10px; }
|
| 101 |
+
.reg-input { width: 60px; padding: 5px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: .85rem; text-align: center; outline: none; }
|
| 102 |
+
.reg-input:focus { border-color: var(--c-inuse); }
|
| 103 |
+
.btn { padding: 6px 16px; border: none; border-radius: 6px; font-size: .82rem; font-weight: 500; cursor: pointer; transition: background .15s, opacity .15s; }
|
| 104 |
+
.btn-primary { background: var(--c-inuse); color: #fff; }
|
| 105 |
+
.btn-primary:hover { background: #2563eb; }
|
| 106 |
+
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
| 107 |
+
.reg-msg { font-size: .78rem; color: var(--text-sec); }
|
| 108 |
+
.reg-msg.ok { color: var(--c-active); }
|
| 109 |
+
.reg-msg.err { color: var(--c-dead); }
|
| 110 |
+
.reg-select { padding: 5px 8px; border: 1px solid var(--border); border-radius: 6px; font-size: .82rem; outline: none; background: var(--bg-card); color: var(--text); max-width: 140px; }
|
| 111 |
+
.reg-select:focus { border-color: var(--c-inuse); }
|
| 112 |
+
.reg-label { font-size: .78rem; font-weight: 400; color: var(--text-sec); white-space: nowrap; }
|
| 113 |
+
|
| 114 |
+
/* Log panel */
|
| 115 |
+
.log-box { max-height: 260px; overflow-y: auto; padding: 14px 20px; background: #1a1a2e; font-family: "SF Mono", "Cascadia Code", Consolas, monospace; font-size: .75rem; line-height: 1.7; }
|
| 116 |
+
.log-line { color: #94a3b8; }
|
| 117 |
+
.log-line .log-time { color: #64748b; margin-right: 8px; }
|
| 118 |
+
.log-line.log-success { color: #34d399; }
|
| 119 |
+
.log-line.log-error { color: #f87171; }
|
| 120 |
+
.log-line.log-warn { color: #fbbf24; }
|
| 121 |
+
.log-empty { color: #475569; text-align: center; padding: 20px 0; }
|
| 122 |
+
|
| 123 |
+
/* Pagination */
|
| 124 |
+
.pager { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; border-top: 1px solid var(--border); font-size: .82rem; color: var(--text-sec); }
|
| 125 |
+
.pager-btns { display: flex; gap: 4px; }
|
| 126 |
+
.pager-btn { padding: 4px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-card); cursor: pointer; font-size: .78rem; color: var(--text); transition: background .15s; }
|
| 127 |
+
.pager-btn:hover { background: #f0f2f5; }
|
| 128 |
+
.pager-btn.active { background: var(--c-inuse); color: #fff; border-color: var(--c-inuse); }
|
| 129 |
+
.pager-btn:disabled { opacity: .4; cursor: not-allowed; }
|
| 130 |
+
|
| 131 |
+
/* Guide */
|
| 132 |
+
.guide { padding: 20px 24px; }
|
| 133 |
+
.guide-title { font-size: .88rem; font-weight: 600; margin-bottom: 12px; }
|
| 134 |
+
.guide-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
| 135 |
+
.guide-item { padding: 16px; background: #f8f9fa; border-radius: 8px; border: 1px solid var(--border); }
|
| 136 |
+
.guide-item h4 { font-size: .82rem; font-weight: 600; margin-bottom: 8px; color: var(--text); }
|
| 137 |
+
.guide-item p { font-size: .78rem; color: var(--text-sec); margin-bottom: 8px; line-height: 1.5; }
|
| 138 |
+
.guide-item code { display: block; padding: 10px 14px; background: #1a1a2e; color: #e2e8f0; border-radius: 6px; font-size: .75rem; font-family: "SF Mono", "Cascadia Code", Consolas, monospace; overflow-x: auto; white-space: pre; line-height: 1.6; }
|
| 139 |
+
|
| 140 |
+
@media (max-width: 900px) { .stats { grid-template-columns: repeat(3, 1fr); } .guide-grid { grid-template-columns: 1fr; } }
|
| 141 |
+
@media (max-width: 500px) { .stats { grid-template-columns: repeat(2, 1fr); } .container { padding: 16px; } .reg-bar { flex-wrap: wrap; } .reg-select { max-width: 100%; } }
|
| 142 |
+
</style>
|
| 143 |
+
</head>
|
| 144 |
+
<body>
|
| 145 |
+
|
| 146 |
+
<header class="header">
|
| 147 |
+
<div><span class="logo">ChatAIBot<span class="logo-sub">API Proxy</span></span></div>
|
| 148 |
+
<div class="header-right">
|
| 149 |
+
<span id="uptime">\u8FD0\u884C\u65F6\u95F4: --</span>
|
| 150 |
+
<span class="dot" id="dot"></span>
|
| 151 |
+
</div>
|
| 152 |
+
</header>
|
| 153 |
+
|
| 154 |
+
<div class="container">
|
| 155 |
+
<div class="stats">
|
| 156 |
+
<div class="card card-total"><div class="card-val" id="s-total">--</div><div class="card-label">\u603B\u8BA1\u8D26\u53F7</div></div>
|
| 157 |
+
<div class="card card-active"><div class="card-val" id="s-active">--</div><div class="card-label">\u53EF\u7528</div></div>
|
| 158 |
+
<div class="card card-inuse"><div class="card-val" id="s-inuse">--</div><div class="card-label">\u4F7F\u7528\u4E2D</div></div>
|
| 159 |
+
<div class="card card-login"><div class="card-val" id="s-login">--</div><div class="card-label">\u9700\u767B\u5F55</div></div>
|
| 160 |
+
<div class="card card-exhaust"><div class="card-val" id="s-exhaust">--</div><div class="card-label">\u5DF2\u8017\u5C3D</div></div>
|
| 161 |
+
<div class="card card-dead"><div class="card-val" id="s-dead">--</div><div class="card-label">\u5DF2\u5931\u6548</div></div>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
<!-- Register + Logs -->
|
| 165 |
+
<div class="section">
|
| 166 |
+
<div class="section-head">
|
| 167 |
+
<span>\u8D26\u53F7\u7BA1\u7406</span>
|
| 168 |
+
<div class="reg-bar">
|
| 169 |
+
<span class="reg-label">\u90AE\u7BB1</span>
|
| 170 |
+
<select class="reg-select" id="regProvider"><option value="">\u9ED8\u8BA4</option></select>
|
| 171 |
+
<span class="reg-label">\u6570\u91CF</span>
|
| 172 |
+
<input type="number" class="reg-input" id="regCount" value="5" min="1" max="50">
|
| 173 |
+
<span class="reg-label">\u5E76\u53D1</span>
|
| 174 |
+
<input type="number" class="reg-input" id="regConcurrency" value="0" min="0" max="20" title="0=\u4F7F\u7528\u9ED8\u8BA4\u503C">
|
| 175 |
+
<button class="btn btn-primary" id="regBtn" onclick="doRegister()">\u624B\u52A8\u6CE8\u518C</button>
|
| 176 |
+
<span class="reg-msg" id="regMsg"></span>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
<div class="log-box" id="logBox"><div class="log-empty">\u6682\u65E0\u65E5\u5FD7</div></div>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<div class="section">
|
| 183 |
+
<div class="section-head">\u53EF\u7528\u6A21\u578B</div>
|
| 184 |
+
<div class="model-groups" id="modelGroups"><div class="empty">\u52A0\u8F7D\u4E2D...</div></div>
|
| 185 |
+
</div>
|
| 186 |
+
|
| 187 |
+
<div class="section">
|
| 188 |
+
<div class="section-head">\u8D26\u53F7\u8BE6\u60C5</div>
|
| 189 |
+
<div class="table-wrap">
|
| 190 |
+
<table>
|
| 191 |
+
<thead><tr><th>#</th><th>\u90AE\u7BB1</th><th>\u72B6\u6001</th><th>\u5269\u4F59\u989D\u5EA6</th><th>\u6700\u540E\u4F7F\u7528</th></tr></thead>
|
| 192 |
+
<tbody id="tbody"><tr><td colspan="5" class="empty">\u52A0\u8F7D\u4E2D...</td></tr></tbody>
|
| 193 |
+
</table>
|
| 194 |
+
</div>
|
| 195 |
+
<div class="pager" id="pager" style="display:none">
|
| 196 |
+
<span class="pager-info" id="pagerInfo"></span>
|
| 197 |
+
<div class="pager-btns" id="pagerBtns"></div>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<!-- Usage Guide -->
|
| 202 |
+
<div class="section">
|
| 203 |
+
<div class="section-head">\u4F7F\u7528\u65B9\u6CD5</div>
|
| 204 |
+
<div class="guide">
|
| 205 |
+
<div class="guide-grid">
|
| 206 |
+
<div class="guide-item">
|
| 207 |
+
<h4>OpenAI \u683C\u5F0F (\u975E\u6D41\u5F0F)</h4>
|
| 208 |
+
<p>\u517C\u5BB9 OpenAI SDK / ChatBox / Cherry Studio \u7B49\u5BA2\u6237\u7AEF</p>
|
| 209 |
+
<code id="guide-openai"></code>
|
| 210 |
+
</div>
|
| 211 |
+
<div class="guide-item">
|
| 212 |
+
<h4>OpenAI \u683C\u5F0F (\u6D41\u5F0F)</h4>
|
| 213 |
+
<p>\u6DFB\u52A0 stream: true \u5F00\u542F SSE \u6D41\u5F0F\u8F93\u51FA</p>
|
| 214 |
+
<code id="guide-stream"></code>
|
| 215 |
+
</div>
|
| 216 |
+
<div class="guide-item">
|
| 217 |
+
<h4>Anthropic \u683C\u5F0F</h4>
|
| 218 |
+
<p>\u517C\u5BB9 Anthropic SDK \u7684 Messages API</p>
|
| 219 |
+
<code id="guide-anthropic"></code>
|
| 220 |
+
</div>
|
| 221 |
+
<div class="guide-item">
|
| 222 |
+
<h4>\u5BA2\u6237\u7AEF\u914D\u7F6E</h4>
|
| 223 |
+
<p>\u5728 Cherry Studio / ChatBox \u7B49\u5BA2\u6237\u7AEF\u4E2D\u914D\u7F6E\uFF1A</p>
|
| 224 |
+
<code id="guide-client"></code>
|
| 225 |
+
</div>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
+
|
| 231 |
+
<script>
|
| 232 |
+
var STATES = { active: "\u53EF\u7528", in_use: "\u4F7F\u7528\u4E2D", needs_login: "\u9700\u767B\u5F55", exhausted: "\u5DF2\u8017\u5C3D", dead: "\u5DF2\u5931\u6548" };
|
| 233 |
+
var GCOLORS = { OpenAI: "#10a37f", Anthropic: "#d97706", Google: "#4285f4", "\u5176\u4ED6": "#6366f1" };
|
| 234 |
+
var GORDER = ["OpenAI", "Anthropic", "Google", "\u5176\u4ED6"];
|
| 235 |
+
|
| 236 |
+
var PAGE_SIZE = 20;
|
| 237 |
+
var currentPage = 1;
|
| 238 |
+
var allAccounts = [];
|
| 239 |
+
var logSince = 0;
|
| 240 |
+
var BASE = location.origin;
|
| 241 |
+
|
| 242 |
+
// 动态填充使用指南 (地址随部署环境变化)
|
| 243 |
+
function fillGuide() {
|
| 244 |
+
var B = location.origin;
|
| 245 |
+
var q = '"';
|
| 246 |
+
document.getElementById("guide-openai").textContent = [
|
| 247 |
+
"curl " + B + "/v1/chat/completions \\\\",
|
| 248 |
+
" -H " + q + "Content-Type: application/json" + q + " \\\\",
|
| 249 |
+
" -d '{",
|
| 250 |
+
" " + q + "model" + q + ": " + q + "gpt-4o" + q + ",",
|
| 251 |
+
" " + q + "messages" + q + ": [",
|
| 252 |
+
" {" + q + "role" + q + ": " + q + "user" + q + ", " + q + "content" + q + ": " + q + "Hello" + q + "}",
|
| 253 |
+
" ]",
|
| 254 |
+
" }'"
|
| 255 |
+
].join("\\n");
|
| 256 |
+
document.getElementById("guide-stream").textContent = [
|
| 257 |
+
"curl " + B + "/v1/chat/completions \\\\",
|
| 258 |
+
" -H " + q + "Content-Type: application/json" + q + " \\\\",
|
| 259 |
+
" -d '{",
|
| 260 |
+
" " + q + "model" + q + ": " + q + "gpt-4o" + q + ",",
|
| 261 |
+
" " + q + "messages" + q + ": [",
|
| 262 |
+
" {" + q + "role" + q + ": " + q + "user" + q + ", " + q + "content" + q + ": " + q + "Hello" + q + "}",
|
| 263 |
+
" ],",
|
| 264 |
+
" " + q + "stream" + q + ": true",
|
| 265 |
+
" }'"
|
| 266 |
+
].join("\\n");
|
| 267 |
+
document.getElementById("guide-anthropic").textContent = [
|
| 268 |
+
"curl " + B + "/v1/messages \\\\",
|
| 269 |
+
" -H " + q + "Content-Type: application/json" + q + " \\\\",
|
| 270 |
+
" -d '{",
|
| 271 |
+
" " + q + "model" + q + ": " + q + "claude-4.6-sonnet" + q + ",",
|
| 272 |
+
" " + q + "max_tokens" + q + ": 1024,",
|
| 273 |
+
" " + q + "messages" + q + ": [",
|
| 274 |
+
" {" + q + "role" + q + ": " + q + "user" + q + ", " + q + "content" + q + ": " + q + "Hello" + q + "}",
|
| 275 |
+
" ]",
|
| 276 |
+
" }'"
|
| 277 |
+
].join("\\n");
|
| 278 |
+
document.getElementById("guide-client").textContent = [
|
| 279 |
+
"API \u5730\u5740: " + B,
|
| 280 |
+
"API Key: \u7559\u7A7A (\u65E0\u9700\u8BA4\u8BC1)",
|
| 281 |
+
"\u6A21\u578B: \u70B9\u51FB\u201C\u83B7\u53D6\u6A21\u578B\u5217\u8868\u201D\u81EA\u52A8\u62C9\u53D6",
|
| 282 |
+
"",
|
| 283 |
+
"\u652F\u6301\u7684\u63A5\u53E3:",
|
| 284 |
+
" OpenAI: POST /v1/chat/completions",
|
| 285 |
+
" Anthropic: POST /v1/messages",
|
| 286 |
+
" \u6A21\u578B\u5217\u8868: GET /v1/models"
|
| 287 |
+
].join("\\n");
|
| 288 |
+
}
|
| 289 |
+
fillGuide();
|
| 290 |
+
|
| 291 |
+
function fmt(s) {
|
| 292 |
+
s = Math.floor(s);
|
| 293 |
+
var d = Math.floor(s/86400), h = Math.floor(s%86400/3600), m = Math.floor(s%3600/60), sec = s%60;
|
| 294 |
+
var t = "";
|
| 295 |
+
if (d) t += d + "\u5929 ";
|
| 296 |
+
if (h || d) t += h + "\u65F6 ";
|
| 297 |
+
t += m + "\u5206 " + sec + "\u79D2";
|
| 298 |
+
return t;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
function ago(iso) {
|
| 302 |
+
if (!iso) return "\u4ECE\u672A\u4F7F\u7528";
|
| 303 |
+
var ms = Date.now() - new Date(iso).getTime();
|
| 304 |
+
if (ms < 60000) return "\u521A\u521A";
|
| 305 |
+
if (ms < 3600000) return Math.floor(ms/60000) + "\u5206\u949F\u524D";
|
| 306 |
+
if (ms < 86400000) return Math.floor(ms/3600000) + "\u5C0F\u65F6\u524D";
|
| 307 |
+
return new Date(iso).toLocaleString("zh-CN", { month:"2-digit", day:"2-digit", hour:"2-digit", minute:"2-digit" });
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
function mask(e) {
|
| 311 |
+
var p = e.split("@");
|
| 312 |
+
if (p[0].length <= 4) return e;
|
| 313 |
+
return p[0].substring(0,4) + "***@" + p[1];
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
function fmtLogTime(iso) {
|
| 317 |
+
return new Date(iso).toLocaleString("zh-CN", { hour:"2-digit", minute:"2-digit", second:"2-digit", hour12:false });
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
function esc(s) {
|
| 321 |
+
return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
function renderPage() {
|
| 325 |
+
var total = allAccounts.length;
|
| 326 |
+
var totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
| 327 |
+
if (currentPage > totalPages) currentPage = totalPages;
|
| 328 |
+
var start = (currentPage - 1) * PAGE_SIZE;
|
| 329 |
+
var pageItems = allAccounts.slice(start, start + PAGE_SIZE);
|
| 330 |
+
|
| 331 |
+
var html = "";
|
| 332 |
+
for (var i = 0; i < pageItems.length; i++) {
|
| 333 |
+
var a = pageItems[i];
|
| 334 |
+
html += "<tr><td>" + (start + i + 1) + '</td><td class="email" title="' + a.email + '">' + mask(a.email) + '</td><td><span class="badge badge-' + a.state + '">' + (STATES[a.state]||a.state) + "</span></td><td>" + (a.remaining != null ? a.remaining : "--") + "</td><td>" + ago(a.lastUsed) + "</td></tr>";
|
| 335 |
+
}
|
| 336 |
+
document.getElementById("tbody").innerHTML = html || '<tr><td colspan="5" class="empty">\u6682\u65E0\u8D26\u53F7</td></tr>';
|
| 337 |
+
|
| 338 |
+
var pager = document.getElementById("pager");
|
| 339 |
+
if (total <= PAGE_SIZE) {
|
| 340 |
+
pager.style.display = "none";
|
| 341 |
+
return;
|
| 342 |
+
}
|
| 343 |
+
pager.style.display = "flex";
|
| 344 |
+
document.getElementById("pagerInfo").textContent = "\u5171 " + total + " \u4E2A\u8D26\u53F7\uFF0C\u7B2C " + currentPage + "/" + totalPages + " \u9875";
|
| 345 |
+
|
| 346 |
+
var btns = "";
|
| 347 |
+
btns += '<button class="pager-btn" onclick="goPage(' + (currentPage-1) + ')"' + (currentPage <= 1 ? " disabled" : "") + '>\u4E0A\u4E00\u9875</button>';
|
| 348 |
+
var lo = Math.max(1, currentPage - 2);
|
| 349 |
+
var hi = Math.min(totalPages, currentPage + 2);
|
| 350 |
+
if (lo > 1) btns += '<button class="pager-btn" onclick="goPage(1)">1</button>';
|
| 351 |
+
if (lo > 2) btns += '<span style="padding:4px 6px;color:var(--text-sec)">...</span>';
|
| 352 |
+
for (var pg = lo; pg <= hi; pg++) {
|
| 353 |
+
btns += '<button class="pager-btn' + (pg === currentPage ? " active" : "") + '" onclick="goPage(' + pg + ')">' + pg + '</button>';
|
| 354 |
+
}
|
| 355 |
+
if (hi < totalPages - 1) btns += '<span style="padding:4px 6px;color:var(--text-sec)">...</span>';
|
| 356 |
+
if (hi < totalPages) btns += '<button class="pager-btn" onclick="goPage(' + totalPages + ')">' + totalPages + '</button>';
|
| 357 |
+
btns += '<button class="pager-btn" onclick="goPage(' + (currentPage+1) + ')"' + (currentPage >= totalPages ? " disabled" : "") + '>\u4E0B\u4E00\u9875</button>';
|
| 358 |
+
document.getElementById("pagerBtns").innerHTML = btns;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
function goPage(p) {
|
| 362 |
+
var totalPages = Math.max(1, Math.ceil(allAccounts.length / PAGE_SIZE));
|
| 363 |
+
if (p < 1 || p > totalPages) return;
|
| 364 |
+
currentPage = p;
|
| 365 |
+
renderPage();
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
async function refreshLogs() {
|
| 369 |
+
try {
|
| 370 |
+
var r = await fetch("/pool/logs?since=" + logSince).then(function(r){return r.json()});
|
| 371 |
+
var logs = r.logs || [];
|
| 372 |
+
if (logs.length === 0) return;
|
| 373 |
+
logSince = logs[logs.length - 1].id;
|
| 374 |
+
|
| 375 |
+
var box = document.getElementById("logBox");
|
| 376 |
+
var empty = box.querySelector(".log-empty");
|
| 377 |
+
if (empty) empty.remove();
|
| 378 |
+
|
| 379 |
+
for (var i = 0; i < logs.length; i++) {
|
| 380 |
+
var log = logs[i];
|
| 381 |
+
var cls = "log-line";
|
| 382 |
+
if (log.level === "success") cls += " log-success";
|
| 383 |
+
else if (log.level === "error") cls += " log-error";
|
| 384 |
+
else if (log.level === "warn") cls += " log-warn";
|
| 385 |
+
var line = document.createElement("div");
|
| 386 |
+
line.className = cls;
|
| 387 |
+
line.innerHTML = '<span class="log-time">' + fmtLogTime(log.time) + "</span>" + esc(log.message);
|
| 388 |
+
box.appendChild(line);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
while (box.children.length > 200) {
|
| 392 |
+
box.removeChild(box.firstChild);
|
| 393 |
+
}
|
| 394 |
+
box.scrollTop = box.scrollHeight;
|
| 395 |
+
} catch(e) {}
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
async function doRegister() {
|
| 399 |
+
var btn = document.getElementById("regBtn");
|
| 400 |
+
var msg = document.getElementById("regMsg");
|
| 401 |
+
var count = parseInt(document.getElementById("regCount").value) || 5;
|
| 402 |
+
var provider = document.getElementById("regProvider").value;
|
| 403 |
+
var concurrency = parseInt(document.getElementById("regConcurrency").value) || 0;
|
| 404 |
+
btn.disabled = true;
|
| 405 |
+
btn.textContent = "\u6CE8\u518C\u4E2D...";
|
| 406 |
+
msg.className = "reg-msg";
|
| 407 |
+
msg.textContent = "";
|
| 408 |
+
try {
|
| 409 |
+
var r = await fetch("/pool/register", {
|
| 410 |
+
method: "POST",
|
| 411 |
+
headers: { "Content-Type": "application/json" },
|
| 412 |
+
body: JSON.stringify({ count: count, provider: provider, concurrency: concurrency })
|
| 413 |
+
}).then(function(r){ return r.json() });
|
| 414 |
+
msg.className = "reg-msg " + (r.ok ? "ok" : "err");
|
| 415 |
+
msg.textContent = r.message;
|
| 416 |
+
} catch(e) {
|
| 417 |
+
msg.className = "reg-msg err";
|
| 418 |
+
msg.textContent = "\u8BF7\u6C42\u5931\u8D25: " + e.message;
|
| 419 |
+
} finally {
|
| 420 |
+
btn.disabled = false;
|
| 421 |
+
btn.textContent = "\u624B\u52A8\u6CE8\u518C";
|
| 422 |
+
}
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
async function refresh() {
|
| 426 |
+
var dot = document.getElementById("dot");
|
| 427 |
+
dot.classList.add("loading");
|
| 428 |
+
try {
|
| 429 |
+
var results = await Promise.allSettled([
|
| 430 |
+
fetch("/health").then(function(r){return r.json()}),
|
| 431 |
+
fetch("/pool/status").then(function(r){return r.json()}),
|
| 432 |
+
fetch("/v1/models").then(function(r){return r.json()})
|
| 433 |
+
]);
|
| 434 |
+
|
| 435 |
+
if (results[0].status === "fulfilled") {
|
| 436 |
+
document.getElementById("uptime").textContent = "\u8FD0\u884C\u65F6\u95F4: " + fmt(results[0].value.uptime);
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
if (results[1].status === "fulfilled") {
|
| 440 |
+
var p = results[1].value;
|
| 441 |
+
document.getElementById("s-total").textContent = p.total;
|
| 442 |
+
document.getElementById("s-active").textContent = p.active;
|
| 443 |
+
document.getElementById("s-inuse").textContent = p.in_use;
|
| 444 |
+
document.getElementById("s-login").textContent = p.needs_login;
|
| 445 |
+
document.getElementById("s-exhaust").textContent = p.exhausted;
|
| 446 |
+
document.getElementById("s-dead").textContent = p.dead;
|
| 447 |
+
|
| 448 |
+
var order = ["in_use","active","needs_login","exhausted","dead"];
|
| 449 |
+
allAccounts = (p.accounts||[]).slice().sort(function(a,b){ return order.indexOf(a.state) - order.indexOf(b.state); });
|
| 450 |
+
renderPage();
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
if (results[2].status === "fulfilled") {
|
| 454 |
+
var groups = {};
|
| 455 |
+
var models = results[2].value.data || [];
|
| 456 |
+
for (var j = 0; j < models.length; j++) {
|
| 457 |
+
var g = models[j].owned_by || "\u5176\u4ED6";
|
| 458 |
+
if (!groups[g]) groups[g] = [];
|
| 459 |
+
groups[g].push(models[j].id);
|
| 460 |
+
}
|
| 461 |
+
var mhtml = "";
|
| 462 |
+
for (var k = 0; k < GORDER.length; k++) {
|
| 463 |
+
var name = GORDER[k];
|
| 464 |
+
var items = groups[name];
|
| 465 |
+
if (!items || !items.length) continue;
|
| 466 |
+
var color = GCOLORS[name] || "#6366f1";
|
| 467 |
+
mhtml += '<div class="model-group"><div class="mg-title"><span class="mg-dot" style="background:' + color + '"></span>' + name + ' <span class="mg-count">' + items.length + " \u4E2A\u6A21\u578B</span></div><div class='mg-chips'>";
|
| 468 |
+
for (var l = 0; l < items.length; l++) {
|
| 469 |
+
mhtml += '<span class="chip">' + items[l] + "</span>";
|
| 470 |
+
}
|
| 471 |
+
mhtml += "</div></div>";
|
| 472 |
+
}
|
| 473 |
+
document.getElementById("modelGroups").innerHTML = mhtml || '<div class="empty">\u6682\u65E0\u6A21\u578B</div>';
|
| 474 |
+
}
|
| 475 |
+
} catch(e) {
|
| 476 |
+
console.error(e);
|
| 477 |
+
} finally {
|
| 478 |
+
dot.classList.remove("loading");
|
| 479 |
+
}
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
async function loadConfig() {
|
| 483 |
+
try {
|
| 484 |
+
var r = await fetch("/pool/config").then(function(r){return r.json()});
|
| 485 |
+
var sel = document.getElementById("regProvider");
|
| 486 |
+
var providers = r.providers || [];
|
| 487 |
+
var current = r.currentProvider || "";
|
| 488 |
+
var maxC = r.maxConcurrent || 3;
|
| 489 |
+
sel.innerHTML = "";
|
| 490 |
+
for (var i = 0; i < providers.length; i++) {
|
| 491 |
+
var opt = document.createElement("option");
|
| 492 |
+
opt.value = providers[i];
|
| 493 |
+
opt.textContent = providers[i] + (providers[i] === current ? " (\u9ED8\u8BA4)" : "");
|
| 494 |
+
if (providers[i] === current) opt.selected = true;
|
| 495 |
+
sel.appendChild(opt);
|
| 496 |
+
}
|
| 497 |
+
var conc = document.getElementById("regConcurrency");
|
| 498 |
+
conc.value = maxC;
|
| 499 |
+
conc.placeholder = maxC;
|
| 500 |
+
} catch(e) {}
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
loadConfig();
|
| 504 |
+
refresh();
|
| 505 |
+
refreshLogs();
|
| 506 |
+
setInterval(refresh, 5000);
|
| 507 |
+
setInterval(refreshLogs, 2000);
|
| 508 |
+
</script>
|
| 509 |
+
</body>
|
| 510 |
+
</html>`;
|
| 511 |
+
}
|