holyterra commited on
Commit
d52274a
·
verified ·
1 Parent(s): 9134c31

Delete unified-server.js

Browse files
Files changed (1) hide show
  1. unified-server.js +0 -1846
unified-server.js DELETED
@@ -1,1846 +0,0 @@
1
- const express = require('express');
2
- const WebSocket = require('ws');
3
- const http = require('http');
4
- const { EventEmitter } = require('events');
5
- const fs = require('fs');
6
- const path = require('path');
7
- const { firefox } = require('playwright');
8
- const os = require('os');
9
-
10
-
11
- // ===================================================================================
12
- // 认证源管理模块 (已升级以支持动态管理)
13
- // ===================================================================================
14
-
15
- class AuthSource {
16
- constructor(logger) {
17
- this.logger = logger;
18
- this.authMode = 'file'; // 默认模式
19
- this.initialIndices = []; // 启动时发现的索引
20
- this.runtimeAuths = new Map(); // 用于动态添加的账号
21
-
22
- if (process.env.AUTH_JSON_1) {
23
- this.authMode = 'env';
24
- this.logger.info('[认证] 检测到 AUTH_JSON_1 环境变量,切换到环境变量认证模式。');
25
- } else {
26
- this.logger.info('[认证] 未检测到环境变量认证,将使用 "auth/" 目录下的文件。');
27
- }
28
-
29
- this._discoverAvailableIndices();
30
-
31
- if (this.getAvailableIndices().length === 0) {
32
- this.logger.error(`[认证] 致命错误:在 '${this.authMode}' 模式下未找到任何有效的认证源。`);
33
- throw new Error("未找到有效的认证源。");
34
- }
35
- }
36
-
37
- _discoverAvailableIndices() {
38
- let indices = [];
39
- if (this.authMode === 'env') {
40
- const regex = /^AUTH_JSON_(\d+)$/;
41
- for (const key in process.env) {
42
- const match = key.match(regex);
43
- // 修正:正确解析捕获组 (match[1]) 而不是整个匹配对象
44
- if (match && match[1]) {
45
- indices.push(parseInt(match[1], 10));
46
- }
47
- }
48
- } else { // 'file' 模式
49
- const authDir = path.join(__dirname, 'auth');
50
- if (!fs.existsSync(authDir)) {
51
- this.logger.warn('[认证] "auth/" 目录不存在。');
52
- this.initialIndices = [];
53
- return;
54
- }
55
- try {
56
- const files = fs.readdirSync(authDir);
57
- const authFiles = files.filter(file => /^auth-\d+\.json$/.test(file));
58
- // 修正:正确解析文件名中的捕获组 (match[1])
59
- indices = authFiles.map(file => {
60
- const match = file.match(/^auth-(\d+)\.json$/);
61
- return parseInt(match[1], 10);
62
- });
63
- } catch (error) {
64
- this.logger.error(`[认证] 扫描 "auth/" 目录失败: ${error.message}`);
65
- this.initialIndices = [];
66
- return;
67
- }
68
- }
69
- this.initialIndices = [...new Set(indices)].sort((a, b) => a - b);
70
- this.logger.info(`[认证] 在 '${this.authMode}' 模式下,检测到 ${this.initialIndices.length} 个认证源。`);
71
- if (this.initialIndices.length > 0) {
72
- this.logger.info(`[认证] 可用初始索引: [${this.initialIndices.join(', ')}]`);
73
- }
74
- }
75
-
76
- getAvailableIndices() {
77
- const runtimeIndices = Array.from(this.runtimeAuths.keys());
78
- const allIndices = [...new Set([...this.initialIndices, ...runtimeIndices])].sort((a, b) => a - b);
79
- return allIndices;
80
- }
81
-
82
- // 新增方法:为仪表盘获取详细信息
83
- getAccountDetails() {
84
- const allIndices = this.getAvailableIndices();
85
- return allIndices.map(index => ({
86
- index,
87
- source: this.runtimeAuths.has(index) ? 'temporary' : this.authMode
88
- }));
89
- }
90
-
91
-
92
- getFirstAvailableIndex() {
93
- const indices = this.getAvailableIndices();
94
- return indices.length > 0 ? indices[0] : null;
95
- }
96
-
97
- getAuth(index) {
98
- if (!this.getAvailableIndices().includes(index)) {
99
- this.logger.error(`[认证] 请求了无效或不存在的认证索引: ${index}`);
100
- return null;
101
- }
102
-
103
- // 优先使用运行时(临时)的认证信息
104
- if (this.runtimeAuths.has(index)) {
105
- this.logger.info(`[认证] 使用索引 ${index} 的临时认证源。`);
106
- return this.runtimeAuths.get(index);
107
- }
108
-
109
- let jsonString;
110
- let sourceDescription;
111
-
112
- if (this.authMode === 'env') {
113
- jsonString = process.env[`AUTH_JSON_${index}`];
114
- sourceDescription = `环境变量 AUTH_JSON_${index}`;
115
- } else {
116
- const authFilePath = path.join(__dirname, 'auth', `auth-${index}.json`);
117
- sourceDescription = `文件 ${authFilePath}`;
118
- if (!fs.existsSync(authFilePath)) {
119
- this.logger.error(`[认证] ${sourceDescription} 在读取时突然消失。`);
120
- return null;
121
- }
122
- try {
123
- jsonString = fs.readFileSync(authFilePath, 'utf-8');
124
- } catch (e) {
125
- this.logger.error(`[认证] 读取 ${sourceDescription} 失败: ${e.message}`);
126
- return null;
127
- }
128
- }
129
-
130
- try {
131
- return JSON.parse(jsonString);
132
- } catch (e) {
133
- this.logger.error(`[认证] 解析来自 ${sourceDescription} 的JSON内容失败: ${e.message}`);
134
- return null;
135
- }
136
- }
137
-
138
- // 新增方法:动态添加账号
139
- addAccount(index, authData) {
140
- if (typeof index !== 'number' || index <= 0) {
141
- return { success: false, message: "索引必须是一个正数。" };
142
- }
143
- if (this.initialIndices.includes(index)) {
144
- return { success: false, message: `索引 ${index} 已作为永久账号存在。` };
145
- }
146
- try {
147
- // 验证 authData 是否为有效的JSON对象
148
- if (typeof authData !== 'object' || authData === null) {
149
- throw new Error("提供的数据不是一个有效的对象。");
150
- }
151
- this.runtimeAuths.set(index, authData);
152
- this.logger.info(`[认证] 成功添加索引为 ${index} 的临时账号。`);
153
- return { success: true, message: `账号 ${index} 已临时添加。` };
154
- } catch (e) {
155
- this.logger.error(`[认证] 添加临时账号 ${index} 失败: ${e.message}`);
156
- return { success: false, message: `添加账号失败: ${e.message}` };
157
- }
158
- }
159
-
160
- // 新增方法:动态删除账号
161
- removeAccount(index) {
162
- if (!this.runtimeAuths.has(index)) {
163
- return { success: false, message: `索引 ${index} 不是一个临时账号,无法移除。` };
164
- }
165
- this.runtimeAuths.delete(index);
166
- this.logger.info(`[认证] 成功移除索引为 ${index} 的临时账号。`);
167
- return { success: true, message: `账号 ${index} 已移除。` };
168
- }
169
- }
170
-
171
-
172
- // ===================================================================================
173
- // 浏览器管理模块
174
- // ===================================================================================
175
-
176
- class BrowserManager {
177
- constructor(logger, config, authSource) {
178
- this.logger = logger;
179
- this.config = config;
180
- this.authSource = authSource;
181
- this.browser = null;
182
- this.context = null;
183
- this.page = null;
184
- this.currentAuthIndex = 0;
185
- this.scriptFileName = 'dark-browser.js';
186
-
187
- if (this.config.browserExecutablePath) {
188
- this.browserExecutablePath = this.config.browserExecutablePath;
189
- this.logger.info(`[系统] 使用环境变量 CAMOUFOX_EXECUTABLE_PATH 指定的浏览器路径。`);
190
- } else {
191
- const platform = os.platform();
192
- if (platform === 'win32') {
193
- this.browserExecutablePath = path.join(__dirname, 'camoufox', 'camoufox.exe');
194
- this.logger.info(`[系统] 检测到操作系统: Windows. 将使用 'camoufox' 目录下的浏览器。`);
195
- } else if (platform === 'linux') {
196
- this.browserExecutablePath = path.join(__dirname, 'camoufox-linux', 'camoufox');
197
- this.logger.info(`[系统] 检测到操作系统: Linux. 将使用 'camoufox-linux' 目录下的浏览器。`);
198
- } else {
199
- this.logger.error(`[系统] 不支持的操作系统: ${platform}.`);
200
- throw new Error(`不支持的操作系统: ${platform}`);
201
- }
202
- }
203
- }
204
-
205
- async launchBrowser(authIndex) {
206
- if (this.browser) {
207
- this.logger.warn('尝试启动一个已在运行的浏览器实例,操作已取消。');
208
- return;
209
- }
210
-
211
- const sourceDescription = this.authSource.authMode === 'env' ? `环境变量 AUTH_JSON_${authIndex}` : `文件 auth-${authIndex}.json`;
212
- this.logger.info('==================================================');
213
- this.logger.info(`🚀 [浏览器] 准备启动浏览器`);
214
- this.logger.info(` • 认证源: ${sourceDescription}`);
215
- this.logger.info(` • 浏览器路径: ${this.browserExecutablePath}`);
216
- this.logger.info('==================================================');
217
-
218
- if (!fs.existsSync(this.browserExecutablePath)) {
219
- this.logger.error(`❌ [浏览器] 找不到浏览器可执行文件: ${this.browserExecutablePath}`);
220
- throw new Error(`找不到浏览器可执行文件路径: ${this.browserExecutablePath}`);
221
- }
222
-
223
- const storageStateObject = this.authSource.getAuth(authIndex);
224
- if (!storageStateObject) {
225
- this.logger.error(`❌ [浏览器] 无法获取或解析索引为 ${authIndex} 的认证信息。`);
226
- throw new Error(`获取或解析索引 ${authIndex} 的认证源失败。`);
227
- }
228
-
229
- if (storageStateObject.cookies && Array.isArray(storageStateObject.cookies)) {
230
- let fixedCount = 0;
231
- const validSameSiteValues = ['Lax', 'Strict', 'None'];
232
- storageStateObject.cookies.forEach(cookie => {
233
- if (!validSameSiteValues.includes(cookie.sameSite)) {
234
- this.logger.warn(`[认证] 发现无效的 sameSite 值: '${cookie.sameSite}',正在自动修正为 'None'。`);
235
- cookie.sameSite = 'None';
236
- fixedCount++;
237
- }
238
- });
239
- if (fixedCount > 0) {
240
- this.logger.info(`[认证] 自动修正了 ${fixedCount} 个无效的 Cookie 'sameSite' 属性。`);
241
- }
242
- }
243
-
244
- let buildScriptContent;
245
- try {
246
- const scriptFilePath = path.join(__dirname, this.scriptFileName);
247
- if (fs.existsSync(scriptFilePath)) {
248
- buildScriptContent = fs.readFileSync(scriptFilePath, 'utf-8');
249
- this.logger.info(`✅ [浏览器] 成功读取注入脚本 "${this.scriptFileName}"`);
250
- } else {
251
- this.logger.warn(`[浏览器] 未找到注入脚本 "${this.scriptFileName}"。将无注入继续运行。`);
252
- buildScriptContent = "console.log('dark-browser.js not found, running without injection.');";
253
- }
254
- } catch (error) {
255
- this.logger.error(`❌ [浏览器] 无法读取注入脚本 "${this.scriptFileName}"!`);
256
- throw error;
257
- }
258
-
259
- try {
260
- this.browser = await firefox.launch({
261
- headless: true,
262
- executablePath: this.browserExecutablePath,
263
- });
264
- this.browser.on('disconnected', () => {
265
- this.logger.error('❌ [浏览器] 浏览器意外断开连接!服务器可能需要重启。');
266
- this.browser = null; this.context = null; this.page = null;
267
- });
268
- this.context = await this.browser.newContext({
269
- storageState: storageStateObject,
270
- viewport: { width: 1280, height: 720 },
271
- });
272
- this.page = await this.context.newPage();
273
- this.logger.info(`[浏览器] 正在加载账号 ${authIndex} 并访问目标网页...`);
274
- const targetUrl = 'https://aistudio.google.com/u/0/apps/bundled/blank?showAssistant=true&showCode=true';
275
- await this.page.goto(targetUrl, { timeout: 120000, waitUntil: 'networkidle' });
276
- this.logger.info('[浏览器] 网页加载完成,正在注入客户端脚本...');
277
-
278
- this.logger.info('[浏览器] 正在点击 "Code" 按钮以切换到代码视图...');
279
- await this.page.getByRole('button', { name: 'Code' }).click();
280
- this.logger.info('[浏览器] 已切换到代码视图。');
281
-
282
- const editorContainerLocator = this.page.locator('div.monaco-editor').first();
283
-
284
- this.logger.info('[浏览器] 等待编辑器附加到DOM,最长120秒...');
285
- await editorContainerLocator.waitFor({ state: 'attached', timeout: 120000 });
286
- this.logger.info('[浏览器] 编辑器已附加。');
287
-
288
- this.logger.info('[浏览器] 等待5秒,之后将在页面下方执行一次模拟点击以确保页面激活...');
289
- await this.page.waitForTimeout(5000);
290
-
291
- const viewport = this.page.viewportSize();
292
- if (viewport) {
293
- const clickX = viewport.width / 2;
294
- const clickY = viewport.height - 120;
295
- this.logger.info(`[浏览器] 在页面底部中心位置 (x≈${Math.round(clickX)}, y=${clickY}) 执行点击。`);
296
- await this.page.mouse.click(clickX, clickY);
297
- } else {
298
- this.logger.warn('[浏览器] 无法获取视窗大小,跳过页面底部模拟点击。');
299
- }
300
-
301
- await editorContainerLocator.click({ force: true, timeout: 120000 });
302
- await this.page.evaluate(text => navigator.clipboard.writeText(text), buildScriptContent);
303
- const isMac = os.platform() === 'darwin';
304
- const pasteKey = isMac ? 'Meta+V' : 'Control+V';
305
- await this.page.keyboard.press(pasteKey);
306
- this.logger.info('[浏览器] 脚本已粘贴。');
307
-
308
- this.logger.info('[浏览器] 正在点击 "Preview" 按钮以使代码生效...');
309
- await this.page.getByRole('button', { name: 'Preview' }).click();
310
- this.logger.info('[浏览器] 已切换到预览视图。浏览器端初始化完成。');
311
-
312
-
313
- this.currentAuthIndex = authIndex;
314
- this.logger.info('==================================================');
315
- this.logger.info(`✅ [浏览器] 账号 ${authIndex} 初始化成功!`);
316
- this.logger.info('✅ [浏览器] 浏览器客户端已准备就绪。');
317
- this.logger.info('==================================================');
318
- } catch (error) {
319
- this.logger.error(`❌ [浏览器] 账号 ${authIndex} 初始化失败: ${error.message}`);
320
- if (this.browser) {
321
- await this.browser.close();
322
- this.browser = null;
323
- }
324
- throw error;
325
- }
326
- }
327
-
328
- async closeBrowser() {
329
- if (this.browser) {
330
- this.logger.info('[浏览器] 正在关闭当前浏览器实例...');
331
- await this.browser.close();
332
- this.browser = null; this.context = null; this.page = null;
333
- this.logger.info('[浏览器] 浏览器已关闭。');
334
- }
335
- }
336
-
337
- async switchAccount(newAuthIndex) {
338
- this.logger.info(`🔄 [浏览器] 开始账号切换: 从 ${this.currentAuthIndex} 到 ${newAuthIndex}`);
339
- await this.closeBrowser();
340
- await this.launchBrowser(newAuthIndex);
341
- this.logger.info(`✅ [浏览器] 账号切换完成,当前账号: ${this.currentAuthIndex}`);
342
- }
343
- }
344
-
345
- // ===================================================================================
346
- // 代理服务模块
347
- // ===================================================================================
348
-
349
- class LoggingService {
350
- constructor(serviceName = 'ProxyServer') {
351
- this.serviceName = serviceName;
352
- }
353
-
354
- _getFormattedTime() {
355
- // 使用 toLocaleTimeString 并指定 en-GB 区域来保证输出为 HH:mm:ss 格式
356
- return new Date().toLocaleTimeString('en-GB', { hour12: false });
357
- }
358
-
359
- // 用于 ERROR, WARN, DEBUG 等带有级别标签的日志
360
- _formatMessage(level, message) {
361
- const time = this._getFormattedTime();
362
- return `[${level}] ${time} [${this.serviceName}] - ${message}`;
363
- }
364
-
365
- // info 级别使用特殊格式,不显示 [INFO]
366
- info(message) {
367
- const time = this._getFormattedTime();
368
- console.log(`${time} [${this.serviceName}] - ${message}`);
369
- }
370
-
371
- error(message) {
372
- console.error(this._formatMessage('ERROR', message));
373
- }
374
-
375
- warn(message) {
376
- console.warn(this._formatMessage('WARN', message));
377
- }
378
-
379
- debug(message) {
380
- // 修正:移除内部对环境变量的检查。
381
- // 现在,只要调用此方法,就会打印日志。
382
- // 是否调用取决于程序其他部分的 this.config.debugMode 判断。
383
- console.debug(this._formatMessage('DEBUG', message));
384
- }
385
- }
386
-
387
- class MessageQueue extends EventEmitter {
388
- constructor(timeoutMs = 1200000) {
389
- super();
390
- this.messages = [];
391
- this.waitingResolvers = [];
392
- this.defaultTimeout = timeoutMs;
393
- this.closed = false;
394
- }
395
- enqueue(message) {
396
- if (this.closed) return;
397
- if (this.waitingResolvers.length > 0) {
398
- const resolver = this.waitingResolvers.shift();
399
- resolver.resolve(message);
400
- } else {
401
- this.messages.push(message);
402
- }
403
- }
404
- async dequeue(timeoutMs = this.defaultTimeout) {
405
- if (this.closed) {
406
- throw new Error('队列已关闭');
407
- }
408
- return new Promise((resolve, reject) => {
409
- if (this.messages.length > 0) {
410
- resolve(this.messages.shift());
411
- return;
412
- }
413
- const resolver = { resolve, reject };
414
- this.waitingResolvers.push(resolver);
415
- const timeoutId = setTimeout(() => {
416
- const index = this.waitingResolvers.indexOf(resolver);
417
- if (index !== -1) {
418
- this.waitingResolvers.splice(index, 1);
419
- reject(new Error('队列超时'));
420
- }
421
- }, timeoutMs);
422
- resolver.timeoutId = timeoutId;
423
- });
424
- }
425
- close() {
426
- this.closed = true;
427
- this.waitingResolvers.forEach(resolver => {
428
- clearTimeout(resolver.timeoutId);
429
- resolver.reject(new Error('队列已关闭'));
430
- });
431
- this.waitingResolvers = [];
432
- this.messages = [];
433
- }
434
- }
435
-
436
- class ConnectionRegistry extends EventEmitter {
437
- constructor(logger) {
438
- super();
439
- this.logger = logger;
440
- this.connections = new Set();
441
- this.messageQueues = new Map();
442
- }
443
- addConnection(websocket, clientInfo) {
444
- this.connections.add(websocket);
445
- this.logger.info(`[服务器] 内部WebSocket客户端已连接 (来自: ${clientInfo.address})`);
446
- websocket.on('message', (data) => this._handleIncomingMessage(data.toString()));
447
- websocket.on('close', () => this._removeConnection(websocket));
448
- websocket.on('error', (error) => this.logger.error(`[服务器] 内部WebSocket连接错误: ${error.message}`));
449
- this.emit('connectionAdded', websocket);
450
- }
451
- _removeConnection(websocket) {
452
- this.connections.delete(websocket);
453
- this.logger.warn('[服务器] 内部WebSocket客户端连接断开');
454
- this.messageQueues.forEach(queue => queue.close());
455
- this.messageQueues.clear();
456
- this.emit('connectionRemoved', websocket);
457
- }
458
- _handleIncomingMessage(messageData) {
459
- try {
460
- const parsedMessage = JSON.parse(messageData);
461
- const requestId = parsedMessage.request_id;
462
- if (!requestId) {
463
- this.logger.warn('[服务器] 收到无效消息:缺少request_id');
464
- return;
465
- }
466
- const queue = this.messageQueues.get(requestId);
467
- if (queue) {
468
- this._routeMessage(parsedMessage, queue);
469
- }
470
- } catch (error) {
471
- this.logger.error('[服务器] 解析内部WebSocket消息失败');
472
- }
473
- }
474
- _routeMessage(message, queue) {
475
- const { event_type } = message;
476
- switch (event_type) {
477
- case 'response_headers': case 'chunk': case 'error':
478
- queue.enqueue(message);
479
- break;
480
- case 'stream_close':
481
- queue.enqueue({ type: 'STREAM_END' });
482
- break;
483
- default:
484
- this.logger.warn(`[服务器] 未知的内部事件类型: ${event_type}`);
485
- }
486
- }
487
- hasActiveConnections() { return this.connections.size > 0; }
488
- getFirstConnection() { return this.connections.values().next().value; }
489
- createMessageQueue(requestId) {
490
- const queue = new MessageQueue();
491
- this.messageQueues.set(requestId, queue);
492
- return queue;
493
- }
494
- removeMessageQueue(requestId) {
495
- const queue = this.messageQueues.get(requestId);
496
- if (queue) {
497
- queue.close();
498
- this.messageQueues.delete(requestId);
499
- }
500
- }
501
- }
502
-
503
- class RequestHandler {
504
- constructor(serverSystem, connectionRegistry, logger, browserManager, config, authSource) {
505
- this.serverSystem = serverSystem;
506
- this.connectionRegistry = connectionRegistry;
507
- this.logger = logger;
508
- this.browserManager = browserManager;
509
- this.config = config;
510
- this.authSource = authSource;
511
- this.maxRetries = this.config.maxRetries;
512
- this.retryDelay = this.config.retryDelay;
513
- this.failureCount = 0;
514
- this.isAuthSwitching = false;
515
- }
516
-
517
- get currentAuthIndex() {
518
- return this.browserManager.currentAuthIndex;
519
- }
520
-
521
- _getNextAuthIndex() {
522
- const available = this.authSource.getAvailableIndices();
523
- if (available.length === 0) return null;
524
- if (available.length === 1) return available[0];
525
-
526
- const currentIndexInArray = available.indexOf(this.currentAuthIndex);
527
-
528
- if (currentIndexInArray === -1) {
529
- this.logger.warn(`[认证] 当前索引 ${this.currentAuthIndex} 不在可用列表中,将切换到第一个可用索引。`);
530
- return available[0];
531
- }
532
-
533
- const nextIndexInArray = (currentIndexInArray + 1) % available.length;
534
- return available[nextIndexInArray];
535
- }
536
-
537
- async _switchToNextAuth() {
538
- if (this.isAuthSwitching) {
539
- this.logger.info('🔄 [认证] 正在切换账号,跳过重复切换');
540
- return;
541
- }
542
-
543
- this.isAuthSwitching = true;
544
- const nextAuthIndex = this._getNextAuthIndex();
545
- const totalAuthCount = this.authSource.getAvailableIndices().length;
546
-
547
- if (nextAuthIndex === null) {
548
- this.logger.error('🔴 [认证] 无法切换账号,因为没有可用的认证源!');
549
- this.isAuthSwitching = false;
550
- throw new Error('没有可用的认证源可以切换。');
551
- }
552
-
553
- this.logger.info('==================================================');
554
- this.logger.info(`🔄 [认证] 开始账号切换流程`);
555
- this.logger.info(` • 失败次数: ${this.failureCount}/${this.config.failureThreshold > 0 ? this.config.failureThreshold : 'N/A'}`);
556
- this.logger.info(` • 当前账号索引: ${this.currentAuthIndex}`);
557
- this.logger.info(` • 目标账号索引: ${nextAuthIndex}`);
558
- this.logger.info(` • 可用账号总数: ${totalAuthCount}`);
559
- this.logger.info('==================================================');
560
-
561
- try {
562
- await this.browserManager.switchAccount(nextAuthIndex);
563
- this.failureCount = 0;
564
- this.logger.info('==================================================');
565
- this.logger.info(`✅ [认证] 成功切换到账号索引 ${this.currentAuthIndex}`);
566
- this.logger.info(`✅ [认证] 失败计数已重置为0`);
567
- this.logger.info('==================================================');
568
- } catch (error) {
569
- this.logger.error('==================================================');
570
- this.logger.error(`❌ [认证] 切换账号失败: ${error.message}`);
571
- this.logger.error('==================================================');
572
- throw error;
573
- } finally {
574
- this.isAuthSwitching = false;
575
- }
576
- }
577
-
578
- _parseAndCorrectErrorDetails(errorDetails) {
579
- const correctedDetails = { ...errorDetails };
580
- this.logger.debug(`[错误解析器] 原始错误详情: status=${correctedDetails.status}, message="${correctedDetails.message}"`);
581
-
582
- if (correctedDetails.message && typeof correctedDetails.message === 'string') {
583
- const regex = /(?:HTTP|status code)\s+(\d{3})/;
584
- const match = correctedDetails.message.match(regex);
585
-
586
- if (match && match[1]) {
587
- const parsedStatus = parseInt(match[1], 10);
588
- if (parsedStatus >= 400 && parsedStatus <= 599) {
589
- if (correctedDetails.status !== parsedStatus) {
590
- this.logger.warn(`[错误解析器] 修正了错误状态码!原始: ${correctedDetails.status}, 从消息中解析得到: ${parsedStatus}`);
591
- correctedDetails.status = parsedStatus;
592
- } else {
593
- this.logger.debug(`[错误解析器] 解析的状态码 (${parsedStatus}) 与原始状态码一致,无需修正。`);
594
- }
595
- }
596
- }
597
- }
598
- return correctedDetails;
599
- }
600
-
601
- async _handleRequestFailureAndSwitch(errorDetails, res) {
602
- // 新增:在调试模式下打印完整的原始错误信息
603
- if (this.config.debugMode) {
604
- this.logger.debug(`[认证][调试] 收到来自浏览器的完整错误详情:\n${JSON.stringify(errorDetails, null, 2)}`);
605
- }
606
-
607
- const correctedDetails = { ...errorDetails };
608
- if (correctedDetails.message && typeof correctedDetails.message === 'string') {
609
- const regex = /(?:HTTP|status code)\s*(\d{3})|"code"\s*:\s*(\d{3})/;
610
- const match = correctedDetails.message.match(regex);
611
- const parsedStatusString = match ? (match[1] || match[2]) : null;
612
-
613
- if (parsedStatusString) {
614
- const parsedStatus = parseInt(parsedStatusString, 10);
615
- if (parsedStatus >= 400 && parsedStatus <= 599 && correctedDetails.status !== parsedStatus) {
616
- this.logger.warn(`[认证] 修正了错误状态码!原始: ${correctedDetails.status}, 从消息中解析得到: ${parsedStatus}`);
617
- correctedDetails.status = parsedStatus;
618
- }
619
- }
620
- }
621
-
622
- const isImmediateSwitch = this.config.immediateSwitchStatusCodes.includes(correctedDetails.status);
623
-
624
- if (isImmediateSwitch) {
625
- this.logger.warn(`🔴 [认证] 收到状态码 ${correctedDetails.status} (已修正),触发立即切换账号...`);
626
- if (res) this._sendErrorChunkToClient(res, `收到状态码 ${correctedDetails.status},正在尝试切换账号...`);
627
- try {
628
- await this._switchToNextAuth();
629
- if (res) this._sendErrorChunkToClient(res, `已切换到账号索引 ${this.currentAuthIndex},请重试`);
630
- } catch (switchError) {
631
- this.logger.error(`🔴 [认证] 账号切换失败: ${switchError.message}`);
632
- if (res) this._sendErrorChunkToClient(res, `切换账号失败: ${switchError.message}`);
633
- }
634
- return;
635
- }
636
-
637
- if (this.config.failureThreshold > 0) {
638
- this.failureCount++;
639
- this.logger.warn(`⚠️ [认证] 请求失败 - 失败计数: ${this.failureCount}/${this.config.failureThreshold} (当前账号索引: ${this.currentAuthIndex}, 状态码: ${correctedDetails.status})`);
640
- if (this.failureCount >= this.config.failureThreshold) {
641
- this.logger.warn(`🔴 [认证] 达到失败阈值!准备切换账号...`);
642
- if (res) this._sendErrorChunkToClient(res, `连续失败${this.failureCount}次,正在尝试切换账号...`);
643
- try {
644
- await this._switchToNextAuth();
645
- if (res) this._sendErrorChunkToClient(res, `已切换到账号索引 ${this.currentAuthIndex},请重试`);
646
- } catch (switchError) {
647
- this.logger.error(`🔴 [认证] 账号切换失败: ${switchError.message}`);
648
- if (res) this._sendErrorChunkToClient(res, `切换账号失败: ${switchError.message}`);
649
- }
650
- }
651
- } else {
652
- this.logger.warn(`[认证] 请求失败 (状态码: ${correctedDetails.status})。基于计数的自动切换已禁用 (failureThreshold=0)`);
653
- }
654
- }
655
-
656
- _getModelFromRequest(req) {
657
- let body = req.body;
658
-
659
- if (Buffer.isBuffer(body)) {
660
- try {
661
- body = JSON.parse(body.toString('utf-8'));
662
- } catch (e) { body = {}; }
663
- } else if (typeof body === 'string') {
664
- try {
665
- body = JSON.parse(body);
666
- } catch (e) { body = {}; }
667
- }
668
-
669
- if (body && typeof body === 'object') {
670
- if (body.model) return body.model;
671
- if (body.generation_config && body.generation_config.model) return body.generation_config.model;
672
- }
673
-
674
- const match = req.path.match(/\/models\/([^/:]+)/);
675
- if (match && match[1]) {
676
- return match[1];
677
- }
678
- return 'unknown_model';
679
- }
680
-
681
- async processRequest(req, res) {
682
- // 关键修复 (V2): 使用 hasOwnProperty 来准确判断 'key' 参数是否存在,
683
- // 无论其值是空字符串还是有内容。
684
- if ((!this.config.apiKeys || this.config.apiKeys.length === 0) && req.query && req.query.hasOwnProperty('key')) {
685
- if (this.config.debugMode) {
686
- this.logger.debug(`[请求预处理] 服务器API密钥认证已禁用。检测到并移除了来自客户端的 'key' 查询参数 (值为: '${req.query.key}')。`);
687
- }
688
- delete req.query.key;
689
- }
690
-
691
- // 提前获取模型名称和当前账号
692
- const modelName = this._getModelFromRequest(req);
693
- const currentAccount = this.currentAuthIndex;
694
-
695
- // 新增的合并日志行,报告路径、账号和模型
696
- this.logger.info(`[请求] ${req.method} ${req.path} | 账号: ${currentAccount} | 模型: 🤖 ${modelName}`);
697
-
698
- // --- 升级的统计逻辑 ---
699
- this.serverSystem.stats.totalCalls++;
700
- if (this.serverSystem.stats.accountCalls[currentAccount]) {
701
- this.serverSystem.stats.accountCalls[currentAccount].total = (this.serverSystem.stats.accountCalls[currentAccount].total || 0) + 1;
702
- this.serverSystem.stats.accountCalls[currentAccount].models[modelName] = (this.serverSystem.stats.accountCalls[currentAccount].models[modelName] || 0) + 1;
703
- } else {
704
- this.serverSystem.stats.accountCalls[currentAccount] = {
705
- total: 1,
706
- models: { [modelName]: 1 }
707
- };
708
- }
709
-
710
- if (!this.connectionRegistry.hasActiveConnections()) {
711
- return this._sendErrorResponse(res, 503, '没有可用的浏览器连接');
712
- }
713
- const requestId = this._generateRequestId();
714
- const proxyRequest = this._buildProxyRequest(req, requestId);
715
- const messageQueue = this.connectionRegistry.createMessageQueue(requestId);
716
- try {
717
- if (this.serverSystem.streamingMode === 'fake') {
718
- await this._handlePseudoStreamResponse(proxyRequest, messageQueue, req, res);
719
- } else {
720
- await this._handleRealStreamResponse(proxyRequest, messageQueue, res);
721
- }
722
- } catch (error) {
723
- this._handleRequestError(error, res);
724
- } finally {
725
- this.connectionRegistry.removeMessageQueue(requestId);
726
- }
727
- }
728
- _generateRequestId() { return `${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; }
729
- _buildProxyRequest(req, requestId) {
730
- const proxyRequest = {
731
- path: req.path,
732
- method: req.method,
733
- headers: req.headers,
734
- query_params: req.query,
735
- request_id: requestId,
736
- streaming_mode: this.serverSystem.streamingMode
737
- };
738
-
739
- // 关键修正:只在允许有请求体的HTTP方法中添加body字段
740
- if (req.method !== 'GET' && req.method !== 'HEAD') {
741
- let requestBodyString;
742
- if (typeof req.body === 'object' && req.body !== null) {
743
- requestBodyString = JSON.stringify(req.body);
744
- } else if (typeof req.body === 'string') {
745
- requestBodyString = req.body;
746
- } else if (Buffer.isBuffer(req.body)) {
747
- requestBodyString = req.body.toString('utf-8');
748
- } else {
749
- requestBodyString = '';
750
- }
751
- proxyRequest.body = requestBodyString;
752
- }
753
-
754
- return proxyRequest;
755
- }
756
- _forwardRequest(proxyRequest) {
757
- const connection = this.connectionRegistry.getFirstConnection();
758
- if (connection) {
759
- connection.send(JSON.stringify(proxyRequest));
760
- } else {
761
- throw new Error("无法转发请求:没有可用的WebSocket连接。");
762
- }
763
- }
764
- _sendErrorChunkToClient(res, errorMessage) {
765
- const errorPayload = {
766
- error: { message: `[代理系统提示] ${errorMessage}`, type: 'proxy_error', code: 'proxy_error' }
767
- };
768
- const chunk = `data: ${JSON.stringify(errorPayload)}\n\n`;
769
- if (res && !res.writableEnded) {
770
- res.write(chunk);
771
- this.logger.info(`[请求] 已向客户端发送标准错误信号: ${errorMessage}`);
772
- }
773
- }
774
-
775
- _getKeepAliveChunk(req) {
776
- if (req.path.includes('chat/completions')) {
777
- const payload = { id: `chatcmpl-${this._generateRequestId()}`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: "gpt-4", choices: [{ index: 0, delta: {}, finish_reason: null }] };
778
- return `data: ${JSON.stringify(payload)}\n\n`;
779
- }
780
- if (req.path.includes('generateContent') || req.path.includes('streamGenerateContent')) {
781
- const payload = { candidates: [{ content: { parts: [{ text: "" }], role: "model" }, finishReason: null, index: 0, safetyRatings: [] }] };
782
- return `data: ${JSON.stringify(payload)}\n\n`;
783
- }
784
- return 'data: {}\n\n';
785
- }
786
-
787
- async _handlePseudoStreamResponse(proxyRequest, messageQueue, req, res) {
788
- const originalPath = req.path;
789
- const isStreamRequest = originalPath.includes(':stream');
790
-
791
- this.logger.info(`[请求] 假流式处理流程启动,路径: "${originalPath}",判定为: ${isStreamRequest ? '流式请求' : '非流式请求'}`);
792
-
793
- let connectionMaintainer = null;
794
-
795
- if (isStreamRequest) {
796
- res.status(200).set({
797
- 'Content-Type': 'text/event-stream',
798
- 'Cache-Control': 'no-cache',
799
- 'Connection': 'keep-alive'
800
- });
801
- const keepAliveChunk = this._getKeepAliveChunk(req);
802
- connectionMaintainer = setInterval(() => { if (!res.writableEnded) res.write(keepAliveChunk); }, 2000);
803
- }
804
-
805
- try {
806
- let lastMessage, requestFailed = false;
807
- for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
808
- this.logger.info(`[请求] 请求尝试 #${attempt}/${this.maxRetries}...`);
809
- this._forwardRequest(proxyRequest);
810
- lastMessage = await messageQueue.dequeue();
811
-
812
- if (lastMessage.event_type === 'error' && lastMessage.status >= 400 && lastMessage.status <= 599) {
813
- const correctedMessage = this._parseAndCorrectErrorDetails(lastMessage);
814
- await this._handleRequestFailureAndSwitch(correctedMessage, isStreamRequest ? res : null);
815
-
816
- const errorText = `收到 ${correctedMessage.status} 错误。${attempt < this.maxRetries ? `将在 ${this.retryDelay / 1000}秒后重试...` : '已达到最大重试次数。'}`;
817
- this.logger.warn(`[请求] ${errorText}`);
818
-
819
- if (isStreamRequest) {
820
- this._sendErrorChunkToClient(res, errorText);
821
- }
822
-
823
- if (attempt < this.maxRetries) {
824
- await new Promise(resolve => setTimeout(resolve, this.retryDelay));
825
- continue;
826
- }
827
- requestFailed = true;
828
- }
829
- break;
830
- }
831
-
832
- if (lastMessage.event_type === 'error' || requestFailed) {
833
- const finalError = this._parseAndCorrectErrorDetails(lastMessage);
834
- if (!res.headersSent) {
835
- this._sendErrorResponse(res, finalError.status, `请求失败: ${finalError.message}`);
836
- } else {
837
- this._sendErrorChunkToClient(res, `请求最终失败 (状态码: ${finalError.status}): ${finalError.message}`);
838
- }
839
- return;
840
- }
841
-
842
- if (this.failureCount > 0) {
843
- this.logger.info(`✅ [认证] 请求成功 - 失败计数已从 ${this.failureCount} 重置为 0`);
844
- }
845
- this.failureCount = 0;
846
-
847
- const dataMessage = await messageQueue.dequeue();
848
- const endMessage = await messageQueue.dequeue();
849
- if (endMessage.type !== 'STREAM_END') this.logger.warn('[请求] 未收到预期的流结束信号。');
850
-
851
- if (isStreamRequest) {
852
- if (dataMessage.data) {
853
- res.write(`data: ${dataMessage.data}\n\n`);
854
- }
855
- res.write('data: [DONE]\n\n');
856
- this.logger.info('[请求] 已将完整响应作为模拟SSE事件发送。');
857
- } else {
858
- this.logger.info('[请求] 准备发送 application/json 响应。');
859
- if (dataMessage.data) {
860
- try {
861
- const jsonData = JSON.parse(dataMessage.data);
862
- res.status(200).json(jsonData);
863
- } catch (e) {
864
- this.logger.error(`[请求] 无法将来自浏览器的响应解析为JSON: ${e.message}`);
865
- this._sendErrorResponse(res, 500, '代理内部错误:无法解析来自后端的响应。');
866
- }
867
- } else {
868
- this._sendErrorResponse(res, 500, '代理内部错误:后端未返回有效数据。');
869
- }
870
- }
871
-
872
- } catch (error) {
873
- this.logger.error(`[请求] 假流式处理期间发生意外错误: ${error.message}`);
874
- if (!res.headersSent) {
875
- this._handleRequestError(error, res);
876
- } else {
877
- this._sendErrorChunkToClient(res, `处理失败: ${error.message}`);
878
- }
879
- } finally {
880
- if (connectionMaintainer) clearInterval(connectionMaintainer);
881
- if (!res.writableEnded) res.end();
882
- this.logger.info('[请求] 假流式响应处理结束。');
883
- }
884
- }
885
-
886
- async _handleRealStreamResponse(proxyRequest, messageQueue, res) {
887
- let headerMessage, requestFailed = false;
888
- for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
889
- this.logger.info(`[请求] 请求尝试 #${attempt}/${this.maxRetries}...`);
890
- this._forwardRequest(proxyRequest);
891
- headerMessage = await messageQueue.dequeue();
892
- if (headerMessage.event_type === 'error' && headerMessage.status >= 400 && headerMessage.status <= 599) {
893
-
894
- const correctedMessage = this._parseAndCorrectErrorDetails(headerMessage);
895
- await this._handleRequestFailureAndSwitch(correctedMessage, null);
896
- this.logger.warn(`[请求] 收到 ${correctedMessage.status} 错误,将在 ${this.retryDelay / 1000}秒后重试...`);
897
-
898
- if (attempt < this.maxRetries) {
899
- await new Promise(resolve => setTimeout(resolve, this.retryDelay));
900
- continue;
901
- }
902
- requestFailed = true;
903
- }
904
- break;
905
- }
906
- if (headerMessage.event_type === 'error' || requestFailed) {
907
- const finalError = this._parseAndCorrectErrorDetails(headerMessage);
908
- return this._sendErrorResponse(res, finalError.status, finalError.message);
909
- }
910
- if (this.failureCount > 0) {
911
- this.logger.info(`✅ [认证] 请求成功 - 失败计数已从 ${this.failureCount} 重置为 0`);
912
- }
913
- this.failureCount = 0;
914
- this._setResponseHeaders(res, headerMessage);
915
- this.logger.info('[请求] 已向客户端发送真实响应头,开始流式传输...');
916
- try {
917
- while (true) {
918
- const dataMessage = await messageQueue.dequeue(30000);
919
- if (dataMessage.type === 'STREAM_END') { this.logger.info('[请求] 收到流结束信号。'); break; }
920
- if (dataMessage.data) res.write(dataMessage.data);
921
- }
922
- } catch (error) {
923
- if (error.message !== '队列超时') throw error;
924
- this.logger.warn('[请求] 真流式响应超时,可能流已正常结束。');
925
- } finally {
926
- if (!res.writableEnded) res.end();
927
- this.logger.info('[请求] 真流式响应连接已关闭。');
928
- }
929
- }
930
-
931
- _setResponseHeaders(res, headerMessage) {
932
- res.status(headerMessage.status || 200);
933
- const headers = headerMessage.headers || {};
934
- Object.entries(headers).forEach(([name, value]) => {
935
- if (name.toLowerCase() !== 'content-length') res.set(name, value);
936
- });
937
- }
938
- _handleRequestError(error, res) {
939
- if (res.headersSent) {
940
- this.logger.error(`[请求] 请求处理错误 (头已发送): ${error.message}`);
941
- if (this.serverSystem.streamingMode === 'fake') this._sendErrorChunkToClient(res, `处理失败: ${error.message}`);
942
- if (!res.writableEnded) res.end();
943
- } else {
944
- this.logger.error(`[请求] 请求处理错误: ${error.message}`);
945
- const status = error.message.includes('超时') ? 504 : 500;
946
- this._sendErrorResponse(res, status, `代理错误: ${error.message}`);
947
- }
948
- }
949
- _sendErrorResponse(res, status, message) {
950
- if (!res.headersSent) res.status(status || 500).type('text/plain').send(message);
951
- }
952
- }
953
-
954
- class ProxyServerSystem extends EventEmitter {
955
- constructor() {
956
- super();
957
- this.logger = new LoggingService('ProxySystem');
958
- this._loadConfiguration();
959
- this.streamingMode = this.config.streamingMode;
960
-
961
- // ��级后的统计结构
962
- this.stats = {
963
- totalCalls: 0,
964
- accountCalls: {} // e.g., { "1": { total: 10, models: { "gemini-pro": 5, "gpt-4": 5 } } }
965
- };
966
-
967
- this.authSource = new AuthSource(this.logger);
968
- this.browserManager = new BrowserManager(this.logger, this.config, this.authSource);
969
- this.connectionRegistry = new ConnectionRegistry(this.logger);
970
- this.requestHandler = new RequestHandler(this, this.connectionRegistry, this.logger, this.browserManager, this.config, this.authSource);
971
-
972
- this.httpServer = null;
973
- this.wsServer = null;
974
- }
975
-
976
- _loadConfiguration() {
977
- let config = {
978
- httpPort: 8889, host: '0.0.0.0', wsPort: 9998, streamingMode: 'real',
979
- failureThreshold: 0,
980
- maxRetries: 3, retryDelay: 2000, browserExecutablePath: null,
981
- apiKeys: [],
982
- immediateSwitchStatusCodes: [],
983
- initialAuthIndex: null,
984
- debugMode: false,
985
- };
986
-
987
- const configPath = path.join(__dirname, 'config.json');
988
- try {
989
- if (fs.existsSync(configPath)) {
990
- const fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
991
- config = { ...config, ...fileConfig };
992
- this.logger.info('[系统] 已从 config.json 加载配置。');
993
- }
994
- } catch (error) {
995
- this.logger.warn(`[系统] 无法读取或解析 config.json: ${error.message}`);
996
- }
997
-
998
- if (process.env.PORT) config.httpPort = parseInt(process.env.PORT, 10) || config.httpPort;
999
- if (process.env.HOST) config.host = process.env.HOST;
1000
- if (process.env.STREAMING_MODE) config.streamingMode = process.env.STREAMING_MODE;
1001
- if (process.env.FAILURE_THRESHOLD) config.failureThreshold = parseInt(process.env.FAILURE_THRESHOLD, 10) || config.failureThreshold;
1002
- if (process.env.MAX_RETRIES) config.maxRetries = parseInt(process.env.MAX_RETRIES, 10) || config.maxRetries;
1003
- if (process.env.RETRY_DELAY) config.retryDelay = parseInt(process.env.RETRY_DELAY, 10) || config.retryDelay;
1004
- if (process.env.CAMOUFOX_EXECUTABLE_PATH) config.browserExecutablePath = process.env.CAMOUFOX_EXECUTABLE_PATH;
1005
- if (process.env.API_KEYS) {
1006
- config.apiKeys = process.env.API_KEYS.split(',');
1007
- }
1008
- if (process.env.DEBUG_MODE) {
1009
- config.debugMode = process.env.DEBUG_MODE === 'true';
1010
- }
1011
- if (process.env.INITIAL_AUTH_INDEX) {
1012
- const envIndex = parseInt(process.env.INITIAL_AUTH_INDEX, 10);
1013
- if (!isNaN(envIndex) && envIndex > 0) {
1014
- config.initialAuthIndex = envIndex;
1015
- }
1016
- }
1017
-
1018
- let rawCodes = process.env.IMMEDIATE_SWITCH_STATUS_CODES;
1019
- let codesSource = '环境变量';
1020
-
1021
- if (!rawCodes && config.immediateSwitchStatusCodes && Array.isArray(config.immediateSwitchStatusCodes)) {
1022
- rawCodes = config.immediateSwitchStatusCodes.join(',');
1023
- codesSource = 'config.json 文件';
1024
- }
1025
-
1026
- if (rawCodes && typeof rawCodes === 'string') {
1027
- config.immediateSwitchStatusCodes = rawCodes
1028
- .split(',')
1029
- .map(code => parseInt(String(code).trim(), 10))
1030
- .filter(code => !isNaN(code) && code >= 400 && code <= 599);
1031
- if (config.immediateSwitchStatusCodes.length > 0) {
1032
- this.logger.info(`[系统] 已从 ${codesSource} 加载“立即切换状态码”。`);
1033
- }
1034
- } else {
1035
- config.immediateSwitchStatusCodes = [];
1036
- }
1037
-
1038
- if (Array.isArray(config.apiKeys)) {
1039
- config.apiKeys = config.apiKeys.map(k => String(k).trim()).filter(k => k);
1040
- } else {
1041
- config.apiKeys = [];
1042
- }
1043
-
1044
- this.config = config;
1045
- this.logger.info('================ [ 生效配置 ] ================');
1046
- this.logger.info(` HTTP 服务端口: ${this.config.httpPort}`);
1047
- this.logger.info(` 监听地址: ${this.config.host}`);
1048
- this.logger.info(` 流式模式: ${this.config.streamingMode}`);
1049
- this.logger.info(` 调试模式: ${this.config.debugMode ? '已开启' : '已关闭'}`);
1050
- if (this.config.initialAuthIndex) {
1051
- this.logger.info(` 指定初始认证索引: ${this.config.initialAuthIndex}`);
1052
- }
1053
- this.logger.info(` 失败计数切换: ${this.config.failureThreshold > 0 ? `连续 ${this.config.failureThreshold} 次失败后切换` : '已禁用'}`);
1054
- this.logger.info(` 立即切换状态码: ${this.config.immediateSwitchStatusCodes.length > 0 ? this.config.immediateSwitchStatusCodes.join(', ') : '已禁用'}`);
1055
- this.logger.info(` 单次请求最大重试: ${this.config.maxRetries}次`);
1056
- this.logger.info(` 重试间隔: ${this.config.retryDelay}ms`);
1057
- if (this.config.apiKeys && this.config.apiKeys.length > 0) {
1058
- this.logger.info(` API 密钥认证: 已启用 (${this.config.apiKeys.length} 个密钥)`);
1059
- } else {
1060
- this.logger.info(` API 密钥认证: 已禁用`);
1061
- }
1062
- this.logger.info('=============================================================');
1063
- }
1064
-
1065
- async start() {
1066
- try {
1067
- // 初始化统计对象
1068
- this.authSource.getAvailableIndices().forEach(index => {
1069
- this.stats.accountCalls[index] = { total: 0, models: {} };
1070
- });
1071
-
1072
- let startupIndex = this.authSource.getFirstAvailableIndex();
1073
- const suggestedIndex = this.config.initialAuthIndex;
1074
-
1075
- if (suggestedIndex) {
1076
- if (this.authSource.getAvailableIndices().includes(suggestedIndex)) {
1077
- this.logger.info(`[系统] 使用配置中指定的有效启动索引: ${suggestedIndex}`);
1078
- startupIndex = suggestedIndex;
1079
- } else {
1080
- this.logger.warn(`[系统] 配置中指定的启动索引 ${suggestedIndex} 无效或不存在,将使用第一个可用索引: ${startupIndex}`);
1081
- }
1082
- } else {
1083
- this.logger.info(`[系统] 未指定启动索引,将自动使用第一个可用索引: ${startupIndex}`);
1084
- }
1085
-
1086
- await this.browserManager.launchBrowser(startupIndex);
1087
- await this._startHttpServer();
1088
- await this._startWebSocketServer();
1089
- this.logger.info(`[系统] 代理服务器系统启动完成。`);
1090
- this.emit('started');
1091
- } catch (error) {
1092
- this.logger.error(`[系统] 启动失败: ${error.message}`);
1093
- this.emit('error', error);
1094
- process.exit(1); // 启动失败时退出
1095
- }
1096
- }
1097
-
1098
- _createDebugLogMiddleware() {
1099
- return (req, res, next) => {
1100
- if (!this.config.debugMode) {
1101
- return next();
1102
- }
1103
-
1104
- const requestId = this.requestHandler._generateRequestId();
1105
- const log = this.logger.info.bind(this.logger);
1106
-
1107
- log(`\n\n--- [调试] 开始处理入站请求 (${requestId}) ---`);
1108
- log(`[调试][${requestId}] 客户端 IP: ${req.ip}`);
1109
- log(`[调试][${requestId}] 方法: ${req.method}`);
1110
- log(`[调试][${requestId}] URL: ${req.originalUrl}`);
1111
- log(`[调试][${requestId}] 请求头: ${JSON.stringify(req.headers, null, 2)}`);
1112
-
1113
- let bodyContent = '无或空';
1114
- if (req.body) {
1115
- if (Buffer.isBuffer(req.body) && req.body.length > 0) {
1116
- try {
1117
- bodyContent = JSON.stringify(JSON.parse(req.body.toString('utf-8')), null, 2);
1118
- } catch (e) {
1119
- bodyContent = `[无法解析为JSON的Buffer, 大小: ${req.body.length} 字节]`;
1120
- }
1121
- } else if (typeof req.body === 'object' && Object.keys(req.body).length > 0) {
1122
- bodyContent = JSON.stringify(req.body, null, 2);
1123
- }
1124
- }
1125
-
1126
- log(`[调试][${requestId}] 请求体:\n${bodyContent}`);
1127
- log(`--- [调试] 结束处理入站请求 (${requestId}) ---\n\n`);
1128
-
1129
- next();
1130
- };
1131
- }
1132
-
1133
-
1134
- _createAuthMiddleware() {
1135
- return (req, res, next) => {
1136
- const serverApiKeys = this.config.apiKeys;
1137
- if (!serverApiKeys || serverApiKeys.length === 0) {
1138
- return next();
1139
- }
1140
-
1141
- let clientKey = null;
1142
- let keySource = null;
1143
-
1144
- const headers = req.headers;
1145
- const xGoogApiKey = headers['x-goog-api-key'] || headers['x_goog_api_key'];
1146
- const xApiKey = headers['x-api-key'] || headers['x_api_key'];
1147
- const authHeader = headers.authorization;
1148
-
1149
- if (xGoogApiKey) {
1150
- clientKey = xGoogApiKey;
1151
- keySource = 'x-goog-api-key 请求头';
1152
- } else if (authHeader && authHeader.startsWith('Bearer ')) {
1153
- clientKey = authHeader.substring(7);
1154
- keySource = 'Authorization 请求头';
1155
- } else if (xApiKey) {
1156
- clientKey = xApiKey;
1157
- keySource = 'X-API-Key 请求头';
1158
- } else if (req.query.key) {
1159
- clientKey = req.query.key;
1160
- keySource = '查询参数';
1161
- }
1162
-
1163
- if (clientKey) {
1164
- if (serverApiKeys.includes(clientKey)) {
1165
- if (this.config.debugMode) {
1166
- this.logger.debug(`[认证][调试] 在 '${keySource}' 中找到API密钥,验证通过。`);
1167
- }
1168
- if (keySource === '查询参数') {
1169
- delete req.query.key;
1170
- }
1171
- return next();
1172
- } else {
1173
- if (this.config.debugMode) {
1174
- this.logger.warn(`[认证][调试] 拒绝请求: 无效的API密钥。IP: ${req.ip}, 路径: ${req.path}`);
1175
- this.logger.debug(`[认证][调试] 来源: ${keySource}`);
1176
- this.logger.debug(`[认证][调试] 提供的错误密钥: '${clientKey}'`);
1177
- this.logger.debug(`[认证][调试] 已加载的有效密钥: [${serverApiKeys.join(', ')}]`);
1178
- } else {
1179
- this.logger.warn(`[认证] 拒绝请求: 无效的API密钥。IP: ${req.ip}, 路径: ${req.path}`);
1180
- }
1181
- return res.status(401).json({ error: { message: "提供了无效的API密钥。" } });
1182
- }
1183
- }
1184
-
1185
- this.logger.warn(`[认证] 拒绝受保护的请求: 缺少API密钥。IP: ${req.ip}, 路径: ${req.path}`);
1186
-
1187
- if (this.config.debugMode) {
1188
- this.logger.debug(`[认证][调试] 未在任何标准位置找到API密钥。`);
1189
- this.logger.debug(`[认证][调试] 搜索的请求头: ${JSON.stringify(headers, null, 2)}`);
1190
- this.logger.debug(`[认证][调试] 搜索的查询参数: ${JSON.stringify(req.query)}`);
1191
- this.logger.debug(`[认证][调试] 已加载的有效密钥: [${serverApiKeys.join(', ')}]`);
1192
- }
1193
-
1194
- return res.status(401).json({ error: { message: "访问被拒绝。未在请求头或查询参数中找到有效的API密钥。" } });
1195
- };
1196
- }
1197
-
1198
- async _startHttpServer() {
1199
- const app = this._createExpressApp();
1200
- this.httpServer = http.createServer(app);
1201
- return new Promise((resolve) => {
1202
- this.httpServer.listen(this.config.httpPort, this.config.host, () => {
1203
- this.logger.info(`[系统] HTTP服务器已在 http://${this.config.host}:${this.config.httpPort} 上监听`);
1204
- this.logger.info(`[系统] 仪表盘可在 http://${this.config.host}:${this.config.httpPort}/dashboard 访问`);
1205
- resolve();
1206
- });
1207
- });
1208
- }
1209
-
1210
- _createExpressApp() {
1211
- const app = express();
1212
- app.use(express.json({ limit: '100mb' }));
1213
- app.use(express.raw({ type: '*/*', limit: '100mb' }));
1214
- app.use((req, res, next) => {
1215
- if (req.is('application/json') && typeof req.body === 'object' && !Buffer.isBuffer(req.body)) {
1216
- // Already parsed correctly by express.json()
1217
- } else if (Buffer.isBuffer(req.body)) {
1218
- const bodyStr = req.body.toString('utf-8');
1219
- if (bodyStr) {
1220
- try {
1221
- req.body = JSON.parse(bodyStr);
1222
- } catch (e) {
1223
- // Not JSON, leave as buffer.
1224
- }
1225
- }
1226
- }
1227
- next();
1228
- });
1229
-
1230
- app.use(this._createDebugLogMiddleware());
1231
-
1232
- // --- 仪表盘和API端点 ---
1233
-
1234
- // 新增: 将根目录重定向到仪表盘
1235
- app.get('/', (req, res) => {
1236
- res.redirect('/dashboard');
1237
- });
1238
-
1239
- // 公开端点:提供仪表盘HTML
1240
- app.get('/dashboard', (req, res) => {
1241
- res.send(this._getDashboardHtml());
1242
- });
1243
-
1244
- // 公开端点:用于仪表盘验证API密钥
1245
- app.post('/dashboard/verify-key', (req, res) => {
1246
- const { key } = req.body;
1247
- const serverApiKeys = this.config.apiKeys;
1248
-
1249
- if (!serverApiKeys || serverApiKeys.length === 0) {
1250
- this.logger.info('[管理] 服务器未配置API密钥,自动授予仪表盘访问权限。');
1251
- return res.json({ success: true });
1252
- }
1253
-
1254
- if (key && serverApiKeys.includes(key)) {
1255
- this.logger.info('[管理] 仪表盘API密钥验证成功。');
1256
- return res.json({ success: true });
1257
- }
1258
-
1259
- this.logger.warn(`[管理] 仪表盘API密钥验证失败。`);
1260
- res.status(401).json({ success: false, message: '无效的API密钥。' });
1261
- });
1262
-
1263
- // 中间件:保护仪表盘API路由
1264
- const dashboardApiAuth = (req, res, next) => {
1265
- const serverApiKeys = this.config.apiKeys;
1266
- if (!serverApiKeys || serverApiKeys.length === 0) {
1267
- return next(); // 未配置密钥,跳过认证
1268
- }
1269
-
1270
- const clientKey = req.headers['x-dashboard-auth'];
1271
- if (clientKey && serverApiKeys.includes(clientKey)) {
1272
- return next();
1273
- }
1274
-
1275
- this.logger.warn(`[管理] 拒绝未经授权的仪表盘API请求。IP: ${req.ip}, 路径: ${req.path}`);
1276
- res.status(401).json({ error: { message: 'Unauthorized dashboard access' } });
1277
- };
1278
-
1279
- const dashboardApiRouter = express.Router();
1280
- dashboardApiRouter.use(dashboardApiAuth);
1281
-
1282
- dashboardApiRouter.get('/data', (req, res) => {
1283
- res.json({
1284
- status: {
1285
- uptime: process.uptime(),
1286
- streamingMode: this.streamingMode,
1287
- debugMode: this.config.debugMode,
1288
- authMode: this.authSource.authMode,
1289
- apiKeyAuth: (this.config.apiKeys && this.config.apiKeys.length > 0) ? '已启用' : '已禁用',
1290
- isAuthSwitching: this.requestHandler.isAuthSwitching,
1291
- browserConnected: !!this.browserManager.browser,
1292
- internalWsClients: this.connectionRegistry.connections.size
1293
- },
1294
- auth: {
1295
- currentAuthIndex: this.requestHandler.currentAuthIndex,
1296
- accounts: this.authSource.getAccountDetails(),
1297
- failureCount: this.requestHandler.failureCount,
1298
- },
1299
- stats: this.stats,
1300
- config: this.config
1301
- });
1302
- });
1303
-
1304
- dashboardApiRouter.post('/config', (req, res) => {
1305
- const newConfig = req.body;
1306
- try {
1307
- if (newConfig.hasOwnProperty('streamingMode') && ['real', 'fake'].includes(newConfig.streamingMode)) {
1308
- this.config.streamingMode = newConfig.streamingMode;
1309
- this.streamingMode = newConfig.streamingMode;
1310
- this.requestHandler.serverSystem.streamingMode = newConfig.streamingMode;
1311
- }
1312
- if (newConfig.hasOwnProperty('debugMode') && typeof newConfig.debugMode === 'boolean') {
1313
- this.config.debugMode = newConfig.debugMode;
1314
- }
1315
- if (newConfig.hasOwnProperty('failureThreshold')) {
1316
- this.config.failureThreshold = parseInt(newConfig.failureThreshold, 10) || 0;
1317
- }
1318
- if (newConfig.hasOwnProperty('maxRetries')) {
1319
- const retries = parseInt(newConfig.maxRetries, 10);
1320
- this.config.maxRetries = retries >= 0 ? retries : 3;
1321
- this.requestHandler.maxRetries = this.config.maxRetries;
1322
- }
1323
- if (newConfig.hasOwnProperty('retryDelay')) {
1324
- this.config.retryDelay = parseInt(newConfig.retryDelay, 10) || 2000;
1325
- this.requestHandler.retryDelay = this.config.retryDelay;
1326
- }
1327
- if (newConfig.hasOwnProperty('immediateSwitchStatusCodes')) {
1328
- if (Array.isArray(newConfig.immediateSwitchStatusCodes)) {
1329
- this.config.immediateSwitchStatusCodes = newConfig.immediateSwitchStatusCodes
1330
- .map(c => parseInt(c, 10))
1331
- .filter(c => !isNaN(c));
1332
- }
1333
- }
1334
- this.logger.info('[管理] 配置已通过仪表盘动态更新。');
1335
- res.status(200).json({ success: true, message: '配置已临时更新。' });
1336
- } catch (error) {
1337
- this.logger.error(`[管理] 更新配置失败: ${error.message}`);
1338
- res.status(500).json({ success: false, message: error.message });
1339
- }
1340
- });
1341
-
1342
- dashboardApiRouter.post('/accounts', (req, res) => {
1343
- const { index, authData } = req.body;
1344
- if (!index || !authData) {
1345
- return res.status(400).json({ success: false, message: "必须提供索引和认证数据。" });
1346
- }
1347
-
1348
- let parsedData;
1349
- try {
1350
- parsedData = (typeof authData === 'string') ? JSON.parse(authData) : authData;
1351
- } catch (e) {
1352
- return res.status(400).json({ success: false, message: "认证数据的JSON格式无效。" });
1353
- }
1354
-
1355
- const result = this.authSource.addAccount(parseInt(index, 10), parsedData);
1356
- if (result.success) {
1357
- if (!this.stats.accountCalls.hasOwnProperty(index)) {
1358
- this.stats.accountCalls[index] = { total: 0, models: {} };
1359
- }
1360
- }
1361
- res.status(result.success ? 200 : 400).json(result);
1362
- });
1363
-
1364
- dashboardApiRouter.delete('/accounts/:index', (req, res) => {
1365
- const index = parseInt(req.params.index, 10);
1366
- const result = this.authSource.removeAccount(index);
1367
- res.status(result.success ? 200 : 400).json(result);
1368
- });
1369
-
1370
- // 挂载受保护的仪表盘API路由
1371
- app.use('/dashboard', dashboardApiRouter);
1372
-
1373
- // 保护 /switch 路由
1374
- app.post('/switch', dashboardApiAuth, async (req, res) => {
1375
- this.logger.info('[管理] 接到 /switch 请求,手动触发账号切换。');
1376
- if (this.requestHandler.isAuthSwitching) {
1377
- const msg = '账号切换已在进行中,请稍后。';
1378
- this.logger.warn(`[管理] /switch 请求被拒绝: ${msg}`);
1379
- return res.status(429).send(msg);
1380
- }
1381
- const oldIndex = this.requestHandler.currentAuthIndex;
1382
- try {
1383
- await this.requestHandler._switchToNextAuth();
1384
- const newIndex = this.requestHandler.currentAuthIndex;
1385
- const message = `成功将账号从索引 ${oldIndex} 切换到 ${newIndex}。`;
1386
- this.logger.info(`[管理] 手动切换成功。 ${message}`);
1387
- res.status(200).send(message);
1388
- } catch (error) {
1389
- const errorMessage = `切换账号失败: ${error.message}`;
1390
- this.logger.error(`[管理] 手动切换失败。错误: ${errorMessage}`);
1391
- res.status(500).send(errorMessage);
1392
- }
1393
- });
1394
-
1395
- app.get('/health', (req, res) => {
1396
- res.status(200).json({
1397
- status: 'healthy',
1398
- uptime: process.uptime(),
1399
- config: {
1400
- streamingMode: this.streamingMode,
1401
- debugMode: this.config.debugMode,
1402
- failureThreshold: this.config.failureThreshold,
1403
- immediateSwitchStatusCodes: this.config.immediateSwitchStatusCodes,
1404
- maxRetries: this.config.maxRetries,
1405
- authMode: this.authSource.authMode,
1406
- apiKeyAuth: (this.config.apiKeys && this.config.apiKeys.length > 0) ? '已启用' : '已禁用',
1407
- },
1408
- auth: {
1409
- currentAuthIndex: this.requestHandler.currentAuthIndex,
1410
- availableIndices: this.authSource.getAvailableIndices(),
1411
- totalAuthSources: this.authSource.getAvailableIndices().length,
1412
- failureCount: this.requestHandler.failureCount,
1413
- isAuthSwitching: this.requestHandler.isAuthSwitching,
1414
- },
1415
- stats: this.stats,
1416
- browser: {
1417
- connected: !!this.browserManager.browser,
1418
- },
1419
- websocket: {
1420
- internalClients: this.connectionRegistry.connections.size
1421
- }
1422
- });
1423
- });
1424
-
1425
- // 主API代理
1426
- app.use(this._createAuthMiddleware());
1427
- app.all(/(.*)/, (req, res) => {
1428
- // 修改: 增加对根路径的判断,防止其被代理
1429
- if (req.path === '/' || req.path === '/favicon.ico' || req.path.startsWith('/dashboard')) {
1430
- return res.status(204).send();
1431
- }
1432
- this.requestHandler.processRequest(req, res);
1433
- });
1434
-
1435
- return app;
1436
- }
1437
-
1438
- _getDashboardHtml() {
1439
- return `
1440
- <!DOCTYPE html>
1441
- <html lang="zh-CN">
1442
- <head>
1443
- <meta charset="UTF-8">
1444
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1445
- <title>服务器仪表盘</title>
1446
- <style>
1447
- :root {
1448
- --pico-font-size: 16px;
1449
- --pico-background-color: #11191f;
1450
- --pico-color: #dce3e9;
1451
- --pico-card-background-color: #1a242c;
1452
- --pico-card-border-color: #2b3a47;
1453
- --pico-primary: #3d8bfd;
1454
- --pico-primary-hover: #529bff;
1455
- --pico-primary-focus: rgba(61, 139, 253, 0.25);
1456
- --pico-primary-inverse: #fff;
1457
- --pico-form-element-background-color: #1a242c;
1458
- --pico-form-element-border-color: #2b3a47;
1459
- --pico-form-element-focus-color: var(--pico-primary);
1460
- --pico-h1-color: #fff;
1461
- --pico-h2-color: #f1f1f1;
1462
- --pico-muted-color: #7a8c99;
1463
- --pico-border-radius: 0.5rem;
1464
- --info-color: #17a2b8; /* 天蓝色,用于状态文本 */
1465
- }
1466
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; margin: 0; padding: 2rem; background-color: var(--pico-background-color); color: var(--pico-color); }
1467
- main.container { max-width: 1200px; margin: 0 auto; padding-top: 30px; display: none; /* Initially hidden */ }
1468
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 1.5rem; }
1469
- article { border: 1px solid var(--pico-card-border-color); border-radius: var(--pico-border-radius); padding: 1.5rem; background: var(--pico-card-background-color); }
1470
- h1, h2 { margin-top: 0; color: var(--pico-h1-color); }
1471
- h2 { border-bottom: 1px solid var(--pico-card-border-color); padding-bottom: 0.5rem; margin-bottom: 1rem; color: var(--pico-h2-color); }
1472
- .status-grid { display: grid; grid-template-columns: auto 1fr; gap: 0.5rem 1rem; align-items: center;}
1473
- .status-grid strong { color: var(--pico-color); white-space: nowrap;}
1474
- .status-grid span { color: var(--pico-muted-color); text-align: right; }
1475
- .status-text-info { color: var(--info-color); font-weight: bold; }
1476
- .status-text-red { color: #dc3545; font-weight: bold; }
1477
- .status-text-yellow { color: #ffc107; font-weight: bold; }
1478
- .status-text-gray { color: var(--pico-muted-color); font-weight: bold; }
1479
- .tag { display: inline-block; padding: 0.25em 0.6em; font-size: 0.75em; font-weight: 700; line-height: 1; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: 0.35rem; color: #fff; }
1480
- .tag-info { background-color: #17a2b8; }
1481
- .tag-blue { background-color: #007bff; }
1482
- .tag-yellow { color: #212529; background-color: #ffc107; }
1483
- ul { list-style: none; padding: 0; margin: 0; }
1484
- .scrollable-list { max-height: 220px; overflow-y: auto; padding-right: 5px; border: 1px solid var(--pico-form-element-border-color); border-radius: 0.25rem; padding: 0.5rem;}
1485
- .account-list li { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-radius: 0.25rem; }
1486
- .account-list li:nth-child(odd) { background-color: rgba(255,255,255,0.03); }
1487
- .account-list .current { font-weight: bold; color: var(--pico-primary); }
1488
- details { width: 100%; border-bottom: 1px solid var(--pico-form-element-border-color); }
1489
- details:last-child { border-bottom: none; }
1490
- details summary { cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0.2rem; list-style: none; }
1491
- details summary::-webkit-details-marker { display: none; }
1492
- details summary:hover { background-color: rgba(255,255,255,0.05); }
1493
- .model-stats-list { padding: 0.5rem 0 0.5rem 1.5rem; font-size: 0.9em; background-color: rgba(0,0,0,0.1); }
1494
- .model-stats-list li { display: flex; justify-content: space-between; padding: 0.2rem; }
1495
- button, input[type="text"], input[type="number"] { background-color: var(--pico-form-element-background-color); border: 1px solid var(--pico-form-element-border-color); color: var(--pico-color); padding: 0.5rem 1rem; border-radius: var(--pico-border-radius); }
1496
- button { cursor: pointer; background-color: var(--pico-primary); border-color: var(--pico-primary); color: var(--pico-primary-inverse); }
1497
- button:hover { background-color: var(--pico-primary-hover); }
1498
- .btn-danger { background-color: #dc3545; border-color: #dc3545; }
1499
- .btn-sm { font-size: 0.8em; padding: 0.2rem 0.5rem; }
1500
- .top-banner { position: fixed; top: 0; right: 0; background-color: #ffc107; color: #212529; padding: 5px 15px; font-size: 0.9em; z-index: 1001; border-bottom-left-radius: 0.5rem; }
1501
- .toast { position: fixed; bottom: 20px; right: 20px; background-color: var(--pico-primary); color: white; padding: 15px; border-radius: 5px; z-index: 1000; opacity: 0; transition: opacity 0.5s; }
1502
- .toast.show { opacity: 1; }
1503
- .toast.error { background-color: #dc3545; }
1504
- form label { display: block; margin-bottom: 0.5rem; }
1505
- form input { width: 100%; box-sizing: border-box; }
1506
- .form-group { margin-bottom: 1rem; }
1507
- .switch-field { display: flex; overflow: hidden; }
1508
- .switch-field input { position: absolute !important; clip: rect(0, 0, 0, 0); height: 1px; width: 1px; border: 0; overflow: hidden; }
1509
- .switch-field label { background-color: var(--pico-form-element-background-color); color: var(--pico-muted-color); font-size: 14px; line-height: 1; text-align: center; padding: 8px 16px; margin-right: -1px; border: 1px solid var(--pico-form-element-border-color); transition: all 0.1s ease-in-out; width: 50%; }
1510
- .switch-field label:hover { cursor: pointer; }
1511
- .switch-field input:checked + label { background-color: var(--pico-primary); color: var(--pico-primary-inverse); box-shadow: none; }
1512
- .switch-field label:first-of-type { border-radius: 4px 0 0 4px; }
1513
- .switch-field label:last-of-type { border-radius: 0 4px 4px 0; }
1514
- </style>
1515
- </head>
1516
- <body data-theme="dark">
1517
- <div class="top-banner">注意: 此面板中添加的账号和修改的变量均是临时的,重启后会丢失</div>
1518
- <main class="container">
1519
- <h1>🐢 服务器仪表盘</h1>
1520
- <div class="grid">
1521
- <article>
1522
- <h2>服务器状态</h2>
1523
- <div class="status-grid">
1524
- <strong>运行时间:</strong> <span id="uptime">--</span>
1525
- <strong>浏览器:</strong> <span id="browserConnected">--</span>
1526
- <strong>认证模式:</strong> <span id="authMode">--</span>
1527
- <strong>API密钥认证:</strong> <span id="apiKeyAuth">--</span>
1528
- <strong>调试模式:</strong> <span id="debugMode">--</span>
1529
- <strong>API总调用次数:</strong> <span id="totalCalls">0</span>
1530
- </div>
1531
- </article>
1532
- <article>
1533
- <h2>调用统计</h2>
1534
- <div id="accountCalls" class="scrollable-list"></div>
1535
- </article>
1536
-
1537
- <article>
1538
- <h2>账号管理</h2>
1539
- <div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
1540
- <button id="switchAccountBtn">切换到下一个账号</button>
1541
- <button id="addAccountBtn">添加临时账号</button>
1542
- </div>
1543
- <h3>账号池</h3>
1544
- <div id="accountPool" class="scrollable-list"></div>
1545
- </article>
1546
-
1547
- <article>
1548
- <h2>实时配置</h2>
1549
- <form id="configForm">
1550
- <div class="form-group">
1551
- <label>流式模式</label>
1552
- <div class="switch-field">
1553
- <input type="radio" id="streamingMode_fake" name="streamingMode" value="fake" />
1554
- <label for="streamingMode_fake">Fake</label>
1555
- <input type="radio" id="streamingMode_real" name="streamingMode" value="real" checked/>
1556
- <label for="streamingMode_real">Real</label>
1557
- </div>
1558
- </div>
1559
-
1560
- <div class="form-group">
1561
- <label for="configFailureThreshold">几次失败后切换账号 (0为禁用)</label>
1562
- <input type="number" id="configFailureThreshold" name="failureThreshold">
1563
- </div>
1564
-
1565
- <div class="form-group">
1566
- <label for="configMaxRetries">单次请求内部重试次数</label>
1567
- <input type="number" id="configMaxRetries" name="maxRetries">
1568
- </div>
1569
-
1570
- <div class="form-group">
1571
- <label for="configRetryDelay">重试间隔 (毫秒)</label>
1572
- <input type="number" id="configRetryDelay" name="retryDelay">
1573
- </div>
1574
-
1575
- <div class="form-group">
1576
- <label for="configImmediateSwitchStatusCodes">立即切换的状态码 (逗号分隔)</label>
1577
- <input type="text" id="configImmediateSwitchStatusCodes" name="immediateSwitchStatusCodes">
1578
- </div>
1579
-
1580
- <button type="submit">应用临时更改</button>
1581
- </form>
1582
- </article>
1583
- </div>
1584
- </main>
1585
- <div id="toast" class="toast"></div>
1586
- <script>
1587
- document.addEventListener('DOMContentLoaded', () => {
1588
- const API_KEY_SESSION_STORAGE = 'dashboard_api_key';
1589
- const API_BASE = '/dashboard';
1590
-
1591
- // DOM Elements
1592
- const mainContainer = document.querySelector('main.container');
1593
- const uptimeEl = document.getElementById('uptime');
1594
- const debugModeEl = document.getElementById('debugMode');
1595
- const browserConnectedEl = document.getElementById('browserConnected');
1596
- const authModeEl = document.getElementById('authMode');
1597
- const apiKeyAuthEl = document.getElementById('apiKeyAuth');
1598
- const totalCallsEl = document.getElementById('totalCalls');
1599
- const accountCallsEl = document.getElementById('accountCalls');
1600
- const accountPoolEl = document.getElementById('accountPool');
1601
- const switchAccountBtn = document.getElementById('switchAccountBtn');
1602
- const addAccountBtn = document.getElementById('addAccountBtn');
1603
- const configForm = document.getElementById('configForm');
1604
- const toastEl = document.getElementById('toast');
1605
-
1606
- function getAuthHeaders(hasBody = false) {
1607
- const headers = {
1608
- 'X-Dashboard-Auth': sessionStorage.getItem(API_KEY_SESSION_STORAGE) || ''
1609
- };
1610
- if (hasBody) {
1611
- headers['Content-Type'] = 'application/json';
1612
- }
1613
- return headers;
1614
- }
1615
-
1616
- function showToast(message, isError = false) {
1617
- toastEl.textContent = message;
1618
- toastEl.className = isError ? 'toast show error' : 'toast show';
1619
- setTimeout(() => { toastEl.className = 'toast'; }, 3000);
1620
- }
1621
-
1622
- function formatUptime(seconds) {
1623
- const d = Math.floor(seconds / (3600*24));
1624
- const h = Math.floor(seconds % (3600*24) / 3600);
1625
- const m = Math.floor(seconds % 3600 / 60);
1626
- const s = Math.floor(seconds % 60);
1627
- return \`\${d}天 \${h}小时 \${m}分钟 \${s}秒\`;
1628
- }
1629
-
1630
- function handleAuthFailure() {
1631
- sessionStorage.removeItem(API_KEY_SESSION_STORAGE);
1632
- mainContainer.style.display = 'none';
1633
- document.body.insertAdjacentHTML('afterbegin', '<h1>认证已过期或无效,请刷新页面重试。</h1>');
1634
- showToast('认证失败', true);
1635
- }
1636
-
1637
- async function fetchData() {
1638
- try {
1639
- const response = await fetch(\`\${API_BASE}/data\`, { headers: getAuthHeaders() });
1640
- if (response.status === 401) return handleAuthFailure();
1641
- if (!response.ok) throw new Error('获取数据失败');
1642
- const data = await response.json();
1643
-
1644
- uptimeEl.textContent = formatUptime(data.status.uptime);
1645
- browserConnectedEl.innerHTML = data.status.browserConnected ? '<span class="status-text-info">已连接</span>' : '<span class="status-text-red">已断开</span>';
1646
- authModeEl.innerHTML = data.status.authMode === 'env' ? '<span class="status-text-info">环境变量</span>' : '<span class="status-text-info">Cookie文件</span>';
1647
- apiKeyAuthEl.innerHTML = data.status.apiKeyAuth === '已启用' ? '<span class="status-text-info">已启用</span>' : '<span class="status-text-gray">已禁用</span>';
1648
- debugModeEl.innerHTML = data.status.debugMode ? '<span class="status-text-yellow">已启用</span>' : '<span class="status-text-gray">已禁用</span>';
1649
- totalCallsEl.textContent = data.stats.totalCalls;
1650
-
1651
- accountCallsEl.innerHTML = '';
1652
- const sortedAccounts = Object.entries(data.stats.accountCalls).sort((a,b) => parseInt(a[0]) - parseInt(b[0]));
1653
- const callsUl = document.createElement('ul');
1654
- callsUl.className = 'account-list';
1655
- for (const [index, stats] of sortedAccounts) {
1656
- const li = document.createElement('li');
1657
- const isCurrent = parseInt(index, 10) === data.auth.currentAuthIndex;
1658
- let modelStatsHtml = '<ul class="model-stats-list">';
1659
- const sortedModels = Object.entries(stats.models).sort((a,b) => b[1] - a[1]);
1660
- sortedModels.length > 0 ? sortedModels.forEach(([model, count]) => { modelStatsHtml += \`<li><span>\${model}:</span> <strong>\${count}</strong></li>\`; }) : modelStatsHtml += '<li>无模型调用记录</li>';
1661
- modelStatsHtml += '</ul>';
1662
- li.innerHTML = \`<details><summary><span class="\${isCurrent ? 'current' : ''}">账号 \${index}</span><strong>总计: \${stats.total}</strong></summary>\${modelStatsHtml}</details>\`;
1663
- if(isCurrent) { li.querySelector('summary').style.color = 'var(--pico-primary)'; }
1664
- callsUl.appendChild(li);
1665
- }
1666
- accountCallsEl.appendChild(callsUl);
1667
-
1668
- accountPoolEl.innerHTML = '';
1669
- const poolUl = document.createElement('ul');
1670
- poolUl.className = 'account-list';
1671
- data.auth.accounts.forEach(acc => {
1672
- const li = document.createElement('li');
1673
- const isCurrent = acc.index === data.auth.currentAuthIndex;
1674
- const sourceTag = acc.source === 'temporary' ? '<span class="tag tag-yellow">临时</span>' : (acc.source === 'env' ? '<span class="tag tag-info">变量</span>' : '<span class="tag tag-blue">文件</span>');
1675
- let html = \`<span class="\${isCurrent ? 'current' : ''}">账号 \${acc.index} \${sourceTag}</span>\`;
1676
- if (acc.source === 'temporary') { html += \`<button class="btn-danger btn-sm" data-index="\${acc.index}">删除</button>\`; } else { html += '<span></span>'; }
1677
- li.innerHTML = html;
1678
- poolUl.appendChild(li);
1679
- });
1680
- accountPoolEl.appendChild(poolUl);
1681
-
1682
- const streamingModeInput = document.querySelector(\`input[name="streamingMode"][value="\${data.config.streamingMode}"]\`);
1683
- if(streamingModeInput) streamingModeInput.checked = true;
1684
- configForm.failureThreshold.value = data.config.failureThreshold;
1685
- configForm.maxRetries.value = data.config.maxRetries;
1686
- configForm.retryDelay.value = data.config.retryDelay;
1687
- configForm.immediateSwitchStatusCodes.value = data.config.immediateSwitchStatusCodes.join(', ');
1688
- } catch (error) {
1689
- console.error('获取数据时出错:', error);
1690
- showToast(error.message, true);
1691
- }
1692
- }
1693
-
1694
- function initializeDashboardListeners() {
1695
- switchAccountBtn.addEventListener('click', async () => {
1696
- switchAccountBtn.disabled = true;
1697
- switchAccountBtn.textContent = '切换中...';
1698
- try {
1699
- const response = await fetch('/switch', { method: 'POST', headers: getAuthHeaders() });
1700
- const text = await response.text();
1701
- if (!response.ok) throw new Error(text);
1702
- showToast(text);
1703
- await fetchData();
1704
- } catch (error) {
1705
- showToast(error.message, true);
1706
- } finally {
1707
- switchAccountBtn.disabled = false;
1708
- switchAccountBtn.textContent = '切换到下一个账号';
1709
- }
1710
- });
1711
-
1712
- addAccountBtn.addEventListener('click', () => {
1713
- const index = prompt("为新的临时账号输入一个唯一的数字索引:");
1714
- if (!index || isNaN(parseInt(index))) { if(index !== null) alert("索引无效。"); return; }
1715
- const authDataStr = prompt("请输入单行压缩后的Cookie内容:");
1716
- if (!authDataStr) return;
1717
- let authData;
1718
- try { authData = JSON.parse(authDataStr); } catch(e) { alert("Cookie JSON格式无效。"); return; }
1719
-
1720
- fetch(\`\${API_BASE}/accounts\`, { method: 'POST', headers: getAuthHeaders(true), body: JSON.stringify({ index: parseInt(index), authData }) })
1721
- .then(res => res.json().then(data => ({ ok: res.ok, data }))).then(({ok, data}) => {
1722
- if (!ok) throw new Error(data.message);
1723
- showToast(data.message); fetchData(); }).catch(err => showToast(err.message, true));
1724
- });
1725
-
1726
- accountPoolEl.addEventListener('click', e => {
1727
- if (e.target.matches('button.btn-danger')) {
1728
- const index = e.target.dataset.index;
1729
- if (confirm(\`您确定要删除临时账号 \${index} 吗?\`)) {
1730
- fetch(\`\${API_BASE}/accounts/\${index}\`, { method: 'DELETE', headers: getAuthHeaders() })
1731
- .then(res => res.json().then(data => ({ ok: res.ok, data }))).then(({ok, data}) => {
1732
- if (!ok) throw new Error(data.message);
1733
- showToast(data.message); fetchData(); }).catch(err => showToast(err.message, true));
1734
- }
1735
- }
1736
- });
1737
-
1738
- configForm.addEventListener('submit', e => {
1739
- e.preventDefault();
1740
- const formData = new FormData(configForm);
1741
- const data = Object.fromEntries(formData.entries());
1742
- data.immediateSwitchStatusCodes = data.immediateSwitchStatusCodes.split(',').map(s => s.trim()).filter(Boolean);
1743
- fetch(\`\${API_BASE}/config\`, { method: 'POST', headers: getAuthHeaders(true), body: JSON.stringify(data) })
1744
- .then(res => res.json().then(data => ({ ok: res.ok, data }))).then(({ok, data}) => {
1745
- if (!ok) throw new Error(data.message);
1746
- showToast('配置已应用。'); fetchData(); }).catch(err => showToast(err.message, true));
1747
- });
1748
-
1749
- configForm.addEventListener('change', e => {
1750
- if (e.target.name === 'streamingMode') {
1751
- fetch(\`\${API_BASE}/config\`, { method: 'POST', headers: getAuthHeaders(true), body: JSON.stringify({ streamingMode: e.target.value }) })
1752
- .then(res => res.json().then(d => ({ ok: res.ok, data: d }))).then(({ok, data}) => {
1753
- if (!ok) throw new Error(data.message);
1754
- showToast(\`流式模式已更新为: \${e.target.value.charAt(0).toUpperCase() + e.target.value.slice(1)}\`);
1755
- }).catch(err => showToast(err.message, true));
1756
- }
1757
- });
1758
- }
1759
-
1760
- async function verifyAndLoad(keyToVerify) {
1761
- try {
1762
- const response = await fetch(\`\${API_BASE}/verify-key\`, {
1763
- method: 'POST',
1764
- headers: { 'Content-Type': 'application/json' },
1765
- body: JSON.stringify({ key: keyToVerify || '' })
1766
- });
1767
- const result = await response.json();
1768
-
1769
- if (response.ok && result.success) {
1770
- if (keyToVerify) {
1771
- sessionStorage.setItem(API_KEY_SESSION_STORAGE, keyToVerify);
1772
- }
1773
- mainContainer.style.display = 'block';
1774
- initializeDashboardListeners();
1775
- fetchData();
1776
- setInterval(fetchData, 5000);
1777
- return true;
1778
- } else {
1779
- sessionStorage.removeItem(API_KEY_SESSION_STORAGE);
1780
- return false;
1781
- }
1782
- } catch (err) {
1783
- document.body.innerHTML = \`<h1>认证时发生错误: \${err.message}</h1>\`;
1784
- return false;
1785
- }
1786
- }
1787
-
1788
- async function checkAndInitiate() {
1789
- const storedApiKey = sessionStorage.getItem(API_KEY_SESSION_STORAGE);
1790
-
1791
- // 尝试使用已存储的密钥或空密钥进行验证
1792
- const initialCheckSuccess = await verifyAndLoad(storedApiKey);
1793
-
1794
- // 如果初次验证失败,说明服务器需要密钥,而我们没有提供或提供了错误的密钥
1795
- if (!initialCheckSuccess) {
1796
- const newApiKey = prompt("请输入API密钥以访问仪表盘 (服务器需要认证):");
1797
- if (newApiKey) {
1798
- // 使用用户新输入的密钥再次尝试
1799
- const secondCheckSuccess = await verifyAndLoad(newApiKey);
1800
- if (!secondCheckSuccess) {
1801
- document.body.innerHTML = \`<h1>认证失败: 无效的API密钥</h1>\`;
1802
- }
1803
- } else {
1804
- // 用户取消了输入
1805
- document.body.innerHTML = '<h1>访问被拒绝</h1>';
1806
- }
1807
- }
1808
- }
1809
-
1810
- checkAndInitiate();
1811
- });
1812
- </script>
1813
- </body>
1814
- </html>
1815
- `;
1816
- }
1817
-
1818
-
1819
-
1820
- async _startWebSocketServer() {
1821
- this.wsServer = new WebSocket.Server({ port: this.config.wsPort, host: this.config.host });
1822
- this.wsServer.on('connection', (ws, req) => {
1823
- this.connectionRegistry.addConnection(ws, { address: req.socket.remoteAddress });
1824
- });
1825
- }
1826
- }
1827
-
1828
- // ===================================================================================
1829
- // 主初始化
1830
- // ===================================================================================
1831
-
1832
- async function initializeServer() {
1833
- try {
1834
- const serverSystem = new ProxyServerSystem();
1835
- await serverSystem.start();
1836
- } catch (error) {
1837
- console.error('❌ 服务器启动失败:', error.message);
1838
- process.exit(1);
1839
- }
1840
- }
1841
-
1842
- if (require.main === module) {
1843
- initializeServer();
1844
- }
1845
-
1846
- module.exports = { ProxyServerSystem, BrowserManager, initializeServer };