Spaces:
Paused
Paused
Upload unified-server.js
Browse files- unified-server.js +243 -219
unified-server.js
CHANGED
|
@@ -57,8 +57,8 @@ class AuthSource {
|
|
| 57 |
const authFiles = files.filter(file => /^auth-\d+\.json$/.test(file));
|
| 58 |
// 修正:正确解析文件名中的捕获组 (match[1])
|
| 59 |
indices = authFiles.map(file => {
|
| 60 |
-
|
| 61 |
-
|
| 62 |
});
|
| 63 |
} catch (error) {
|
| 64 |
this.logger.error(`[认证] 扫描 "auth/" 目录失败: ${error.message}`);
|
|
@@ -78,14 +78,14 @@ class AuthSource {
|
|
| 78 |
const allIndices = [...new Set([...this.initialIndices, ...runtimeIndices])].sort((a, b) => a - b);
|
| 79 |
return allIndices;
|
| 80 |
}
|
| 81 |
-
|
| 82 |
// 新增方法:为仪表盘获取详细信息
|
| 83 |
getAccountDetails() {
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
}
|
| 90 |
|
| 91 |
|
|
@@ -99,11 +99,11 @@ class AuthSource {
|
|
| 99 |
this.logger.error(`[认证] 请求了无效或不存在的认证索引: ${index}`);
|
| 100 |
return null;
|
| 101 |
}
|
| 102 |
-
|
| 103 |
// 优先使用运行时(临时)的认证信息
|
| 104 |
if (this.runtimeAuths.has(index)) {
|
| 105 |
-
|
| 106 |
-
|
| 107 |
}
|
| 108 |
|
| 109 |
let jsonString;
|
|
@@ -134,37 +134,37 @@ class AuthSource {
|
|
| 134 |
return null;
|
| 135 |
}
|
| 136 |
}
|
| 137 |
-
|
| 138 |
// 新增方法:动态添加账号
|
| 139 |
addAccount(index, authData) {
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 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 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
}
|
| 169 |
}
|
| 170 |
|
|
@@ -244,7 +244,7 @@ class BrowserManager {
|
|
| 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 {
|
|
@@ -280,7 +280,7 @@ class BrowserManager {
|
|
| 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 |
|
|
@@ -353,24 +353,24 @@ class LoggingService {
|
|
| 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 |
-
if(process.env.DEBUG_MODE === 'true') {
|
| 373 |
-
|
| 374 |
}
|
| 375 |
}
|
| 376 |
}
|
|
@@ -640,28 +640,28 @@ class RequestHandler {
|
|
| 640 |
}
|
| 641 |
|
| 642 |
_getModelFromRequest(req) {
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
if (Buffer.isBuffer(body)) {
|
| 646 |
-
try {
|
| 647 |
-
body = JSON.parse(body.toString('utf-8'));
|
| 648 |
-
} catch (e) { body = {}; }
|
| 649 |
-
} else if (typeof body === 'string') {
|
| 650 |
-
try {
|
| 651 |
-
body = JSON.parse(body);
|
| 652 |
-
} catch(e) { body = {}; }
|
| 653 |
-
}
|
| 654 |
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
}
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
}
|
| 666 |
|
| 667 |
async processRequest(req, res) {
|
|
@@ -671,19 +671,19 @@ class RequestHandler {
|
|
| 671 |
|
| 672 |
// 新增的合并日志行,报告路径、账号和模型
|
| 673 |
this.logger.info(`[请求] ${req.method} ${req.path} | 账号: ${currentAccount} | 模型: 🤖 ${modelName}`);
|
| 674 |
-
|
| 675 |
// --- 升级的统计逻辑 ---
|
| 676 |
this.serverSystem.stats.totalCalls++;
|
| 677 |
if (this.serverSystem.stats.accountCalls[currentAccount]) {
|
| 678 |
-
|
| 679 |
-
|
| 680 |
} else {
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
}
|
| 686 |
-
|
| 687 |
if (!this.connectionRegistry.hasActiveConnections()) {
|
| 688 |
return this._sendErrorResponse(res, 503, '没有可用的浏览器连接');
|
| 689 |
}
|
|
@@ -925,11 +925,11 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 925 |
this.logger = new LoggingService('ProxySystem');
|
| 926 |
this._loadConfiguration();
|
| 927 |
this.streamingMode = this.config.streamingMode;
|
| 928 |
-
|
| 929 |
// 升级后的统计结构
|
| 930 |
this.stats = {
|
| 931 |
-
|
| 932 |
-
|
| 933 |
};
|
| 934 |
|
| 935 |
this.authSource = new AuthSource(this.logger);
|
|
@@ -1034,9 +1034,9 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 1034 |
try {
|
| 1035 |
// 初始化统计对象
|
| 1036 |
this.authSource.getAvailableIndices().forEach(index => {
|
| 1037 |
-
|
| 1038 |
});
|
| 1039 |
-
|
| 1040 |
let startupIndex = this.authSource.getFirstAvailableIndex();
|
| 1041 |
const suggestedIndex = this.config.initialAuthIndex;
|
| 1042 |
|
|
@@ -1081,13 +1081,13 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 1081 |
let bodyContent = '无或空';
|
| 1082 |
if (req.body) {
|
| 1083 |
if (Buffer.isBuffer(req.body) && req.body.length > 0) {
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
} else if (typeof req.body === 'object' && Object.keys(req.body).length > 0) {
|
| 1090 |
-
|
| 1091 |
}
|
| 1092 |
}
|
| 1093 |
|
|
@@ -1131,7 +1131,7 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 1131 |
if (clientKey) {
|
| 1132 |
if (serverApiKeys.includes(clientKey)) {
|
| 1133 |
if (this.config.debugMode) {
|
| 1134 |
-
|
| 1135 |
}
|
| 1136 |
if (keySource === '查询参数') {
|
| 1137 |
delete req.query.key;
|
|
@@ -1151,7 +1151,7 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 1151 |
}
|
| 1152 |
|
| 1153 |
this.logger.warn(`[认证] 拒绝受保护的请求: 缺少API密钥。IP: ${req.ip}, 路径: ${req.path}`);
|
| 1154 |
-
|
| 1155 |
if (this.config.debugMode) {
|
| 1156 |
this.logger.debug(`[认证][调试] 未在任何标准位置找到API密钥。`);
|
| 1157 |
this.logger.debug(`[认证][调试] 搜索的请求头: ${JSON.stringify(headers, null, 2)}`);
|
|
@@ -1176,162 +1176,162 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 1176 |
}
|
| 1177 |
|
| 1178 |
// [可复制并覆盖]
|
| 1179 |
-
// 请用此版本完整替换您文件中的 _createExpressApp 方法
|
| 1180 |
_createExpressApp() {
|
| 1181 |
const app = express();
|
| 1182 |
app.use(express.json({ limit: '100mb' }));
|
| 1183 |
app.use(express.raw({ type: '*/*', limit: '100mb' }));
|
| 1184 |
app.use((req, res, next) => {
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
}
|
| 1195 |
}
|
| 1196 |
}
|
| 1197 |
-
|
|
|
|
| 1198 |
});
|
| 1199 |
-
|
| 1200 |
app.use(this._createDebugLogMiddleware());
|
| 1201 |
-
|
| 1202 |
// --- 仪表盘和API端点 ---
|
| 1203 |
-
|
| 1204 |
// 公开端点:提供仪表盘HTML
|
| 1205 |
app.get('/dashboard', (req, res) => {
|
| 1206 |
-
|
| 1207 |
});
|
| 1208 |
-
|
| 1209 |
// 公开端点:用于仪表盘验证API密钥
|
| 1210 |
app.post('/dashboard/verify-key', (req, res) => {
|
| 1211 |
-
|
| 1212 |
-
|
| 1213 |
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
|
| 1222 |
-
|
| 1223 |
-
|
| 1224 |
-
|
| 1225 |
-
|
| 1226 |
});
|
| 1227 |
|
| 1228 |
// 中间件:保护仪表盘API路由
|
| 1229 |
const dashboardApiAuth = (req, res, next) => {
|
| 1230 |
-
|
| 1231 |
-
|
| 1232 |
-
|
| 1233 |
-
|
| 1234 |
|
| 1235 |
-
|
| 1236 |
-
|
| 1237 |
-
|
| 1238 |
-
|
| 1239 |
-
|
| 1240 |
-
|
| 1241 |
-
|
| 1242 |
};
|
| 1243 |
|
| 1244 |
const dashboardApiRouter = express.Router();
|
| 1245 |
dashboardApiRouter.use(dashboardApiAuth);
|
| 1246 |
|
| 1247 |
dashboardApiRouter.get('/data', (req, res) => {
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
-
|
| 1254 |
-
|
| 1255 |
-
|
| 1256 |
-
|
| 1257 |
-
|
| 1258 |
-
|
| 1259 |
-
|
| 1260 |
-
|
| 1261 |
-
|
| 1262 |
-
|
| 1263 |
-
|
| 1264 |
-
|
| 1265 |
-
|
| 1266 |
-
|
| 1267 |
});
|
| 1268 |
-
|
| 1269 |
dashboardApiRouter.post('/config', (req, res) => {
|
| 1270 |
-
|
| 1271 |
-
|
| 1272 |
-
|
| 1273 |
-
|
| 1274 |
-
|
| 1275 |
-
|
| 1276 |
-
|
| 1277 |
-
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
|
| 1282 |
-
|
| 1283 |
-
|
| 1284 |
-
|
| 1285 |
-
|
| 1286 |
-
|
| 1287 |
-
|
| 1288 |
-
|
| 1289 |
-
|
| 1290 |
-
|
| 1291 |
-
|
| 1292 |
-
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
|
| 1297 |
-
|
| 1298 |
-
}
|
| 1299 |
-
this.logger.info('[管理] 配置已通过仪表盘动态更新。');
|
| 1300 |
-
res.status(200).json({ success: true, message: '配置已临时更新。' });
|
| 1301 |
-
} catch (error) {
|
| 1302 |
-
this.logger.error(`[管理] 更新配置失败: ${error.message}`);
|
| 1303 |
-
res.status(500).json({ success: false, message: error.message });
|
| 1304 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1305 |
});
|
| 1306 |
|
| 1307 |
dashboardApiRouter.post('/accounts', (req, res) => {
|
| 1308 |
-
|
| 1309 |
-
|
| 1310 |
-
|
| 1311 |
-
|
| 1312 |
-
|
| 1313 |
-
let parsedData;
|
| 1314 |
-
try {
|
| 1315 |
-
parsedData = (typeof authData === 'string') ? JSON.parse(authData) : authData;
|
| 1316 |
-
} catch(e) {
|
| 1317 |
-
return res.status(400).json({ success: false, message: "认证数据的JSON格式无效。" });
|
| 1318 |
-
}
|
| 1319 |
|
| 1320 |
-
|
| 1321 |
-
|
| 1322 |
-
|
| 1323 |
-
|
| 1324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1325 |
}
|
| 1326 |
-
|
|
|
|
| 1327 |
});
|
| 1328 |
|
| 1329 |
dashboardApiRouter.delete('/accounts/:index', (req, res) => {
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
});
|
| 1334 |
-
|
| 1335 |
// 挂载受保护的仪表盘API路由
|
| 1336 |
app.use('/dashboard', dashboardApiRouter);
|
| 1337 |
|
|
@@ -1356,7 +1356,7 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 1356 |
res.status(500).send(errorMessage);
|
| 1357 |
}
|
| 1358 |
});
|
| 1359 |
-
|
| 1360 |
app.get('/health', (req, res) => {
|
| 1361 |
res.status(200).json({
|
| 1362 |
status: 'healthy',
|
|
@@ -1398,7 +1398,7 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 1398 |
|
| 1399 |
return app;
|
| 1400 |
}
|
| 1401 |
-
|
| 1402 |
_getDashboardHtml() {
|
| 1403 |
return `
|
| 1404 |
<!DOCTYPE html>
|
|
@@ -1721,29 +1721,53 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 1721 |
});
|
| 1722 |
}
|
| 1723 |
|
| 1724 |
-
async function
|
| 1725 |
-
let apiKey = sessionStorage.getItem(API_KEY_SESSION_STORAGE);
|
| 1726 |
-
if (!apiKey) {
|
| 1727 |
-
apiKey = prompt("请输入API密钥以访问仪表盘:");
|
| 1728 |
-
if (!apiKey) { document.body.innerHTML = '<h1>访问被拒绝</h1>'; return; }
|
| 1729 |
-
}
|
| 1730 |
-
|
| 1731 |
try {
|
| 1732 |
-
const response = await fetch(\`\${API_BASE}/verify-key\`, {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1733 |
const result = await response.json();
|
| 1734 |
|
| 1735 |
if (response.ok && result.success) {
|
| 1736 |
-
|
|
|
|
|
|
|
| 1737 |
mainContainer.style.display = 'block';
|
| 1738 |
initializeDashboardListeners();
|
| 1739 |
fetchData();
|
| 1740 |
setInterval(fetchData, 5000);
|
|
|
|
| 1741 |
} else {
|
| 1742 |
sessionStorage.removeItem(API_KEY_SESSION_STORAGE);
|
| 1743 |
-
|
| 1744 |
}
|
| 1745 |
} catch (err) {
|
| 1746 |
document.body.innerHTML = \`<h1>认证时发生错误: \${err.message}</h1>\`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1747 |
}
|
| 1748 |
}
|
| 1749 |
|
|
@@ -1754,7 +1778,7 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 1754 |
</html>
|
| 1755 |
`;
|
| 1756 |
}
|
| 1757 |
-
|
| 1758 |
|
| 1759 |
|
| 1760 |
async _startWebSocketServer() {
|
|
|
|
| 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}`);
|
|
|
|
| 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 |
|
|
|
|
| 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;
|
|
|
|
| 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 |
|
|
|
|
| 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 {
|
|
|
|
| 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 |
|
|
|
|
| 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 |
+
if (process.env.DEBUG_MODE === 'true') {
|
| 373 |
+
console.debug(this._formatMessage('DEBUG', message));
|
| 374 |
}
|
| 375 |
}
|
| 376 |
}
|
|
|
|
| 640 |
}
|
| 641 |
|
| 642 |
_getModelFromRequest(req) {
|
| 643 |
+
let body = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 644 |
|
| 645 |
+
if (Buffer.isBuffer(body)) {
|
| 646 |
+
try {
|
| 647 |
+
body = JSON.parse(body.toString('utf-8'));
|
| 648 |
+
} catch (e) { body = {}; }
|
| 649 |
+
} else if (typeof body === 'string') {
|
| 650 |
+
try {
|
| 651 |
+
body = JSON.parse(body);
|
| 652 |
+
} catch (e) { body = {}; }
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
if (body && typeof body === 'object') {
|
| 656 |
+
if (body.model) return body.model;
|
| 657 |
+
if (body.generation_config && body.generation_config.model) return body.generation_config.model;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
const match = req.path.match(/\/models\/([^/:]+)/);
|
| 661 |
+
if (match && match[1]) {
|
| 662 |
+
return match[1];
|
| 663 |
+
}
|
| 664 |
+
return 'unknown_model';
|
| 665 |
}
|
| 666 |
|
| 667 |
async processRequest(req, res) {
|
|
|
|
| 671 |
|
| 672 |
// 新增的合并日志行,报告路径、账号和模型
|
| 673 |
this.logger.info(`[请求] ${req.method} ${req.path} | 账号: ${currentAccount} | 模型: 🤖 ${modelName}`);
|
| 674 |
+
|
| 675 |
// --- 升级的统计逻辑 ---
|
| 676 |
this.serverSystem.stats.totalCalls++;
|
| 677 |
if (this.serverSystem.stats.accountCalls[currentAccount]) {
|
| 678 |
+
this.serverSystem.stats.accountCalls[currentAccount].total = (this.serverSystem.stats.accountCalls[currentAccount].total || 0) + 1;
|
| 679 |
+
this.serverSystem.stats.accountCalls[currentAccount].models[modelName] = (this.serverSystem.stats.accountCalls[currentAccount].models[modelName] || 0) + 1;
|
| 680 |
} else {
|
| 681 |
+
this.serverSystem.stats.accountCalls[currentAccount] = {
|
| 682 |
+
total: 1,
|
| 683 |
+
models: { [modelName]: 1 }
|
| 684 |
+
};
|
| 685 |
}
|
| 686 |
+
|
| 687 |
if (!this.connectionRegistry.hasActiveConnections()) {
|
| 688 |
return this._sendErrorResponse(res, 503, '没有可用的浏览器连接');
|
| 689 |
}
|
|
|
|
| 925 |
this.logger = new LoggingService('ProxySystem');
|
| 926 |
this._loadConfiguration();
|
| 927 |
this.streamingMode = this.config.streamingMode;
|
| 928 |
+
|
| 929 |
// 升级后的统计结构
|
| 930 |
this.stats = {
|
| 931 |
+
totalCalls: 0,
|
| 932 |
+
accountCalls: {} // e.g., { "1": { total: 10, models: { "gemini-pro": 5, "gpt-4": 5 } } }
|
| 933 |
};
|
| 934 |
|
| 935 |
this.authSource = new AuthSource(this.logger);
|
|
|
|
| 1034 |
try {
|
| 1035 |
// 初始化统计对象
|
| 1036 |
this.authSource.getAvailableIndices().forEach(index => {
|
| 1037 |
+
this.stats.accountCalls[index] = { total: 0, models: {} };
|
| 1038 |
});
|
| 1039 |
+
|
| 1040 |
let startupIndex = this.authSource.getFirstAvailableIndex();
|
| 1041 |
const suggestedIndex = this.config.initialAuthIndex;
|
| 1042 |
|
|
|
|
| 1081 |
let bodyContent = '无或空';
|
| 1082 |
if (req.body) {
|
| 1083 |
if (Buffer.isBuffer(req.body) && req.body.length > 0) {
|
| 1084 |
+
try {
|
| 1085 |
+
bodyContent = JSON.stringify(JSON.parse(req.body.toString('utf-8')), null, 2);
|
| 1086 |
+
} catch (e) {
|
| 1087 |
+
bodyContent = `[无法解析为JSON的Buffer, 大小: ${req.body.length} 字节]`;
|
| 1088 |
+
}
|
| 1089 |
} else if (typeof req.body === 'object' && Object.keys(req.body).length > 0) {
|
| 1090 |
+
bodyContent = JSON.stringify(req.body, null, 2);
|
| 1091 |
}
|
| 1092 |
}
|
| 1093 |
|
|
|
|
| 1131 |
if (clientKey) {
|
| 1132 |
if (serverApiKeys.includes(clientKey)) {
|
| 1133 |
if (this.config.debugMode) {
|
| 1134 |
+
this.logger.debug(`[认证][调试] 在 '${keySource}' 中找到API密钥,验证通过。`);
|
| 1135 |
}
|
| 1136 |
if (keySource === '查询参数') {
|
| 1137 |
delete req.query.key;
|
|
|
|
| 1151 |
}
|
| 1152 |
|
| 1153 |
this.logger.warn(`[认证] 拒绝受保护的请求: 缺少API密钥。IP: ${req.ip}, 路径: ${req.path}`);
|
| 1154 |
+
|
| 1155 |
if (this.config.debugMode) {
|
| 1156 |
this.logger.debug(`[认证][调试] 未在任何标准位置找到API密钥。`);
|
| 1157 |
this.logger.debug(`[认证][调试] 搜索的请求头: ${JSON.stringify(headers, null, 2)}`);
|
|
|
|
| 1176 |
}
|
| 1177 |
|
| 1178 |
// [可复制并覆盖]
|
| 1179 |
+
// 请用此版本完整替换您文件中的 _createExpressApp 方法
|
| 1180 |
_createExpressApp() {
|
| 1181 |
const app = express();
|
| 1182 |
app.use(express.json({ limit: '100mb' }));
|
| 1183 |
app.use(express.raw({ type: '*/*', limit: '100mb' }));
|
| 1184 |
app.use((req, res, next) => {
|
| 1185 |
+
if (req.is('application/json') && typeof req.body === 'object' && !Buffer.isBuffer(req.body)) {
|
| 1186 |
+
// Already parsed correctly by express.json()
|
| 1187 |
+
} else if (Buffer.isBuffer(req.body)) {
|
| 1188 |
+
const bodyStr = req.body.toString('utf-8');
|
| 1189 |
+
if (bodyStr) {
|
| 1190 |
+
try {
|
| 1191 |
+
req.body = JSON.parse(bodyStr);
|
| 1192 |
+
} catch (e) {
|
| 1193 |
+
// Not JSON, leave as buffer.
|
|
|
|
| 1194 |
}
|
| 1195 |
}
|
| 1196 |
+
}
|
| 1197 |
+
next();
|
| 1198 |
});
|
| 1199 |
+
|
| 1200 |
app.use(this._createDebugLogMiddleware());
|
| 1201 |
+
|
| 1202 |
// --- 仪表盘和API端点 ---
|
| 1203 |
+
|
| 1204 |
// 公开端点:提供仪表盘HTML
|
| 1205 |
app.get('/dashboard', (req, res) => {
|
| 1206 |
+
res.send(this._getDashboardHtml());
|
| 1207 |
});
|
| 1208 |
+
|
| 1209 |
// 公开端点:用于仪表盘验证API密钥
|
| 1210 |
app.post('/dashboard/verify-key', (req, res) => {
|
| 1211 |
+
const { key } = req.body;
|
| 1212 |
+
const serverApiKeys = this.config.apiKeys;
|
| 1213 |
|
| 1214 |
+
if (!serverApiKeys || serverApiKeys.length === 0) {
|
| 1215 |
+
this.logger.info('[管理] 服务器未配置API密钥,自动授予仪表盘访问权限。');
|
| 1216 |
+
return res.json({ success: true });
|
| 1217 |
+
}
|
| 1218 |
|
| 1219 |
+
if (key && serverApiKeys.includes(key)) {
|
| 1220 |
+
this.logger.info('[管理] 仪表盘API密钥验证成功。');
|
| 1221 |
+
return res.json({ success: true });
|
| 1222 |
+
}
|
| 1223 |
+
|
| 1224 |
+
this.logger.warn(`[管理] 仪表盘API密钥验证失败。`);
|
| 1225 |
+
res.status(401).json({ success: false, message: '无效的API密钥。' });
|
| 1226 |
});
|
| 1227 |
|
| 1228 |
// 中间件:保护仪表盘API路由
|
| 1229 |
const dashboardApiAuth = (req, res, next) => {
|
| 1230 |
+
const serverApiKeys = this.config.apiKeys;
|
| 1231 |
+
if (!serverApiKeys || serverApiKeys.length === 0) {
|
| 1232 |
+
return next(); // 未配置密钥,跳过认证
|
| 1233 |
+
}
|
| 1234 |
|
| 1235 |
+
const clientKey = req.headers['x-dashboard-auth'];
|
| 1236 |
+
if (clientKey && serverApiKeys.includes(clientKey)) {
|
| 1237 |
+
return next();
|
| 1238 |
+
}
|
| 1239 |
+
|
| 1240 |
+
this.logger.warn(`[管理] 拒绝未经授权的仪表盘API请求。IP: ${req.ip}, 路径: ${req.path}`);
|
| 1241 |
+
res.status(401).json({ error: { message: 'Unauthorized dashboard access' } });
|
| 1242 |
};
|
| 1243 |
|
| 1244 |
const dashboardApiRouter = express.Router();
|
| 1245 |
dashboardApiRouter.use(dashboardApiAuth);
|
| 1246 |
|
| 1247 |
dashboardApiRouter.get('/data', (req, res) => {
|
| 1248 |
+
res.json({
|
| 1249 |
+
status: {
|
| 1250 |
+
uptime: process.uptime(),
|
| 1251 |
+
streamingMode: this.streamingMode,
|
| 1252 |
+
debugMode: this.config.debugMode,
|
| 1253 |
+
authMode: this.authSource.authMode,
|
| 1254 |
+
apiKeyAuth: (this.config.apiKeys && this.config.apiKeys.length > 0) ? '已启用' : '已禁用',
|
| 1255 |
+
isAuthSwitching: this.requestHandler.isAuthSwitching,
|
| 1256 |
+
browserConnected: !!this.browserManager.browser,
|
| 1257 |
+
internalWsClients: this.connectionRegistry.connections.size
|
| 1258 |
+
},
|
| 1259 |
+
auth: {
|
| 1260 |
+
currentAuthIndex: this.requestHandler.currentAuthIndex,
|
| 1261 |
+
accounts: this.authSource.getAccountDetails(),
|
| 1262 |
+
failureCount: this.requestHandler.failureCount,
|
| 1263 |
+
},
|
| 1264 |
+
stats: this.stats,
|
| 1265 |
+
config: this.config
|
| 1266 |
+
});
|
| 1267 |
});
|
| 1268 |
+
|
| 1269 |
dashboardApiRouter.post('/config', (req, res) => {
|
| 1270 |
+
const newConfig = req.body;
|
| 1271 |
+
try {
|
| 1272 |
+
if (newConfig.hasOwnProperty('streamingMode') && ['real', 'fake'].includes(newConfig.streamingMode)) {
|
| 1273 |
+
this.config.streamingMode = newConfig.streamingMode;
|
| 1274 |
+
this.streamingMode = newConfig.streamingMode;
|
| 1275 |
+
this.requestHandler.serverSystem.streamingMode = newConfig.streamingMode;
|
| 1276 |
+
}
|
| 1277 |
+
if (newConfig.hasOwnProperty('debugMode') && typeof newConfig.debugMode === 'boolean') {
|
| 1278 |
+
this.config.debugMode = newConfig.debugMode;
|
| 1279 |
+
}
|
| 1280 |
+
if (newConfig.hasOwnProperty('failureThreshold')) {
|
| 1281 |
+
this.config.failureThreshold = parseInt(newConfig.failureThreshold, 10) || 0;
|
| 1282 |
+
}
|
| 1283 |
+
if (newConfig.hasOwnProperty('maxRetries')) {
|
| 1284 |
+
const retries = parseInt(newConfig.maxRetries, 10);
|
| 1285 |
+
this.config.maxRetries = retries >= 0 ? retries : 3;
|
| 1286 |
+
this.requestHandler.maxRetries = this.config.maxRetries;
|
| 1287 |
+
}
|
| 1288 |
+
if (newConfig.hasOwnProperty('retryDelay')) {
|
| 1289 |
+
this.config.retryDelay = parseInt(newConfig.retryDelay, 10) || 2000;
|
| 1290 |
+
this.requestHandler.retryDelay = this.config.retryDelay;
|
| 1291 |
+
}
|
| 1292 |
+
if (newConfig.hasOwnProperty('immediateSwitchStatusCodes')) {
|
| 1293 |
+
if (Array.isArray(newConfig.immediateSwitchStatusCodes)) {
|
| 1294 |
+
this.config.immediateSwitchStatusCodes = newConfig.immediateSwitchStatusCodes
|
| 1295 |
+
.map(c => parseInt(c, 10))
|
| 1296 |
+
.filter(c => !isNaN(c));
|
| 1297 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1298 |
}
|
| 1299 |
+
this.logger.info('[管理] 配置已通过仪表盘动态更新。');
|
| 1300 |
+
res.status(200).json({ success: true, message: '配置已临时更新。' });
|
| 1301 |
+
} catch (error) {
|
| 1302 |
+
this.logger.error(`[管理] 更新配置失败: ${error.message}`);
|
| 1303 |
+
res.status(500).json({ success: false, message: error.message });
|
| 1304 |
+
}
|
| 1305 |
});
|
| 1306 |
|
| 1307 |
dashboardApiRouter.post('/accounts', (req, res) => {
|
| 1308 |
+
const { index, authData } = req.body;
|
| 1309 |
+
if (!index || !authData) {
|
| 1310 |
+
return res.status(400).json({ success: false, message: "必须提供索引和认证数据。" });
|
| 1311 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1312 |
|
| 1313 |
+
let parsedData;
|
| 1314 |
+
try {
|
| 1315 |
+
parsedData = (typeof authData === 'string') ? JSON.parse(authData) : authData;
|
| 1316 |
+
} catch (e) {
|
| 1317 |
+
return res.status(400).json({ success: false, message: "认证数据的JSON格式无效。" });
|
| 1318 |
+
}
|
| 1319 |
+
|
| 1320 |
+
const result = this.authSource.addAccount(parseInt(index, 10), parsedData);
|
| 1321 |
+
if (result.success) {
|
| 1322 |
+
if (!this.stats.accountCalls.hasOwnProperty(index)) {
|
| 1323 |
+
this.stats.accountCalls[index] = { total: 0, models: {} };
|
| 1324 |
}
|
| 1325 |
+
}
|
| 1326 |
+
res.status(result.success ? 200 : 400).json(result);
|
| 1327 |
});
|
| 1328 |
|
| 1329 |
dashboardApiRouter.delete('/accounts/:index', (req, res) => {
|
| 1330 |
+
const index = parseInt(req.params.index, 10);
|
| 1331 |
+
const result = this.authSource.removeAccount(index);
|
| 1332 |
+
res.status(result.success ? 200 : 400).json(result);
|
| 1333 |
});
|
| 1334 |
+
|
| 1335 |
// 挂载受保护的仪表盘API路由
|
| 1336 |
app.use('/dashboard', dashboardApiRouter);
|
| 1337 |
|
|
|
|
| 1356 |
res.status(500).send(errorMessage);
|
| 1357 |
}
|
| 1358 |
});
|
| 1359 |
+
|
| 1360 |
app.get('/health', (req, res) => {
|
| 1361 |
res.status(200).json({
|
| 1362 |
status: 'healthy',
|
|
|
|
| 1398 |
|
| 1399 |
return app;
|
| 1400 |
}
|
| 1401 |
+
|
| 1402 |
_getDashboardHtml() {
|
| 1403 |
return `
|
| 1404 |
<!DOCTYPE html>
|
|
|
|
| 1721 |
});
|
| 1722 |
}
|
| 1723 |
|
| 1724 |
+
async function verifyAndLoad(keyToVerify) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1725 |
try {
|
| 1726 |
+
const response = await fetch(\`\${API_BASE}/verify-key\`, {
|
| 1727 |
+
method: 'POST',
|
| 1728 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1729 |
+
body: JSON.stringify({ key: keyToVerify || '' })
|
| 1730 |
+
});
|
| 1731 |
const result = await response.json();
|
| 1732 |
|
| 1733 |
if (response.ok && result.success) {
|
| 1734 |
+
if (keyToVerify) {
|
| 1735 |
+
sessionStorage.setItem(API_KEY_SESSION_STORAGE, keyToVerify);
|
| 1736 |
+
}
|
| 1737 |
mainContainer.style.display = 'block';
|
| 1738 |
initializeDashboardListeners();
|
| 1739 |
fetchData();
|
| 1740 |
setInterval(fetchData, 5000);
|
| 1741 |
+
return true;
|
| 1742 |
} else {
|
| 1743 |
sessionStorage.removeItem(API_KEY_SESSION_STORAGE);
|
| 1744 |
+
return false;
|
| 1745 |
}
|
| 1746 |
} catch (err) {
|
| 1747 |
document.body.innerHTML = \`<h1>认证时发生错误: \${err.message}</h1>\`;
|
| 1748 |
+
return false;
|
| 1749 |
+
}
|
| 1750 |
+
}
|
| 1751 |
+
|
| 1752 |
+
async function checkAndInitiate() {
|
| 1753 |
+
const storedApiKey = sessionStorage.getItem(API_KEY_SESSION_STORAGE);
|
| 1754 |
+
|
| 1755 |
+
// 尝试使用已存储的密钥或空密钥进行验证
|
| 1756 |
+
const initialCheckSuccess = await verifyAndLoad(storedApiKey);
|
| 1757 |
+
|
| 1758 |
+
// 如果初次验证失败,说明服务器需要密钥,而我们没有提供或提供了错误的密钥
|
| 1759 |
+
if (!initialCheckSuccess) {
|
| 1760 |
+
const newApiKey = prompt("请输入API密钥以访问仪表盘 (服务器需要认证):");
|
| 1761 |
+
if (newApiKey) {
|
| 1762 |
+
// 使用用户新输入的密钥再次尝试
|
| 1763 |
+
const secondCheckSuccess = await verifyAndLoad(newApiKey);
|
| 1764 |
+
if (!secondCheckSuccess) {
|
| 1765 |
+
document.body.innerHTML = \`<h1>认证失败: 无效的API密钥</h1>\`;
|
| 1766 |
+
}
|
| 1767 |
+
} else {
|
| 1768 |
+
// 用户取消了输入
|
| 1769 |
+
document.body.innerHTML = '<h1>访问被拒绝</h1>';
|
| 1770 |
+
}
|
| 1771 |
}
|
| 1772 |
}
|
| 1773 |
|
|
|
|
| 1778 |
</html>
|
| 1779 |
`;
|
| 1780 |
}
|
| 1781 |
+
|
| 1782 |
|
| 1783 |
|
| 1784 |
async _startWebSocketServer() {
|