Selinore commited on
Commit
ba31439
·
verified ·
1 Parent(s): e9ac7f1

Create index.js

Browse files
Files changed (1) hide show
  1. index.js +844 -0
index.js ADDED
@@ -0,0 +1,844 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const http = require('http');
2
+ const crypto = require('crypto');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const ZAI_BASE = 'https://chat.z.ai';
7
+ const PORT = process.env.PORT || 7860;
8
+
9
+ // ================= 日志开关配置 =================
10
+ const IS_DEBUG = process.env.DEBUG !== 'false' && process.env.LOG_LEVEL !== 'error';
11
+
12
+ // 首字等待超时:10分钟;中途无流数据超时:10分钟
13
+ const FIRST_TOKEN_TIMEOUT_MS = 600000;
14
+ const STREAM_IDLE_TIMEOUT_MS = 600000;
15
+
16
+ // 持久化路径配置
17
+ const DATA_DIR = fs.existsSync('/data') ? '/data' : path.join(__dirname, 'data');
18
+ const TOKENS_FILE = path.join(DATA_DIR, 'tokens.json');
19
+
20
+ if (!fs.existsSync(DATA_DIR)) {
21
+ try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch (e) {}
22
+ }
23
+
24
+ function getTS() {
25
+ const d = new Date();
26
+ return d.toTimeString().split(' ')[0] + '.' + String(d.getMilliseconds()).padStart(3, '0');
27
+ }
28
+
29
+ function logInfo(...args) {
30
+ if (IS_DEBUG) console.log(`[${getTS()}]`, ...args);
31
+ }
32
+ function logWarn(...args) {
33
+ console.warn(`[${getTS()}]`, ...args);
34
+ }
35
+ function logError(...args) {
36
+ console.error(`[${getTS()}]`, ...args);
37
+ }
38
+
39
+ const COMMON_HEADERS = {
40
+ 'accept': 'application/json, text/event-stream',
41
+ 'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
42
+ 'content-type': 'application/json',
43
+ 'origin': ZAI_BASE,
44
+ 'referer': `${ZAI_BASE}/`,
45
+ 'sec-ch-ua': '"Chromium";v="132", "Not A(Brand";v="24", "Google Chrome";v="132"',
46
+ 'sec-ch-ua-mobile': '?0',
47
+ 'sec-ch-ua-platform': '"Windows"',
48
+ 'sec-fetch-dest': 'empty',
49
+ 'sec-fetch-mode': 'cors',
50
+ 'sec-fetch-site': 'same-origin',
51
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
52
+ 'x-region': 'overseas'
53
+ };
54
+
55
+ /* =========================================================
56
+ 1. JWT Payload 解析与 Token 状态管理
57
+ ========================================================= */
58
+ function parseTokenEmail(tokenStr) {
59
+ if (!tokenStr || typeof tokenStr !== 'string') return '';
60
+ try {
61
+ const parts = tokenStr.split('.');
62
+ if (parts.length < 2) return '';
63
+ let base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
64
+ while (base64.length % 4 !== 0) base64 += '=';
65
+ const payloadJson = Buffer.from(base64, 'base64').toString('utf8');
66
+ const payload = JSON.parse(payloadJson);
67
+ return payload.email || payload.id || '';
68
+ } catch (e) {
69
+ return '';
70
+ }
71
+ }
72
+
73
+ let tokensPool = [];
74
+
75
+ function formatToken(tokenStr) {
76
+ if (!tokenStr) return 'Guest/None';
77
+ const email = parseTokenEmail(tokenStr);
78
+ const emailTag = email ? ` [${email}]` : '';
79
+ if (tokenStr.length <= 16) return tokenStr + emailTag;
80
+ return `${tokenStr.slice(0, 8)}...${tokenStr.slice(-6)}${emailTag}`;
81
+ }
82
+
83
+ function loadTokens() {
84
+ if (fs.existsSync(TOKENS_FILE)) {
85
+ try {
86
+ const content = fs.readFileSync(TOKENS_FILE, 'utf8');
87
+ tokensPool = JSON.parse(content);
88
+ tokensPool.forEach(item => { item.email = parseTokenEmail(item.token); });
89
+ logInfo(`[+] Loaded ${tokensPool.length} token(s) from persistent storage (${TOKENS_FILE}).`);
90
+ return;
91
+ } catch (e) {
92
+ logError(`[-] Failed to parse ${TOKENS_FILE}, fallback to ENV:`, e.message);
93
+ }
94
+ }
95
+
96
+ const envVal = process.env.TOKENS || '';
97
+ let rawList = [];
98
+ try {
99
+ const parsed = JSON.parse(envVal);
100
+ if (Array.isArray(parsed)) rawList = parsed;
101
+ } catch (e) {
102
+ rawList = envVal.split(',').map(t => t.trim()).filter(Boolean);
103
+ }
104
+
105
+ tokensPool = rawList.map((t, idx) => {
106
+ const str = typeof t === 'string' ? t.trim() : '';
107
+ return {
108
+ id: `tok_${Date.now()}_${idx}_${Math.random().toString(36).slice(2, 6)}`,
109
+ token: str,
110
+ email: parseTokenEmail(str),
111
+ enabled: true,
112
+ errorCount: 0,
113
+ lastError: '',
114
+ inputTokens: 0,
115
+ outputTokens: 0,
116
+ totalRequests: 0,
117
+ lastUsed: null
118
+ };
119
+ }).filter(item => item.token);
120
+
121
+ saveTokens();
122
+ }
123
+
124
+ function saveTokens() {
125
+ try {
126
+ fs.writeFileSync(TOKENS_FILE, JSON.stringify(tokensPool, null, 2), 'utf8');
127
+ } catch (e) {
128
+ logError(`[-] Failed to save tokens to ${TOKENS_FILE}:`, e.message);
129
+ }
130
+ }
131
+
132
+ function getActiveToken() {
133
+ const enabledTokens = tokensPool.filter(t => t.enabled);
134
+ return enabledTokens.length > 0 ? enabledTokens[0].token : null;
135
+ }
136
+
137
+ function demoteToken(badToken, reason = 'Rate-limited/Exhausted') {
138
+ if (!badToken) return;
139
+ const idx = tokensPool.findIndex(t => t.token === badToken);
140
+ if (idx !== -1) {
141
+ const item = tokensPool[idx];
142
+ item.errorCount = (item.errorCount || 0) + 1;
143
+ item.lastError = `[${getTS()}] ${reason}`;
144
+ tokensPool.splice(idx, 1);
145
+ tokensPool.push(item);
146
+ saveTokens();
147
+ logWarn(`[!] Token 轮换 -> ${formatToken(badToken)} 已移至队尾。原因: ${reason}`);
148
+ }
149
+ }
150
+
151
+ function recordTokenUsage(tokenStr, inputChars = 0, outputChars = 0) {
152
+ if (!tokenStr) return;
153
+ const item = tokensPool.find(t => t.token === tokenStr);
154
+ if (item) {
155
+ item.totalRequests = (item.totalRequests || 0) + 1;
156
+ item.inputTokens = (item.inputTokens || 0) + Math.ceil(inputChars / 4);
157
+ item.outputTokens = (item.outputTokens || 0) + Math.ceil(outputChars / 4);
158
+ item.lastUsed = new Date().toISOString();
159
+ saveTokens();
160
+ }
161
+ }
162
+
163
+ loadTokens();
164
+
165
+ /* =========================================================
166
+ 2. 模型映射与配置
167
+ ========================================================= */
168
+ const modelAliasMap = new Map([
169
+ ['glm-5.3-flash', 'x-preview-l'],
170
+ ['glm-5.3-flash-preview', 'x-preview-l'],
171
+ ['glm-5.3', 'glm-5.3'],
172
+ ['glm-5.2', 'glm-5.2'],
173
+ ['glm-5-turbo', 'GLM-5-Turbo'],
174
+ ['glm-5v-turbo', 'GLM-5v-Turbo'],
175
+ ['glm-4.7', 'glm-4.7'],
176
+ ['glm-4.5-air', '0727-106B-API'],
177
+ ['glm-4.5', '0727-360B-API']
178
+ ]);
179
+
180
+ function parseModelConfig(rawModel) {
181
+ const wantSearch = rawModel.includes('-search');
182
+ let cleanModel = rawModel.replace(/-search/g, '');
183
+
184
+ let enableThinking = true;
185
+ let reasoningEffort = 'max';
186
+
187
+ if (cleanModel.includes('-thinking-off') || cleanModel.includes('-no-thinking')) {
188
+ enableThinking = false;
189
+ reasoningEffort = null;
190
+ cleanModel = cleanModel.replace('-thinking-off', '').replace('-no-thinking', '');
191
+ } else if (cleanModel.includes('-thinking-low')) {
192
+ enableThinking = true;
193
+ reasoningEffort = 'low';
194
+ cleanModel = cleanModel.replace('-thinking-low', '');
195
+ } else if (cleanModel.includes('-thinking-high')) {
196
+ enableThinking = true;
197
+ reasoningEffort = 'high';
198
+ cleanModel = cleanModel.replace('-thinking-high', '');
199
+ } else if (cleanModel.includes('-thinking-max')) {
200
+ enableThinking = true;
201
+ reasoningEffort = 'max';
202
+ cleanModel = cleanModel.replace('-thinking-max', '');
203
+ }
204
+
205
+ const key = cleanModel.toLowerCase();
206
+ const realBaseModel = modelAliasMap.get(key) || cleanModel;
207
+
208
+ return {
209
+ requestedModel: rawModel,
210
+ baseModel: realBaseModel,
211
+ webSearch: wantSearch,
212
+ enableThinking,
213
+ reasoningEffort
214
+ };
215
+ }
216
+
217
+ /* =========================================================
218
+ 3. 原生 HTTP 接口实现 (彻底替代 Puppeteer)
219
+ ========================================================= */
220
+ async function deleteChatSession(chatId, token) {
221
+ if (!chatId || !token) return;
222
+ try {
223
+ await fetch(`${ZAI_BASE}/api/v1/chats/${chatId}`, {
224
+ method: 'DELETE',
225
+ headers: { ...COMMON_HEADERS, 'Cookie': `token=${token}`, 'Authorization': `Bearer ${token}` }
226
+ });
227
+ logInfo(`[+] Cleaned Session ID: ${chatId}`);
228
+ } catch (e) {}
229
+ }
230
+
231
+ async function stopTask(messageId, token) {
232
+ if (!messageId || !token) return;
233
+ try {
234
+ await fetch(`${ZAI_BASE}/api/tasks/stop/${messageId}`, {
235
+ method: 'POST',
236
+ headers: { ...COMMON_HEADERS, 'Cookie': `token=${token}`, 'Authorization': `Bearer ${token}` },
237
+ body: JSON.stringify({})
238
+ });
239
+ logInfo(`[+] Stopped Task ID: ${messageId}`);
240
+ } catch (e) {}
241
+ }
242
+
243
+ // 创建新会话拿到 chatId 和初始 messageId
244
+ async function createNewChatSession(prompt, token, modelOptions) {
245
+ const msgId = crypto.randomUUID();
246
+ const payload = {
247
+ chat: {
248
+ auto_web_search: modelOptions.webSearch,
249
+ enable_thinking: modelOptions.enableThinking,
250
+ models: [modelOptions.baseModel],
251
+ history: {
252
+ currentId: msgId,
253
+ messages: {
254
+ [msgId]: {
255
+ id: msgId,
256
+ role: 'user',
257
+ content: prompt
258
+ }
259
+ }
260
+ }
261
+ }
262
+ };
263
+ if (modelOptions.enableThinking && modelOptions.reasoningEffort) {
264
+ payload.chat.reasoning_effort = modelOptions.reasoningEffort;
265
+ }
266
+
267
+ const res = await fetch(`${ZAI_BASE}/api/v1/chats/new`, {
268
+ method: 'POST',
269
+ headers: { ...COMMON_HEADERS, 'Cookie': `token=${token}`, 'Authorization': `Bearer ${token}` },
270
+ body: JSON.stringify(payload)
271
+ });
272
+
273
+ if (!res.ok) {
274
+ const errText = await res.text();
275
+ throw new Error(`UPSTREAM_HTTP_ERROR_${res.status}: ${errText.slice(0, 300)}`);
276
+ }
277
+
278
+ const data = await res.json();
279
+ const chatId = data?.id || data?.chat_id || data?.chat?.id;
280
+ return { chatId, msgId };
281
+ }
282
+
283
+ // 直连 /api/v2/chat/completions 获取流式 SSE
284
+ async function sendCompletionsHttp(prompt, token, modelOptions, chatId, msgId, abortSignal) {
285
+ const payload = {
286
+ models: [modelOptions.baseModel],
287
+ model: modelOptions.baseModel,
288
+ auto_web_search: modelOptions.webSearch,
289
+ enable_thinking: modelOptions.enableThinking,
290
+ chat_id: chatId,
291
+ features: {
292
+ auto_web_search: modelOptions.webSearch,
293
+ web_search: modelOptions.webSearch,
294
+ enable_thinking: modelOptions.enableThinking
295
+ }
296
+ };
297
+ if (modelOptions.enableThinking && modelOptions.reasoningEffort) {
298
+ payload.reasoning_effort = modelOptions.reasoningEffort;
299
+ payload.features.reasoning_effort = modelOptions.reasoningEffort;
300
+ }
301
+
302
+ const res = await fetch(`${ZAI_BASE}/api/v2/chat/completions`, {
303
+ method: 'POST',
304
+ headers: { ...COMMON_HEADERS, 'Cookie': `token=${token}`, 'Authorization': `Bearer ${token}` },
305
+ body: JSON.stringify(payload),
306
+ signal: abortSignal
307
+ });
308
+
309
+ if (!res.ok) {
310
+ const errText = await res.text();
311
+ throw new Error(`UPSTREAM_HTTP_ERROR_${res.status}: ${errText.slice(0, 300)}`);
312
+ }
313
+
314
+ return res.body; // 原生 ReadableStream
315
+ }
316
+
317
+ /* =========================================================
318
+ 4. 队列调度管理
319
+ ========================================================= */
320
+ let isProcessing = false;
321
+ let lastTaskTime = Date.now();
322
+ let currentProcessingContext = null;
323
+ const requestQueue = [];
324
+
325
+ function queueChatRequest(prompt, targetModel, modelOptions, streamCallback, taskContext) {
326
+ return new Promise((resolve, reject) => {
327
+ requestQueue.push({ prompt, targetModel, modelOptions, streamCallback, taskContext, resolve, reject });
328
+ processNextInQueue();
329
+ });
330
+ }
331
+
332
+ async function processNextInQueue() {
333
+ if (isProcessing || requestQueue.length === 0) return;
334
+ isProcessing = true;
335
+ lastTaskTime = Date.now();
336
+
337
+ const task = requestQueue.shift();
338
+ const { prompt, targetModel, modelOptions, streamCallback, taskContext, resolve, reject } = task;
339
+
340
+ currentProcessingContext = taskContext;
341
+
342
+ if (taskContext.isAborted) {
343
+ isProcessing = false;
344
+ currentProcessingContext = null;
345
+ processNextInQueue();
346
+ return;
347
+ }
348
+
349
+ // 基础防风控间隔
350
+ const randomDelay = 1500 + Math.floor(Math.random() * 2000);
351
+ await new Promise(r => setTimeout(r, randomDelay));
352
+
353
+ const MAX_ATTEMPTS = 3;
354
+ let attempt = 0;
355
+ let executionSuccess = false;
356
+ let lastError = null;
357
+
358
+ while (attempt < MAX_ATTEMPTS && !executionSuccess && !taskContext.isAborted) {
359
+ attempt++;
360
+ const activeToken = getActiveToken();
361
+ taskContext.activeToken = activeToken;
362
+
363
+ if (!activeToken) {
364
+ lastError = new Error('No enabled token available in pool.');
365
+ break;
366
+ }
367
+
368
+ const abortController = new AbortController();
369
+ taskContext.abortController = abortController;
370
+
371
+ try {
372
+ logInfo(`[>] HTTP Task (Attempt ${attempt}/${MAX_ATTEMPTS}) Model="${targetModel}" Token="${formatToken(activeToken)}"`);
373
+
374
+ // 1. 创建 Chat 会话
375
+ const { chatId, msgId } = await createNewChatSession(prompt, activeToken, modelOptions);
376
+ taskContext.createdChatId = chatId;
377
+ taskContext.capturedMessageId = msgId;
378
+
379
+ if (taskContext.isAborted) break;
380
+
381
+ // 2. 直连 HTTP SSE 流
382
+ const stream = await sendCompletionsHttp(prompt, activeToken, modelOptions, chatId, msgId, abortController.signal);
383
+
384
+ // 3. 读取 Node.js ReadableStream / WebStream
385
+ const reader = stream.getReader();
386
+ const decoder = new TextDecoder('utf-8');
387
+
388
+ while (true) {
389
+ const { done, value } = await reader.read();
390
+ if (done) {
391
+ if (streamCallback) streamCallback('[DONE]');
392
+ break;
393
+ }
394
+ const chunk = decoder.decode(value, { stream: true });
395
+ if (streamCallback) streamCallback(chunk);
396
+ }
397
+
398
+ executionSuccess = true;
399
+ resolve();
400
+ } catch (err) {
401
+ lastError = err;
402
+ logWarn(`[-] Attempt ${attempt} failed: ${err.message}`);
403
+
404
+ const errStr = String(err.message);
405
+ const is405 = errStr.includes('405');
406
+ const isWaf = is405 || errStr.includes('403') || errStr.includes('401');
407
+
408
+ if (isWaf) {
409
+ demoteToken(activeToken, `WAF_HTTP_STATUS_${errStr.slice(0, 30)}`);
410
+ }
411
+
412
+ if (attempt < MAX_ATTEMPTS && !taskContext.isAborted) {
413
+ const wait = is405 ? 15 : 2;
414
+ await new Promise(r => setTimeout(r, wait * 1000));
415
+ }
416
+ } finally {
417
+ if (taskContext.createdChatId && activeToken) {
418
+ deleteChatSession(taskContext.createdChatId, activeToken).catch(() => {});
419
+ }
420
+ }
421
+ }
422
+
423
+ if (!executionSuccess && !taskContext.isAborted) {
424
+ if (lastError && String(lastError.message).includes('405')) {
425
+ logWarn(`[!] 3 次尝试均遭遇 405,触发兜底输出 "123"`);
426
+ taskContext.fallbackTo123 = true;
427
+ resolve();
428
+ } else {
429
+ reject(lastError);
430
+ }
431
+ }
432
+
433
+ currentProcessingContext = null;
434
+ isProcessing = false;
435
+ processNextInQueue();
436
+ }
437
+
438
+ /* =========================================================
439
+ 5. OpenAI 格式兼容处理与路由 (handleChat)
440
+ ========================================================= */
441
+ function makeChunk(id, model, content, reasoningContent, finishReason) {
442
+ const chunk = {
443
+ id, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model,
444
+ choices: [{ index: 0, delta: {}, finish_reason: finishReason || null }],
445
+ };
446
+ if (content !== undefined) chunk.choices[0].delta.content = content;
447
+ if (reasoningContent !== undefined) chunk.choices[0].delta.reasoning_content = reasoningContent;
448
+ return chunk;
449
+ }
450
+
451
+ function makeCompletion(id, model, content, reasoningContent) {
452
+ const msg = { role: 'assistant', content };
453
+ if (reasoningContent) msg.reasoning_content = reasoningContent;
454
+ return {
455
+ id, object: 'chat.completion', created: Math.floor(Date.now() / 1000), model,
456
+ choices: [{ index: 0, message: msg, finish_reason: 'stop' }],
457
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
458
+ };
459
+ }
460
+
461
+ function parseBody(req) {
462
+ return new Promise((resolve) => {
463
+ let body = '';
464
+ req.on('data', c => body += c);
465
+ req.on('end', () => { try { resolve(JSON.parse(body || '{}')); } catch (e) { resolve({}); } });
466
+ });
467
+ }
468
+
469
+ function buildPromptFromMessages(messages) {
470
+ if (!Array.isArray(messages) || messages.length === 0) return '';
471
+ let systemText = '';
472
+ const conversationLines = [];
473
+
474
+ for (const m of messages) {
475
+ let contentStr = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '');
476
+ if (m.role === 'system') systemText += (systemText ? '\n' : '') + contentStr;
477
+ else conversationLines.push(`${m.role === 'user' ? 'User' : 'Assistant'}: ${contentStr}`);
478
+ }
479
+
480
+ if (!systemText && conversationLines.length === 1 && messages[0].role === 'user') return messages[0].content;
481
+ return (systemText ? `[System Instruction]\n${systemText}\n\n` : '') + conversationLines.join('\n\n');
482
+ }
483
+
484
+ async function handleChat(req, res) {
485
+ let isFinished = false;
486
+ const taskContext = {
487
+ activeToken: null,
488
+ capturedMessageId: null,
489
+ createdChatId: null,
490
+ abortController: null,
491
+ isAborted: false,
492
+ fallbackTo123: false
493
+ };
494
+
495
+ try {
496
+ const body = await parseBody(req);
497
+ const model = body.model || 'glm-5.3';
498
+ const modelOptions = parseModelConfig(model);
499
+ const messages = body.messages || [];
500
+ const wantStream = body.stream ?? false;
501
+ const chatId = 'chatcmpl-' + crypto.randomUUID().replace(/-/g, '').slice(0, 24);
502
+
503
+ const prompt = buildPromptFromMessages(messages);
504
+
505
+ if (!getActiveToken()) {
506
+ res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
507
+ return res.end(JSON.stringify({ error: { message: "No enabled token available in pool." } }));
508
+ }
509
+
510
+ let fullContent = '';
511
+ let fullReasoning = '';
512
+ let sseBuffer = '';
513
+ let contentStreamBuffer = '';
514
+ const STOP_WORD_REGEX = /(\n?\s*(?:User|用户|Human|HumanMessage)\s*[::])/i;
515
+
516
+ if (wantStream) {
517
+ res.writeHead(200, {
518
+ 'Content-Type': 'text/event-stream',
519
+ 'Cache-Control': 'no-cache',
520
+ 'Connection': 'keep-alive',
521
+ 'X-Accel-Buffering': 'no',
522
+ 'Access-Control-Allow-Origin': '*',
523
+ });
524
+ res.write(`data: ${JSON.stringify(makeChunk(chatId, model, '', undefined, null))}\n\n`);
525
+ }
526
+
527
+ const markFinished = (reason = 'normal') => {
528
+ if (!isFinished) {
529
+ isFinished = true;
530
+ if (wantStream && !res.writableEnded && contentStreamBuffer) {
531
+ res.write(`data: ${JSON.stringify(makeChunk(chatId, model, contentStreamBuffer, undefined, null))}\n\n`);
532
+ contentStreamBuffer = '';
533
+ }
534
+ if (wantStream && !res.writableEnded) {
535
+ res.write(`data: ${JSON.stringify(makeChunk(chatId, model, undefined, undefined, 'stop'))}\n\n`);
536
+ res.write('data: [DONE]\n\n');
537
+ res.end();
538
+ }
539
+ if (taskContext.capturedMessageId && taskContext.activeToken) {
540
+ stopTask(taskContext.capturedMessageId, taskContext.activeToken).catch(() => {});
541
+ }
542
+ }
543
+ };
544
+
545
+ req.on('close', () => {
546
+ if (!isFinished) {
547
+ taskContext.isAborted = true;
548
+ if (taskContext.abortController) taskContext.abortController.abort();
549
+ markFinished('client_disconnected');
550
+ }
551
+ });
552
+
553
+ const processLine = (line) => {
554
+ if (!line.startsWith('data: ')) return;
555
+ const raw = line.slice(6).trim();
556
+ if (!raw || raw === '[DONE]') return;
557
+
558
+ let parsed;
559
+ try { parsed = JSON.parse(raw); } catch { return; }
560
+
561
+ const d = (parsed.type === 'chat:completion' && parsed.data) ? parsed.data : parsed;
562
+ if (!d || typeof d !== 'object') return;
563
+
564
+ if (d.phase === 'done' || d.done === true) {
565
+ markFinished('phase_done');
566
+ return;
567
+ }
568
+
569
+ const delta = d.delta_content || d.content || d.reasoning_content || d.thinking_content || d.edit_content || d.text || '';
570
+ const phase = (d.phase || '').toLowerCase();
571
+ const isThinking = ['thinking', 'thought', 'think', 'reasoning'].includes(phase) || !!d.reasoning_content || !!d.thinking_content;
572
+
573
+ if (delta) {
574
+ if (isThinking) {
575
+ if (modelOptions.enableThinking !== false) {
576
+ fullReasoning += delta;
577
+ if (wantStream && !res.writableEnded) {
578
+ res.write(`data: ${JSON.stringify(makeChunk(chatId, model, undefined, delta, null))}\n\n`);
579
+ }
580
+ }
581
+ } else {
582
+ contentStreamBuffer += delta;
583
+ const match = contentStreamBuffer.match(STOP_WORD_REGEX);
584
+ if (match) {
585
+ const validPart = contentStreamBuffer.slice(0, match.index);
586
+ fullContent += validPart;
587
+ if (wantStream && !res.writableEnded && validPart) {
588
+ res.write(`data: ${JSON.stringify(makeChunk(chatId, model, validPart, undefined, null))}\n\n`);
589
+ }
590
+ contentStreamBuffer = '';
591
+ markFinished('hallucination_user_detected');
592
+ return;
593
+ }
594
+
595
+ const SAFE_WINDOW = 16;
596
+ if (contentStreamBuffer.length > SAFE_WINDOW) {
597
+ const emitLen = contentStreamBuffer.length - SAFE_WINDOW;
598
+ const toEmit = contentStreamBuffer.slice(0, emitLen);
599
+ contentStreamBuffer = contentStreamBuffer.slice(emitLen);
600
+ fullContent += toEmit;
601
+ if (wantStream && !res.writableEnded) {
602
+ res.write(`data: ${JSON.stringify(makeChunk(chatId, model, toEmit, undefined, null))}\n\n`);
603
+ }
604
+ }
605
+ }
606
+ }
607
+ };
608
+
609
+ const handleSseChunk = (rawChunk) => {
610
+ if (isFinished || taskContext.isAborted) return;
611
+ if (rawChunk === '[DONE]') {
612
+ if (sseBuffer.trim()) { processLine(sseBuffer.trim()); sseBuffer = ''; }
613
+ markFinished('sse_done_signal');
614
+ return;
615
+ }
616
+ sseBuffer += rawChunk;
617
+ const lines = sseBuffer.split(/\r?\n/);
618
+ sseBuffer = lines.pop() || '';
619
+ for (const line of lines) {
620
+ if (isFinished) break;
621
+ processLine(line);
622
+ }
623
+ };
624
+
625
+ await queueChatRequest(prompt, model, modelOptions, handleSseChunk, taskContext);
626
+
627
+ if (taskContext.fallbackTo123) {
628
+ if (wantStream && !res.writableEnded) {
629
+ res.write(`data: ${JSON.stringify(makeChunk(chatId, model, "123", undefined, null))}\n\n`);
630
+ res.write(`data: ${JSON.stringify(makeChunk(chatId, model, undefined, undefined, 'stop'))}\n\n`);
631
+ res.write('data: [DONE]\n\n');
632
+ return res.end();
633
+ } else if (!wantStream && !res.writableEnded) {
634
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
635
+ return res.end(JSON.stringify(makeCompletion(chatId, model, "123", undefined)));
636
+ }
637
+ }
638
+
639
+ if (!wantStream && !res.writableEnded && !taskContext.isAborted) {
640
+ if (contentStreamBuffer) fullContent += contentStreamBuffer;
641
+ const match = fullContent.match(STOP_WORD_REGEX);
642
+ if (match) fullContent = fullContent.slice(0, match.index).trimEnd();
643
+
644
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
645
+ res.end(JSON.stringify(makeCompletion(chatId, model, fullContent, modelOptions.enableThinking !== false ? (fullReasoning || undefined) : undefined)));
646
+ }
647
+
648
+ if (taskContext.activeToken) {
649
+ recordTokenUsage(taskContext.activeToken, prompt.length, fullContent.length + fullReasoning.length);
650
+ }
651
+
652
+ } catch (e) {
653
+ if (!res.headersSent) {
654
+ res.writeHead(500, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
655
+ res.end(JSON.stringify({ error: { message: e.message } }));
656
+ } else if (!res.writableEnded) {
657
+ res.write(`data: ${JSON.stringify({ error: { message: e.message } })}\n\n`);
658
+ res.end();
659
+ }
660
+ }
661
+ }
662
+
663
+ /* =========================================================
664
+ 6. Dashboard 页面与轻量 Server 启动
665
+ ========================================================= */
666
+ const ADMIN_HTML = `
667
+ <!DOCTYPE html>
668
+ <html lang="zh-CN">
669
+ <head>
670
+ <meta charset="UTF-8">
671
+ <title>Token Dashboard (Pure HTTP Engine)</title>
672
+ <style>
673
+ body { font-family: -apple-system, sans-serif; background: #f0f2f5; margin: 0; padding: 20px; color: #1f2937; }
674
+ .container { max-width: 1200px; margin: 0 auto; }
675
+ .card { background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
676
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; }
677
+ .stat-card { background: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
678
+ .stat-card .num { font-size: 22px; font-weight: bold; color: #2563eb; margin-top: 5px; }
679
+ textarea { width: 100%; height: 90px; border: 1px solid #d1d5db; border-radius: 6px; padding: 10px; font-family: monospace; box-sizing: border-box; }
680
+ button { background: #2563eb; color: white; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; margin-right: 5px; }
681
+ button.danger { background: #ef4444; }
682
+ button.secondary { background: #6b7280; }
683
+ table { width: 100%; border-collapse: collapse; margin-top: 10px; }
684
+ th, td { text-align: left; padding: 12px; border-bottom: 1px solid #e5e7eb; font-size: 14px; }
685
+ .badge { padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; }
686
+ .bg-green { background: #d1fae5; color: #065f46; }
687
+ .bg-red { background: #fee2e2; color: #991b1b; }
688
+ .tok-text { font-family: monospace; font-size: 13px; }
689
+ </style>
690
+ </head>
691
+ <body>
692
+ <div class="container">
693
+ <h2>Token 状态仪表盘 (轻量无头 HTTP 引擎)</h2>
694
+ <div class="stats-grid">
695
+ <div class="stat-card"><div>总 Token 数</div><div class="num" id="statTotal">0</div></div>
696
+ <div class="stat-card"><div>已启用 Token</div><div class="num" id="statEnabled" style="color:#10b981">0</div></div>
697
+ <div class="stat-card"><div>累计总调用次数</div><div class="num" id="statRequests" style="color:#8b5cf6">0</div></div>
698
+ </div>
699
+ <div class="card">
700
+ <h3>批量添加 Token</h3>
701
+ <textarea id="newTokenInput" placeholder="请输入 Token,每行一个..."></textarea>
702
+ <div style="margin-top:10px"><button onclick="addTokens()">批量添加</button></div>
703
+ </div>
704
+ <div class="card">
705
+ <h3>Token 列表</h3>
706
+ <table>
707
+ <thead>
708
+ <tr><th>账号/邮箱</th><th>Token</th><th>状态</th><th>调用次数</th><th>错误数</th><th>最后错误</th><th>操作</th></tr>
709
+ </thead>
710
+ <tbody id="tokenTable"></tbody>
711
+ </table>
712
+ </div>
713
+ </div>
714
+ <script>
715
+ function formatTok(s) { return !s || s.length <= 16 ? s : s.slice(0, 8) + '...' + s.slice(-6); }
716
+ async function loadData() {
717
+ const res = await fetch('/admin/api/tokens');
718
+ const tokens = await res.json();
719
+ document.getElementById('statTotal').innerText = tokens.length;
720
+ document.getElementById('statEnabled').innerText = tokens.filter(t => t.enabled).length;
721
+ document.getElementById('statRequests').innerText = tokens.reduce((a, b) => a + (b.totalRequests || 0), 0);
722
+ const tbody = document.getElementById('tokenTable');
723
+ tbody.innerHTML = '';
724
+ tokens.forEach(t => {
725
+ const tr = document.createElement('tr');
726
+ tr.innerHTML = \`
727
+ <td>\${t.email || '-'}</td>
728
+ <td class="tok-text">\${formatTok(t.token)}</td>
729
+ <td><span class="badge \${t.enabled ? 'bg-green' : 'bg-red'}">\${t.enabled ? '已启用' : '已禁用'}</span></td>
730
+ <td>\${t.totalRequests || 0}</td>
731
+ <td>\${t.errorCount || 0}</td>
732
+ <td><small style="color:#ef4444">\${t.lastError || '-'}</small></td>
733
+ <td>
734
+ <button class="\${t.enabled ? 'secondary' : ''}" onclick="toggleToken('\${t.id}', \${!t.enabled})">\${t.enabled ? '禁用' : '启用'}</button>
735
+ <button class="danger" onclick="deleteToken('\${t.id}')">删除</button>
736
+ </td>
737
+ \`;
738
+ tbody.appendChild(tr);
739
+ });
740
+ }
741
+ async function addTokens() {
742
+ const text = document.getElementById('newTokenInput').value;
743
+ if (!text.trim()) return;
744
+ await fetch('/admin/api/tokens/add', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ rawText: text }) });
745
+ document.getElementById('newTokenInput').value = '';
746
+ loadData();
747
+ }
748
+ async function toggleToken(id, enabled) {
749
+ await fetch('/admin/api/tokens/toggle', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id, enabled }) });
750
+ loadData();
751
+ }
752
+ async function deleteToken(id) {
753
+ if (!confirm('确定删除?')) return;
754
+ await fetch('/admin/api/tokens/delete', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id }) });
755
+ loadData();
756
+ }
757
+ loadData();
758
+ </script>
759
+ </body>
760
+ </html>
761
+ `;
762
+
763
+ const server = http.createServer(async (req, res) => {
764
+ const urlObj = new URL(req.url, `http://${req.headers.host}`);
765
+ let reqPath = urlObj.pathname.replace(/\/$/, '') || '/';
766
+
767
+ if (req.method === 'OPTIONS') {
768
+ res.writeHead(200, {
769
+ 'Access-Control-Allow-Origin': '*',
770
+ 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
771
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
772
+ });
773
+ return res.end();
774
+ }
775
+
776
+ if (reqPath === '/' || reqPath === '/admin') {
777
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
778
+ return res.end(ADMIN_HTML);
779
+ }
780
+
781
+ if (reqPath === '/admin/api/tokens' && req.method === 'GET') {
782
+ res.writeHead(200, { 'Content-Type': 'application/json' });
783
+ return res.end(JSON.stringify(tokensPool));
784
+ }
785
+
786
+ if (reqPath === '/admin/api/tokens/add' && req.method === 'POST') {
787
+ const body = await parseBody(req);
788
+ const raw = body.rawText || '';
789
+ const list = raw.split(/[\n,]/).map(s => s.trim()).filter(Boolean);
790
+ list.forEach((t, i) => {
791
+ if (!tokensPool.some(x => x.token === t)) {
792
+ tokensPool.push({
793
+ id: `tok_${Date.now()}_${i}_${Math.random().toString(36).slice(2, 6)}`,
794
+ token: t,
795
+ email: parseTokenEmail(t),
796
+ enabled: true,
797
+ errorCount: 0,
798
+ lastError: '',
799
+ inputTokens: 0,
800
+ outputTokens: 0,
801
+ totalRequests: 0,
802
+ lastUsed: null
803
+ });
804
+ }
805
+ });
806
+ saveTokens();
807
+ res.writeHead(200, { 'Content-Type': 'application/json' });
808
+ return res.end(JSON.stringify({ success: true }));
809
+ }
810
+
811
+ if (reqPath === '/admin/api/tokens/toggle' && req.method === 'POST') {
812
+ const body = await parseBody(req);
813
+ const item = tokensPool.find(t => t.id === body.id);
814
+ if (item) { item.enabled = !!body.enabled; saveTokens(); }
815
+ res.writeHead(200, { 'Content-Type': 'application/json' });
816
+ return res.end(JSON.stringify({ success: true }));
817
+ }
818
+
819
+ if (reqPath === '/admin/api/tokens/delete' && req.method === 'POST') {
820
+ const body = await parseBody(req);
821
+ tokensPool = tokensPool.filter(t => t.id !== body.id);
822
+ saveTokens();
823
+ res.writeHead(200, { 'Content-Type': 'application/json' });
824
+ return res.end(JSON.stringify({ success: true }));
825
+ }
826
+
827
+ if (reqPath === '/v1/models' || reqPath === '/models') {
828
+ const models = Array.from(modelAliasMap.keys()).map(m => ({ id: m, object: 'model', created: 1677610602, owned_by: 'zhipu' }));
829
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
830
+ return res.end(JSON.stringify({ object: 'list', data: models }));
831
+ }
832
+
833
+ if ((reqPath === '/v1/chat/completions' || reqPath === '/chat/completions') && req.method === 'POST') {
834
+ return handleChat(req, res);
835
+ }
836
+
837
+ res.writeHead(404, { 'Content-Type': 'application/json' });
838
+ res.end(JSON.stringify({ error: 'Not found' }));
839
+ });
840
+
841
+ server.listen(PORT, () => {
842
+ console.log(`\n> Pure HTTP Engine listening on port ${PORT}`);
843
+ console.log(`> UI Dashboard: http://localhost:${PORT}/\n`);
844
+ });