Spaces:
Paused
Paused
Upload unified-server.js
Browse files- unified-server.js +207 -116
unified-server.js
CHANGED
|
@@ -5,7 +5,7 @@ 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 |
// ===================================================================================
|
|
@@ -60,13 +60,13 @@ class AuthSource {
|
|
| 60 |
return;
|
| 61 |
}
|
| 62 |
}
|
| 63 |
-
|
| 64 |
// 排序并去重,确保索引列表干净有序
|
| 65 |
this.availableIndices = [...new Set(indices)].sort((a, b) => a - b);
|
| 66 |
|
| 67 |
this.logger.info(`[Auth] 在 '${this.authMode}' 模式下,检测到 ${this.availableIndices.length} 个认证源。`);
|
| 68 |
-
if(this.availableIndices.length > 0) {
|
| 69 |
-
|
| 70 |
}
|
| 71 |
}
|
| 72 |
|
|
@@ -443,16 +443,16 @@ class RequestHandler {
|
|
| 443 |
if (available.length === 1) return available[0]; // 只有一个,切给自己
|
| 444 |
|
| 445 |
const currentIndexInArray = available.indexOf(this.currentAuthIndex);
|
| 446 |
-
|
| 447 |
// 如果当前索引不知为何不在可用列表里,安全起见返回第一个
|
| 448 |
if (currentIndexInArray === -1) {
|
| 449 |
-
|
| 450 |
-
|
| 451 |
}
|
| 452 |
-
|
| 453 |
// 计算下一个索引在数组中的位置,使用模运算实现循环
|
| 454 |
const nextIndexInArray = (currentIndexInArray + 1) % available.length;
|
| 455 |
-
|
| 456 |
return available[nextIndexInArray];
|
| 457 |
}
|
| 458 |
|
|
@@ -467,10 +467,10 @@ class RequestHandler {
|
|
| 467 |
const totalAuthCount = this.authSource.getAvailableIndices().length;
|
| 468 |
|
| 469 |
if (nextAuthIndex === null) {
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
}
|
| 475 |
|
| 476 |
this.logger.info('==================================================');
|
|
@@ -526,25 +526,25 @@ class RequestHandler {
|
|
| 526 |
return correctedDetails;
|
| 527 |
}
|
| 528 |
|
| 529 |
-
|
| 530 |
// 创建一个副本进行操作,并进行深度解析
|
| 531 |
const correctedDetails = { ...errorDetails };
|
| 532 |
if (correctedDetails.message && typeof correctedDetails.message === 'string') {
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
}
|
| 547 |
}
|
|
|
|
| 548 |
}
|
| 549 |
|
| 550 |
// --- 后续逻辑使用修正后的 correctedDetails ---
|
|
@@ -563,24 +563,24 @@ class RequestHandler {
|
|
| 563 |
}
|
| 564 |
return; // 结束函数,外层循环将进行重试
|
| 565 |
}
|
| 566 |
-
|
| 567 |
// 基于失败计数的切换逻辑
|
| 568 |
if (this.config.failureThreshold > 0) {
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
}
|
| 581 |
}
|
|
|
|
| 582 |
} else {
|
| 583 |
-
|
| 584 |
}
|
| 585 |
}
|
| 586 |
|
|
@@ -634,94 +634,128 @@ class RequestHandler {
|
|
| 634 |
this.logger.info(`[Request] 已向客户端发送标准错误信号: ${errorMessage}`);
|
| 635 |
}
|
| 636 |
}
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
let connectionMaintainer = null;
|
| 642 |
-
try {
|
| 643 |
-
// 为了防止连接过早断开,可以先启动一个通用的心跳计时器
|
| 644 |
-
// 这个计时器只发送注释行,对SSE和JSON客户端都无害,但能保持连接
|
| 645 |
-
connectionMaintainer = setInterval(() => { if (!res.writableEnded) { res.write(': keep-alive\n\n'); } }, 15000);
|
| 646 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 647 |
let lastMessage, requestFailed = false;
|
| 648 |
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
| 649 |
this.logger.info(`[Request] 请求尝试 #${attempt}/${this.maxRetries}...`);
|
| 650 |
this._forwardRequest(proxyRequest);
|
| 651 |
lastMessage = await messageQueue.dequeue();
|
|
|
|
| 652 |
if (lastMessage.event_type === 'error' && lastMessage.status >= 400 && lastMessage.status <= 599) {
|
| 653 |
const correctedMessage = this._parseAndCorrectErrorDetails(lastMessage);
|
| 654 |
-
await this._handleRequestFailureAndSwitch(correctedMessage, res);
|
|
|
|
| 655 |
const errorText = `收到 ${correctedMessage.status} 错误。${attempt < this.maxRetries ? `将在 ${this.retryDelay / 1000}秒后重试...` : '已达到最大重试次数。'}`;
|
| 656 |
-
this.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
if (attempt < this.maxRetries) {
|
| 658 |
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
|
| 659 |
continue;
|
| 660 |
}
|
| 661 |
requestFailed = true;
|
| 662 |
}
|
| 663 |
-
break;
|
| 664 |
}
|
|
|
|
|
|
|
| 665 |
if (lastMessage.event_type === 'error' || requestFailed) {
|
| 666 |
const finalError = this._parseAndCorrectErrorDetails(lastMessage);
|
| 667 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
}
|
| 669 |
-
|
|
|
|
| 670 |
if (this.failureCount > 0) {
|
| 671 |
this.logger.info(`✅ [Auth] 请求成功 - 失败计数已从 ${this.failureCount} 重置为 0`);
|
| 672 |
}
|
| 673 |
this.failureCount = 0;
|
| 674 |
-
|
| 675 |
const dataMessage = await messageQueue.dequeue();
|
| 676 |
const endMessage = await messageQueue.dequeue();
|
| 677 |
if (endMessage.type !== 'STREAM_END') this.logger.warn('[Request] 未收到预期的流结束信号。');
|
| 678 |
|
| 679 |
-
//
|
| 680 |
-
if (
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
// ======================= START: CORE LOGIC CHANGE =======================
|
| 684 |
-
// 检查原始请求路径,判断客户端期望的是流还是普通JSON
|
| 685 |
-
const originalPath = req.path;
|
| 686 |
-
const isStreamRequest = originalPath.includes(':stream');
|
| 687 |
-
|
| 688 |
-
if (isStreamRequest) {
|
| 689 |
-
// 客户端想要一个流,我们模拟它 (保持原有逻辑)
|
| 690 |
-
this.logger.info(`[Request] 原始请求路径 "${originalPath}" 是流式请求,将模拟SSE响应。`);
|
| 691 |
-
res.status(200).set({
|
| 692 |
-
'Content-Type': 'text/event-stream',
|
| 693 |
-
'Cache-Control': 'no-cache',
|
| 694 |
-
'Connection': 'keep-alive'
|
| 695 |
-
});
|
| 696 |
-
// 发送数据块
|
| 697 |
res.write(`data: ${dataMessage.data}\n\n`);
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
|
|
|
| 704 |
try {
|
| 705 |
// 确保我们发送的是有效的JSON
|
| 706 |
const jsonData = JSON.parse(dataMessage.data);
|
| 707 |
res.status(200).json(jsonData);
|
| 708 |
} catch (e) {
|
| 709 |
this.logger.error(`[Request] 无法将来自浏览器的响应解析为JSON: ${e.message}`);
|
| 710 |
-
this._sendErrorResponse(res, 500, '
|
| 711 |
}
|
|
|
|
|
|
|
| 712 |
}
|
| 713 |
-
// ======================== END: CORE LOGIC CHANGE ========================
|
| 714 |
}
|
|
|
|
| 715 |
|
| 716 |
} catch (error) {
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
}
|
| 725 |
} finally {
|
| 726 |
if (connectionMaintainer) clearInterval(connectionMaintainer);
|
| 727 |
if (!res.writableEnded) res.end();
|
|
@@ -736,13 +770,13 @@ class RequestHandler {
|
|
| 736 |
this._forwardRequest(proxyRequest);
|
| 737 |
headerMessage = await messageQueue.dequeue();
|
| 738 |
if (headerMessage.event_type === 'error' && headerMessage.status >= 400 && headerMessage.status <= 599) {
|
| 739 |
-
|
| 740 |
// --- START: MODIFICATION ---
|
| 741 |
const correctedMessage = this._parseAndCorrectErrorDetails(headerMessage);
|
| 742 |
await this._handleRequestFailureAndSwitch(correctedMessage, null); // res is not available
|
| 743 |
this.logger.warn(`[Request] 收到 ${correctedMessage.status} 错误,将在 ${this.retryDelay / 1000}秒后重试...`);
|
| 744 |
// --- END: MODIFICATION ---
|
| 745 |
-
|
| 746 |
if (attempt < this.maxRetries) {
|
| 747 |
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
|
| 748 |
continue;
|
|
@@ -778,17 +812,7 @@ class RequestHandler {
|
|
| 778 |
this.logger.info('[Request] 真流式响应连接已关闭。');
|
| 779 |
}
|
| 780 |
}
|
| 781 |
-
|
| 782 |
-
if (req.path.includes('chat/completions')) {
|
| 783 |
-
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 }] };
|
| 784 |
-
return `data: ${JSON.stringify(payload)}\n\n`;
|
| 785 |
-
}
|
| 786 |
-
if (req.path.includes('generateContent') || req.path.includes('streamGenerateContent')) {
|
| 787 |
-
const payload = { candidates: [{ content: { parts: [{ text: "" }], role: "model" }, finishReason: null, index: 0, safetyRatings: [] }] };
|
| 788 |
-
return `data: ${JSON.stringify(payload)}\n\n`;
|
| 789 |
-
}
|
| 790 |
-
return 'data: {}\n\n';
|
| 791 |
-
}
|
| 792 |
_setResponseHeaders(res, headerMessage) {
|
| 793 |
res.status(headerMessage.status || 200);
|
| 794 |
const headers = headerMessage.headers || {};
|
|
@@ -830,12 +854,13 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 830 |
|
| 831 |
_loadConfiguration() {
|
| 832 |
let config = {
|
| 833 |
-
httpPort:
|
| 834 |
failureThreshold: 0,
|
| 835 |
maxRetries: 3, retryDelay: 2000, browserExecutablePath: null,
|
| 836 |
apiKeys: [],
|
| 837 |
immediateSwitchStatusCodes: [],
|
| 838 |
-
initialAuthIndex: null,
|
|
|
|
| 839 |
};
|
| 840 |
|
| 841 |
const configPath = path.join(__dirname, 'config.json');
|
|
@@ -859,12 +884,15 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 859 |
if (process.env.API_KEYS) {
|
| 860 |
config.apiKeys = process.env.API_KEYS.split(',');
|
| 861 |
}
|
|
|
|
|
|
|
|
|
|
| 862 |
// 新增:处理环境变量,它会覆盖 config.json 中的设置
|
| 863 |
if (process.env.INITIAL_AUTH_INDEX) {
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
}
|
| 869 |
|
| 870 |
|
|
@@ -900,9 +928,10 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 900 |
this.logger.info(` HTTP 服务端口: ${this.config.httpPort}`);
|
| 901 |
this.logger.info(` 监听地址: ${this.config.host}`);
|
| 902 |
this.logger.info(` 流式模式: ${this.config.streamingMode}`);
|
|
|
|
| 903 |
// 新增:在日志中显示初始索引的配置
|
| 904 |
if (this.config.initialAuthIndex) {
|
| 905 |
-
|
| 906 |
}
|
| 907 |
// MODIFIED: 日志输出已汉化
|
| 908 |
this.logger.info(` 失败计数切换: ${this.config.failureThreshold > 0 ? `连续 ${this.config.failureThreshold} 次失败后切换` : '已禁用'}`);
|
|
@@ -926,10 +955,10 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 926 |
|
| 927 |
if (suggestedIndex) {
|
| 928 |
if (this.authSource.getAvailableIndices().includes(suggestedIndex)) {
|
| 929 |
-
|
| 930 |
-
|
| 931 |
} else {
|
| 932 |
-
|
| 933 |
}
|
| 934 |
} else {
|
| 935 |
this.logger.info(`[System] 未指定启动索引,将自动使用第一个可用索引: ${startupIndex}`);
|
|
@@ -940,14 +969,54 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 940 |
await this._startWebSocketServer();
|
| 941 |
this.logger.info(`[System] 代理服务器系统启动完成。`);
|
| 942 |
this.emit('started');
|
| 943 |
-
} catch (error)
|
| 944 |
-
{
|
| 945 |
this.logger.error(`[System] 启动失败: ${error.message}`);
|
| 946 |
this.emit('error', error);
|
| 947 |
throw error;
|
| 948 |
}
|
| 949 |
}
|
| 950 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 951 |
_createAuthMiddleware() {
|
| 952 |
return (req, res, next) => {
|
| 953 |
const serverApiKeys = this.config.apiKeys;
|
|
@@ -1018,25 +1087,47 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 1018 |
|
| 1019 |
_createExpressApp() {
|
| 1020 |
const app = express();
|
|
|
|
| 1021 |
app.use(express.json({ limit: '100mb' }));
|
| 1022 |
app.use(express.raw({ type: '*/*', limit: '100mb' }));
|
| 1023 |
|
|
|
|
|
|
|
|
|
|
| 1024 |
app.get('/admin/set-mode', (req, res) => {
|
| 1025 |
const newMode = req.query.mode;
|
| 1026 |
if (newMode === 'fake' || newMode === 'real') {
|
| 1027 |
this.streamingMode = newMode;
|
|
|
|
| 1028 |
res.status(200).send(`流式模式已切换为: ${this.streamingMode}`);
|
| 1029 |
} else {
|
| 1030 |
res.status(400).send('无效模式. 请用 "fake" 或 "real".');
|
| 1031 |
}
|
| 1032 |
});
|
| 1033 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1034 |
app.get('/health', (req, res) => {
|
| 1035 |
res.status(200).json({
|
| 1036 |
status: 'healthy',
|
| 1037 |
uptime: process.uptime(),
|
| 1038 |
config: {
|
| 1039 |
streamingMode: this.streamingMode,
|
|
|
|
| 1040 |
failureThreshold: this.config.failureThreshold,
|
| 1041 |
immediateSwitchStatusCodes: this.config.immediateSwitchStatusCodes,
|
| 1042 |
maxRetries: this.config.maxRetries,
|
|
@@ -1074,7 +1165,7 @@ class ProxyServerSystem extends EventEmitter {
|
|
| 1074 |
try {
|
| 1075 |
await this.requestHandler._switchToNextAuth();
|
| 1076 |
const newIndex = this.requestHandler.currentAuthIndex;
|
| 1077 |
-
|
| 1078 |
const message = `成功将账号从索引 ${oldIndex} 切换到 ${newIndex}。`;
|
| 1079 |
this.logger.info(`[Admin] 手动切换成功。 ${message}`);
|
| 1080 |
res.status(200).send(message);
|
|
|
|
| 5 |
const fs = require('fs');
|
| 6 |
const path = require('path');
|
| 7 |
const { firefox } = require('playwright');
|
| 8 |
+
const os = require('os');
|
| 9 |
|
| 10 |
|
| 11 |
// ===================================================================================
|
|
|
|
| 60 |
return;
|
| 61 |
}
|
| 62 |
}
|
| 63 |
+
|
| 64 |
// 排序并去重,确保索引列表干净有序
|
| 65 |
this.availableIndices = [...new Set(indices)].sort((a, b) => a - b);
|
| 66 |
|
| 67 |
this.logger.info(`[Auth] 在 '${this.authMode}' 模式下,检测到 ${this.availableIndices.length} 个认证源。`);
|
| 68 |
+
if (this.availableIndices.length > 0) {
|
| 69 |
+
this.logger.info(`[Auth] 可用索引列表: [${this.availableIndices.join(', ')}]`);
|
| 70 |
}
|
| 71 |
}
|
| 72 |
|
|
|
|
| 443 |
if (available.length === 1) return available[0]; // 只有一个,切给自己
|
| 444 |
|
| 445 |
const currentIndexInArray = available.indexOf(this.currentAuthIndex);
|
| 446 |
+
|
| 447 |
// 如果当前索引不知为何不在可用列表里,安全起见返回第一个
|
| 448 |
if (currentIndexInArray === -1) {
|
| 449 |
+
this.logger.warn(`[Auth] 当前索引 ${this.currentAuthIndex} 不在可用列表中,将切换到第一个可用索引。`);
|
| 450 |
+
return available[0];
|
| 451 |
}
|
| 452 |
+
|
| 453 |
// 计算下一个索引在数组中的位置,使用模运算实现循环
|
| 454 |
const nextIndexInArray = (currentIndexInArray + 1) % available.length;
|
| 455 |
+
|
| 456 |
return available[nextIndexInArray];
|
| 457 |
}
|
| 458 |
|
|
|
|
| 467 |
const totalAuthCount = this.authSource.getAvailableIndices().length;
|
| 468 |
|
| 469 |
if (nextAuthIndex === null) {
|
| 470 |
+
this.logger.error('🔴 [Auth] 无法切换账号,因为没有可用的认证源!');
|
| 471 |
+
this.isAuthSwitching = false;
|
| 472 |
+
// 抛出错误以便调用者可以捕获它
|
| 473 |
+
throw new Error('No available authentication sources to switch to.');
|
| 474 |
}
|
| 475 |
|
| 476 |
this.logger.info('==================================================');
|
|
|
|
| 526 |
return correctedDetails;
|
| 527 |
}
|
| 528 |
|
| 529 |
+
async _handleRequestFailureAndSwitch(errorDetails, res) {
|
| 530 |
// 创建一个副本进行操作,并进行深度解析
|
| 531 |
const correctedDetails = { ...errorDetails };
|
| 532 |
if (correctedDetails.message && typeof correctedDetails.message === 'string') {
|
| 533 |
+
// 增强版正则表达式,能匹配 "HTTP 429" 或 JSON 中的 "code":429 等多种模式
|
| 534 |
+
const regex = /(?:HTTP|status code)\s*(\d{3})|"code"\s*:\s*(\d{3})/;
|
| 535 |
+
const match = correctedDetails.message.match(regex);
|
| 536 |
+
|
| 537 |
+
// match[1] 对应 (?:HTTP|status code)\s*(\d{3})
|
| 538 |
+
// match[2] 对应 "code"\s*:\s*(\d{3})
|
| 539 |
+
const parsedStatusString = match ? (match[1] || match[2]) : null;
|
| 540 |
+
|
| 541 |
+
if (parsedStatusString) {
|
| 542 |
+
const parsedStatus = parseInt(parsedStatusString, 10);
|
| 543 |
+
if (parsedStatus >= 400 && parsedStatus <= 599 && correctedDetails.status !== parsedStatus) {
|
| 544 |
+
this.logger.warn(`[Auth] 修正了错误状态码!原始: ${correctedDetails.status}, 从消息中解析得到: ${parsedStatus}`);
|
| 545 |
+
correctedDetails.status = parsedStatus;
|
|
|
|
| 546 |
}
|
| 547 |
+
}
|
| 548 |
}
|
| 549 |
|
| 550 |
// --- 后续逻辑使用修正后的 correctedDetails ---
|
|
|
|
| 563 |
}
|
| 564 |
return; // 结束函数,外层循环将进行重试
|
| 565 |
}
|
| 566 |
+
|
| 567 |
// 基于失败计数的切换逻辑
|
| 568 |
if (this.config.failureThreshold > 0) {
|
| 569 |
+
this.failureCount++;
|
| 570 |
+
this.logger.warn(`⚠️ [Auth] 请求失败 - 失败计数: ${this.failureCount}/${this.config.failureThreshold} (当前账号索引: ${this.currentAuthIndex}, 状态码: ${correctedDetails.status})`);
|
| 571 |
+
if (this.failureCount >= this.config.failureThreshold) {
|
| 572 |
+
this.logger.warn(`🔴 [Auth] 达到失败阈值!准备切换账号...`);
|
| 573 |
+
if (res) this._sendErrorChunkToClient(res, `连续失败${this.failureCount}次,正在尝试切换账号...`);
|
| 574 |
+
try {
|
| 575 |
+
await this._switchToNextAuth();
|
| 576 |
+
if (res) this._sendErrorChunkToClient(res, `已切换到账号索引 ${this.currentAuthIndex},请重试`);
|
| 577 |
+
} catch (switchError) {
|
| 578 |
+
this.logger.error(`🔴 [Auth] 账号切换失败: ${switchError.message}`);
|
| 579 |
+
if (res) this._sendErrorChunkToClient(res, `切换账号失败: ${switchError.message}`);
|
|
|
|
| 580 |
}
|
| 581 |
+
}
|
| 582 |
} else {
|
| 583 |
+
this.logger.warn(`[Auth] 请求失败 (状态码: ${correctedDetails.status})。基于计数的自动切换已禁用 (failureThreshold=0)`);
|
| 584 |
}
|
| 585 |
}
|
| 586 |
|
|
|
|
| 634 |
this.logger.info(`[Request] 已向客户端发送标准错误信号: ${errorMessage}`);
|
| 635 |
}
|
| 636 |
}
|
| 637 |
+
|
| 638 |
+
//========================================================
|
| 639 |
+
// START: MODIFIED SECTION
|
| 640 |
+
//========================================================
|
| 641 |
+
|
| 642 |
+
_getKeepAliveChunk(req) {
|
| 643 |
+
if (req.path.includes('chat/completions')) {
|
| 644 |
+
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 }] };
|
| 645 |
+
return `data: ${JSON.stringify(payload)}\n\n`;
|
| 646 |
+
}
|
| 647 |
+
if (req.path.includes('generateContent') || req.path.includes('streamGenerateContent')) {
|
| 648 |
+
const payload = { candidates: [{ content: { parts: [{ text: "" }], role: "model" }, finishReason: null, index: 0, safetyRatings: [] }] };
|
| 649 |
+
return `data: ${JSON.stringify(payload)}\n\n`;
|
| 650 |
+
}
|
| 651 |
+
// Provide a generic, harmless default
|
| 652 |
+
return 'data: {}\n\n';
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
async _handlePseudoStreamResponse(proxyRequest, messageQueue, req, res) {
|
| 656 |
+
// 关键决策点: 通过请求路径判断客户端期望的是流还是普通JSON
|
| 657 |
+
const originalPath = req.path;
|
| 658 |
+
const isStreamRequest = originalPath.includes(':stream');
|
| 659 |
+
|
| 660 |
+
this.logger.info(`[Request] 假流式处理流程启动,路径: "${originalPath}",判定为: ${isStreamRequest ? '流式请求' : '非流式请求'}`);
|
| 661 |
+
|
| 662 |
let connectionMaintainer = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
|
| 664 |
+
// 只有在确定是流式请求时,才立即发送头并启动心跳
|
| 665 |
+
if (isStreamRequest) {
|
| 666 |
+
res.status(200).set({
|
| 667 |
+
'Content-Type': 'text/event-stream',
|
| 668 |
+
'Cache-Control': 'no-cache',
|
| 669 |
+
'Connection': 'keep-alive'
|
| 670 |
+
});
|
| 671 |
+
const keepAliveChunk = this._getKeepAliveChunk(req);
|
| 672 |
+
connectionMaintainer = setInterval(() => { if (!res.writableEnded) res.write(keepAliveChunk); }, 2000);
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
try {
|
| 676 |
let lastMessage, requestFailed = false;
|
| 677 |
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
| 678 |
this.logger.info(`[Request] 请求尝试 #${attempt}/${this.maxRetries}...`);
|
| 679 |
this._forwardRequest(proxyRequest);
|
| 680 |
lastMessage = await messageQueue.dequeue();
|
| 681 |
+
|
| 682 |
if (lastMessage.event_type === 'error' && lastMessage.status >= 400 && lastMessage.status <= 599) {
|
| 683 |
const correctedMessage = this._parseAndCorrectErrorDetails(lastMessage);
|
| 684 |
+
await this._handleRequestFailureAndSwitch(correctedMessage, isStreamRequest ? res : null); // 仅在流模式下才向客户端发送错误块
|
| 685 |
+
|
| 686 |
const errorText = `收到 ${correctedMessage.status} 错误。${attempt < this.maxRetries ? `将在 ${this.retryDelay / 1000}秒后重试...` : '已达到最大重试次数。'}`;
|
| 687 |
+
this.logger.warn(`[Request] ${errorText}`);
|
| 688 |
+
|
| 689 |
+
// 如果是流式请求,则通过数据块通知客户端错误
|
| 690 |
+
if (isStreamRequest) {
|
| 691 |
+
this._sendErrorChunkToClient(res, errorText);
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
if (attempt < this.maxRetries) {
|
| 695 |
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
|
| 696 |
continue;
|
| 697 |
}
|
| 698 |
requestFailed = true;
|
| 699 |
}
|
| 700 |
+
break; // 成功则跳出循环
|
| 701 |
}
|
| 702 |
+
|
| 703 |
+
// 如果所有重试都失败
|
| 704 |
if (lastMessage.event_type === 'error' || requestFailed) {
|
| 705 |
const finalError = this._parseAndCorrectErrorDetails(lastMessage);
|
| 706 |
+
// 对于非流式请求,现在可以安全地发送一个完整的错误响应
|
| 707 |
+
if (!res.headersSent) {
|
| 708 |
+
this._sendErrorResponse(res, finalError.status, `请求失败: ${finalError.message}`);
|
| 709 |
+
} else { // 对于流式请求,只能发送最后一个错误块
|
| 710 |
+
this._sendErrorChunkToClient(res, `请求最终失败 (状态码: ${finalError.status}): ${finalError.message}`);
|
| 711 |
+
}
|
| 712 |
+
return; // 结束函数
|
| 713 |
}
|
| 714 |
+
|
| 715 |
+
// 请求成功
|
| 716 |
if (this.failureCount > 0) {
|
| 717 |
this.logger.info(`✅ [Auth] 请求成功 - 失败计数已从 ${this.failureCount} 重置为 0`);
|
| 718 |
}
|
| 719 |
this.failureCount = 0;
|
| 720 |
+
|
| 721 |
const dataMessage = await messageQueue.dequeue();
|
| 722 |
const endMessage = await messageQueue.dequeue();
|
| 723 |
if (endMessage.type !== 'STREAM_END') this.logger.warn('[Request] 未收到预期的流结束信号。');
|
| 724 |
|
| 725 |
+
// ======================= 核心逻辑:根据请求类型格式化最终响应 =======================
|
| 726 |
+
if (isStreamRequest) {
|
| 727 |
+
// 客户端想要一个流,我们发送SSE数据块
|
| 728 |
+
if (dataMessage.data) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
res.write(`data: ${dataMessage.data}\n\n`);
|
| 730 |
+
}
|
| 731 |
+
res.write('data: [DONE]\n\n');
|
| 732 |
+
this.logger.info('[Request] 已将完整响应作为模拟SSE事件发送。');
|
| 733 |
+
} else {
|
| 734 |
+
// 客户端想要一个普通JSON,我们直接返回它
|
| 735 |
+
this.logger.info('[Request] 准备发送 application/json 响应。');
|
| 736 |
+
if (dataMessage.data) {
|
| 737 |
try {
|
| 738 |
// 确保我们发送的是有效的JSON
|
| 739 |
const jsonData = JSON.parse(dataMessage.data);
|
| 740 |
res.status(200).json(jsonData);
|
| 741 |
} catch (e) {
|
| 742 |
this.logger.error(`[Request] 无法将来自浏览器的响应解析为JSON: ${e.message}`);
|
| 743 |
+
this._sendErrorResponse(res, 500, '代理内部错误:无法解析来自后端的响应。');
|
| 744 |
}
|
| 745 |
+
} else {
|
| 746 |
+
this._sendErrorResponse(res, 500, '代理内部错误:后端未返回有效数据。');
|
| 747 |
}
|
|
|
|
| 748 |
}
|
| 749 |
+
// =================================================================================
|
| 750 |
|
| 751 |
} catch (error) {
|
| 752 |
+
// 这个 catch 块处理意外错误,比如队列超时
|
| 753 |
+
this.logger.error(`[Request] 假流式处理期间发生意外错误: ${error.message}`);
|
| 754 |
+
if (!res.headersSent) {
|
| 755 |
+
this._handleRequestError(error, res);
|
| 756 |
+
} else {
|
| 757 |
+
this._sendErrorChunkToClient(res, `处理失败: ${error.message}`);
|
| 758 |
+
}
|
|
|
|
| 759 |
} finally {
|
| 760 |
if (connectionMaintainer) clearInterval(connectionMaintainer);
|
| 761 |
if (!res.writableEnded) res.end();
|
|
|
|
| 770 |
this._forwardRequest(proxyRequest);
|
| 771 |
headerMessage = await messageQueue.dequeue();
|
| 772 |
if (headerMessage.event_type === 'error' && headerMessage.status >= 400 && headerMessage.status <= 599) {
|
| 773 |
+
|
| 774 |
// --- START: MODIFICATION ---
|
| 775 |
const correctedMessage = this._parseAndCorrectErrorDetails(headerMessage);
|
| 776 |
await this._handleRequestFailureAndSwitch(correctedMessage, null); // res is not available
|
| 777 |
this.logger.warn(`[Request] 收到 ${correctedMessage.status} 错误,将在 ${this.retryDelay / 1000}秒后重试...`);
|
| 778 |
// --- END: MODIFICATION ---
|
| 779 |
+
|
| 780 |
if (attempt < this.maxRetries) {
|
| 781 |
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
|
| 782 |
continue;
|
|
|
|
| 812 |
this.logger.info('[Request] 真流式响应连接已关闭。');
|
| 813 |
}
|
| 814 |
}
|
| 815 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 816 |
_setResponseHeaders(res, headerMessage) {
|
| 817 |
res.status(headerMessage.status || 200);
|
| 818 |
const headers = headerMessage.headers || {};
|
|
|
|
| 854 |
|
| 855 |
_loadConfiguration() {
|
| 856 |
let config = {
|
| 857 |
+
httpPort: 8889, host: '0.0.0.0', wsPort: 9998, streamingMode: 'real',
|
| 858 |
failureThreshold: 0,
|
| 859 |
maxRetries: 3, retryDelay: 2000, browserExecutablePath: null,
|
| 860 |
apiKeys: [],
|
| 861 |
immediateSwitchStatusCodes: [],
|
| 862 |
+
initialAuthIndex: null,
|
| 863 |
+
debugMode: false, // [新增] 调试模式默认关闭
|
| 864 |
};
|
| 865 |
|
| 866 |
const configPath = path.join(__dirname, 'config.json');
|
|
|
|
| 884 |
if (process.env.API_KEYS) {
|
| 885 |
config.apiKeys = process.env.API_KEYS.split(',');
|
| 886 |
}
|
| 887 |
+
if (process.env.DEBUG_MODE) { // [新增] 从环境变量读取调试模式
|
| 888 |
+
config.debugMode = process.env.DEBUG_MODE === 'true';
|
| 889 |
+
}
|
| 890 |
// 新增:处理环境变量,它会覆盖 config.json 中的设置
|
| 891 |
if (process.env.INITIAL_AUTH_INDEX) {
|
| 892 |
+
const envIndex = parseInt(process.env.INITIAL_AUTH_INDEX, 10);
|
| 893 |
+
if (!isNaN(envIndex) && envIndex > 0) {
|
| 894 |
+
config.initialAuthIndex = envIndex;
|
| 895 |
+
}
|
| 896 |
}
|
| 897 |
|
| 898 |
|
|
|
|
| 928 |
this.logger.info(` HTTP 服务端口: ${this.config.httpPort}`);
|
| 929 |
this.logger.info(` 监听地址: ${this.config.host}`);
|
| 930 |
this.logger.info(` 流式模式: ${this.config.streamingMode}`);
|
| 931 |
+
this.logger.info(` 调试模式: ${this.config.debugMode ? '已开启' : '已关闭'}`); // [新增] 打印调试模式状态
|
| 932 |
// 新增:在日志中显示初始索引的配置
|
| 933 |
if (this.config.initialAuthIndex) {
|
| 934 |
+
this.logger.info(` 指定初始认证索引: ${this.config.initialAuthIndex}`);
|
| 935 |
}
|
| 936 |
// MODIFIED: 日志输出已汉化
|
| 937 |
this.logger.info(` 失败计数切换: ${this.config.failureThreshold > 0 ? `连续 ${this.config.failureThreshold} 次失败后切换` : '已禁用'}`);
|
|
|
|
| 955 |
|
| 956 |
if (suggestedIndex) {
|
| 957 |
if (this.authSource.getAvailableIndices().includes(suggestedIndex)) {
|
| 958 |
+
this.logger.info(`[System] 使用配置中指定的有效启动索引: ${suggestedIndex}`);
|
| 959 |
+
startupIndex = suggestedIndex;
|
| 960 |
} else {
|
| 961 |
+
this.logger.warn(`[System] 配置中指定的启动索引 ${suggestedIndex} 无效或不存在,将使用第一个可用索引: ${startupIndex}`);
|
| 962 |
}
|
| 963 |
} else {
|
| 964 |
this.logger.info(`[System] 未指定启动索引,将自动使用第一个可用索引: ${startupIndex}`);
|
|
|
|
| 969 |
await this._startWebSocketServer();
|
| 970 |
this.logger.info(`[System] 代理服务器系统启动完成。`);
|
| 971 |
this.emit('started');
|
| 972 |
+
} catch (error) {
|
|
|
|
| 973 |
this.logger.error(`[System] 启动失败: ${error.message}`);
|
| 974 |
this.emit('error', error);
|
| 975 |
throw error;
|
| 976 |
}
|
| 977 |
}
|
| 978 |
|
| 979 |
+
// [新增] 调试日志中间件
|
| 980 |
+
_createDebugLogMiddleware() {
|
| 981 |
+
return (req, res, next) => {
|
| 982 |
+
if (!this.config.debugMode) {
|
| 983 |
+
return next();
|
| 984 |
+
}
|
| 985 |
+
|
| 986 |
+
const requestId = this.requestHandler._generateRequestId();
|
| 987 |
+
const log = this.logger.info.bind(this.logger); // 使用 info 级别以保证显示
|
| 988 |
+
|
| 989 |
+
log(`\n\n--- [DEBUG] START INCOMING REQUEST (${requestId}) ---`);
|
| 990 |
+
log(`[DEBUG][${requestId}] Client IP: ${req.ip}`);
|
| 991 |
+
log(`[DEBUG][${requestId}] Method: ${req.method}`);
|
| 992 |
+
log(`[DEBUG][${requestId}] URL: ${req.originalUrl}`);
|
| 993 |
+
log(`[DEBUG][${requestId}] Headers: ${JSON.stringify(req.headers, null, 2)}`);
|
| 994 |
+
|
| 995 |
+
// 智能处理请求体
|
| 996 |
+
let bodyContent = 'N/A or empty';
|
| 997 |
+
if (req.body) {
|
| 998 |
+
if (Buffer.isBuffer(req.body) && req.body.length > 0) {
|
| 999 |
+
// 对于 buffer,尝试以 utf-8 解码,如果失败则显示原始 buffer 信息
|
| 1000 |
+
try {
|
| 1001 |
+
bodyContent = req.body.toString('utf-8');
|
| 1002 |
+
} catch (e) {
|
| 1003 |
+
bodyContent = `[Non-UTF8 Buffer, size: ${req.body.length} bytes]`;
|
| 1004 |
+
}
|
| 1005 |
+
} else if (typeof req.body === 'object' && Object.keys(req.body).length > 0) {
|
| 1006 |
+
bodyContent = JSON.stringify(req.body, null, 2);
|
| 1007 |
+
} else if (typeof req.body === 'string' && req.body.length > 0) {
|
| 1008 |
+
bodyContent = req.body;
|
| 1009 |
+
}
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
log(`[DEBUG][${requestId}] Body:\n${bodyContent}`);
|
| 1013 |
+
log(`--- [DEBUG] END INCOMING REQUEST (${requestId}) ---\n\n`);
|
| 1014 |
+
|
| 1015 |
+
next();
|
| 1016 |
+
};
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
|
| 1020 |
_createAuthMiddleware() {
|
| 1021 |
return (req, res, next) => {
|
| 1022 |
const serverApiKeys = this.config.apiKeys;
|
|
|
|
| 1087 |
|
| 1088 |
_createExpressApp() {
|
| 1089 |
const app = express();
|
| 1090 |
+
// [修改] body-parser 中间件需要先于我们的调试中间件
|
| 1091 |
app.use(express.json({ limit: '100mb' }));
|
| 1092 |
app.use(express.raw({ type: '*/*', limit: '100mb' }));
|
| 1093 |
|
| 1094 |
+
// [新增] 插入调试日志中间件。它会在body解析后,但在任何业务逻辑之前运行。
|
| 1095 |
+
app.use(this._createDebugLogMiddleware());
|
| 1096 |
+
|
| 1097 |
app.get('/admin/set-mode', (req, res) => {
|
| 1098 |
const newMode = req.query.mode;
|
| 1099 |
if (newMode === 'fake' || newMode === 'real') {
|
| 1100 |
this.streamingMode = newMode;
|
| 1101 |
+
this.logger.info(`[Admin] 流式模式已切换为: ${this.streamingMode}`);
|
| 1102 |
res.status(200).send(`流式模式已切换为: ${this.streamingMode}`);
|
| 1103 |
} else {
|
| 1104 |
res.status(400).send('无效模式. 请用 "fake" 或 "real".');
|
| 1105 |
}
|
| 1106 |
});
|
| 1107 |
|
| 1108 |
+
// [新增] 切换调试模式的管理端点
|
| 1109 |
+
app.get('/admin/set-debug', (req, res) => {
|
| 1110 |
+
const enable = req.query.enable;
|
| 1111 |
+
if (enable === 'true') {
|
| 1112 |
+
this.config.debugMode = true;
|
| 1113 |
+
this.logger.info('[Admin] 调试模式已开启 (Debug Mode ON)');
|
| 1114 |
+
res.status(200).send('调试模式已开启 (Debug Mode ON)');
|
| 1115 |
+
} else if (enable === 'false') {
|
| 1116 |
+
this.config.debugMode = false;
|
| 1117 |
+
this.logger.info('[Admin] 调试模式已关闭 (Debug Mode OFF)');
|
| 1118 |
+
res.status(200).send('调试模式已关闭 (Debug Mode OFF)');
|
| 1119 |
+
} else {
|
| 1120 |
+
res.status(400).send('无效的参数. 请使用 ?enable=true 或 ?enable=false');
|
| 1121 |
+
}
|
| 1122 |
+
});
|
| 1123 |
+
|
| 1124 |
app.get('/health', (req, res) => {
|
| 1125 |
res.status(200).json({
|
| 1126 |
status: 'healthy',
|
| 1127 |
uptime: process.uptime(),
|
| 1128 |
config: {
|
| 1129 |
streamingMode: this.streamingMode,
|
| 1130 |
+
debugMode: this.config.debugMode, // [新增] 在健康检查中报告调试模式状态
|
| 1131 |
failureThreshold: this.config.failureThreshold,
|
| 1132 |
immediateSwitchStatusCodes: this.config.immediateSwitchStatusCodes,
|
| 1133 |
maxRetries: this.config.maxRetries,
|
|
|
|
| 1165 |
try {
|
| 1166 |
await this.requestHandler._switchToNextAuth();
|
| 1167 |
const newIndex = this.requestHandler.currentAuthIndex;
|
| 1168 |
+
|
| 1169 |
const message = `成功将账号从索引 ${oldIndex} 切换到 ${newIndex}。`;
|
| 1170 |
this.logger.info(`[Admin] 手动切换成功。 ${message}`);
|
| 1171 |
res.status(200).send(message);
|