bingn commited on
Commit
fd211b3
·
verified ·
1 Parent(s): 8b48c98

Upload 17 files

Browse files
Files changed (17) hide show
  1. .dockerignore +8 -0
  2. Dockerfile +25 -0
  3. README.md +19 -10
  4. account.js +70 -0
  5. anthropic-adapter.js +169 -0
  6. chat.js +142 -0
  7. config.js +204 -0
  8. http.js +230 -0
  9. mail.js +1368 -0
  10. message-convert.js +120 -0
  11. openai-adapter.js +182 -0
  12. package.json +13 -0
  13. pool.js +516 -0
  14. protocol.js +287 -0
  15. server.js +211 -0
  16. stream-transform.js +281 -0
  17. 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: Ikun2
3
- emoji: 👀
4
- colorFrom: yellow
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
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
+ }