kanpand commited on
Commit
f505714
·
verified ·
1 Parent(s): e0e18b0

Create unified-server.js

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