aicodeswitch 3.0.19 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/server/fs-database.js +80 -2
- package/dist/server/main.js +23 -0
- package/dist/server/proxy-server.js +256 -79
- package/dist/server/type-migration.js +68 -0
- package/dist/ui/assets/{index-BCdyCT1c.js → index-B_Vn-9wd.js} +8 -7
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
|
@@ -158,7 +158,10 @@ class ProxyServer {
|
|
|
158
158
|
}
|
|
159
159
|
// 尝试每个规则,直到成功或全部失败
|
|
160
160
|
let lastError = null;
|
|
161
|
-
|
|
161
|
+
let lastFailedRule = null;
|
|
162
|
+
let lastFailedService = null;
|
|
163
|
+
for (let index = 0; index < allRules.length; index++) {
|
|
164
|
+
const rule = allRules[index];
|
|
162
165
|
const service = this.getServiceById(rule.targetServiceId);
|
|
163
166
|
if (!service)
|
|
164
167
|
continue;
|
|
@@ -169,13 +172,19 @@ class ProxyServer {
|
|
|
169
172
|
continue;
|
|
170
173
|
}
|
|
171
174
|
try {
|
|
175
|
+
const nextServiceName = yield this.findNextAvailableServiceName(allRules, index + 1, route.id);
|
|
172
176
|
// 尝试代理请求
|
|
173
|
-
yield this.proxyRequest(req, res, route, rule, service
|
|
177
|
+
yield this.proxyRequest(req, res, route, rule, service, {
|
|
178
|
+
failoverEnabled: true,
|
|
179
|
+
forwardedToServiceName: nextServiceName,
|
|
180
|
+
});
|
|
174
181
|
return; // 成功,直接返回
|
|
175
182
|
}
|
|
176
183
|
catch (error) {
|
|
177
184
|
console.error(`Service ${service.name} failed:`, error.message);
|
|
178
185
|
lastError = error;
|
|
186
|
+
lastFailedRule = rule;
|
|
187
|
+
lastFailedService = service;
|
|
179
188
|
// 检测是否是 timeout 错误
|
|
180
189
|
const isTimeout = error.code === 'ECONNABORTED' ||
|
|
181
190
|
((_b = error.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('timeout')) ||
|
|
@@ -201,6 +210,21 @@ class ProxyServer {
|
|
|
201
210
|
}
|
|
202
211
|
// 所有服务都失败了
|
|
203
212
|
console.error('All services failed');
|
|
213
|
+
// 如果有失败的服务但都在黑名单中,尝试使用最后一个失败的服务(作为 fallback)
|
|
214
|
+
if (lastFailedRule && lastFailedService) {
|
|
215
|
+
console.log(`All services in blacklist, attempting fallback to last failed service: ${lastFailedService.name}`);
|
|
216
|
+
try {
|
|
217
|
+
yield this.proxyRequest(req, res, route, lastFailedRule, lastFailedService, {
|
|
218
|
+
failoverEnabled: false, // Fallback 模式不启用故障切换
|
|
219
|
+
forwardedToServiceName: undefined,
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
catch (fallbackError) {
|
|
224
|
+
console.error(`Fallback to service ${lastFailedService.name} also failed:`, fallbackError.message);
|
|
225
|
+
lastError = fallbackError;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
204
228
|
// 记录日志
|
|
205
229
|
if (((_d = this.config) === null || _d === void 0 ? void 0 : _d.enableLogging) !== false && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
206
230
|
yield this.dbManager.addLog({
|
|
@@ -339,7 +363,10 @@ class ProxyServer {
|
|
|
339
363
|
}
|
|
340
364
|
// 尝试每个规则,直到成功或全部失败
|
|
341
365
|
let lastError = null;
|
|
342
|
-
|
|
366
|
+
let lastFailedRule = null;
|
|
367
|
+
let lastFailedService = null;
|
|
368
|
+
for (let index = 0; index < allRules.length; index++) {
|
|
369
|
+
const rule = allRules[index];
|
|
343
370
|
const service = this.getServiceById(rule.targetServiceId);
|
|
344
371
|
if (!service)
|
|
345
372
|
continue;
|
|
@@ -350,13 +377,19 @@ class ProxyServer {
|
|
|
350
377
|
continue;
|
|
351
378
|
}
|
|
352
379
|
try {
|
|
380
|
+
const nextServiceName = yield this.findNextAvailableServiceName(allRules, index + 1, route.id);
|
|
353
381
|
// 尝试代理请求
|
|
354
|
-
yield this.proxyRequest(req, res, route, rule, service
|
|
382
|
+
yield this.proxyRequest(req, res, route, rule, service, {
|
|
383
|
+
failoverEnabled: true,
|
|
384
|
+
forwardedToServiceName: nextServiceName,
|
|
385
|
+
});
|
|
355
386
|
return; // 成功,直接返回
|
|
356
387
|
}
|
|
357
388
|
catch (error) {
|
|
358
389
|
console.error(`Service ${service.name} failed:`, error.message);
|
|
359
390
|
lastError = error;
|
|
391
|
+
lastFailedRule = rule;
|
|
392
|
+
lastFailedService = service;
|
|
360
393
|
// 检测是否是 timeout 错误
|
|
361
394
|
const isTimeout = error.code === 'ECONNABORTED' ||
|
|
362
395
|
((_b = error.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('timeout')) ||
|
|
@@ -382,6 +415,21 @@ class ProxyServer {
|
|
|
382
415
|
}
|
|
383
416
|
// 所有服务都失败了
|
|
384
417
|
console.error('All services failed');
|
|
418
|
+
// 如果有失败的服务但都在黑名单中,尝试使用最后一个失败的服务(作为 fallback)
|
|
419
|
+
if (lastFailedRule && lastFailedService) {
|
|
420
|
+
console.log(`All services in blacklist, attempting fallback to last failed service: ${lastFailedService.name}`);
|
|
421
|
+
try {
|
|
422
|
+
yield this.proxyRequest(req, res, route, lastFailedRule, lastFailedService, {
|
|
423
|
+
failoverEnabled: false, // Fallback 模式不启用故障切换
|
|
424
|
+
forwardedToServiceName: undefined,
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
catch (fallbackError) {
|
|
429
|
+
console.error(`Fallback to service ${lastFailedService.name} also failed:`, fallbackError.message);
|
|
430
|
+
lastError = fallbackError;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
385
433
|
// 记录日志
|
|
386
434
|
if (((_d = this.config) === null || _d === void 0 ? void 0 : _d.enableLogging) !== false && SUPPORTED_TARGETS.some(target => req.path.startsWith(`/${target}/`))) {
|
|
387
435
|
yield this.dbManager.addLog({
|
|
@@ -497,6 +545,36 @@ class ProxyServer {
|
|
|
497
545
|
const activeRoutes = this.getActiveRoutes();
|
|
498
546
|
return activeRoutes.find(route => route.targetType === targetType && route.isActive);
|
|
499
547
|
}
|
|
548
|
+
findNextAvailableServiceName(allRules, startIndex, routeId) {
|
|
549
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
550
|
+
for (let index = startIndex; index < allRules.length; index++) {
|
|
551
|
+
const rule = allRules[index];
|
|
552
|
+
const service = this.getServiceById(rule.targetServiceId);
|
|
553
|
+
if (!service)
|
|
554
|
+
continue;
|
|
555
|
+
const isBlacklisted = yield this.dbManager.isServiceBlacklisted(service.id, routeId, rule.contentType);
|
|
556
|
+
if (isBlacklisted)
|
|
557
|
+
continue;
|
|
558
|
+
return service.name;
|
|
559
|
+
}
|
|
560
|
+
return undefined;
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
buildFailoverHint(forwardedToServiceName) {
|
|
564
|
+
if (!forwardedToServiceName) {
|
|
565
|
+
return '';
|
|
566
|
+
}
|
|
567
|
+
return `;已自动转发给 ${forwardedToServiceName} 服务继续处理`;
|
|
568
|
+
}
|
|
569
|
+
createFailoverError(message, statusCode, originalError) {
|
|
570
|
+
const failoverError = new Error(message);
|
|
571
|
+
failoverError.isFailoverCandidate = true;
|
|
572
|
+
failoverError.response = { status: statusCode };
|
|
573
|
+
if (originalError === null || originalError === void 0 ? void 0 : originalError.stack) {
|
|
574
|
+
failoverError.stack = originalError.stack;
|
|
575
|
+
}
|
|
576
|
+
return failoverError;
|
|
577
|
+
}
|
|
500
578
|
/**
|
|
501
579
|
* 计算请求内容的哈希值,用于去重
|
|
502
580
|
* 基于请求的关键字段生成唯一标识
|
|
@@ -945,6 +1023,10 @@ class ProxyServer {
|
|
|
945
1023
|
type: 'thinking',
|
|
946
1024
|
match: (_req, body) => this.hasThinkingSignal(body),
|
|
947
1025
|
},
|
|
1026
|
+
{
|
|
1027
|
+
type: 'high-iq',
|
|
1028
|
+
match: (_req, body) => this.hasHighIqSignal(body),
|
|
1029
|
+
},
|
|
948
1030
|
{
|
|
949
1031
|
type: 'long-context',
|
|
950
1032
|
match: (_req, body) => this.hasLongContextSignal(body),
|
|
@@ -1009,6 +1091,10 @@ class ProxyServer {
|
|
|
1009
1091
|
bg: 'background',
|
|
1010
1092
|
thinking: 'thinking',
|
|
1011
1093
|
reasoning: 'thinking',
|
|
1094
|
+
'high-iq': 'high-iq',
|
|
1095
|
+
high_iq: 'high-iq',
|
|
1096
|
+
highiq: 'high-iq',
|
|
1097
|
+
smart: 'high-iq',
|
|
1012
1098
|
'long-context': 'long-context',
|
|
1013
1099
|
long_context: 'long-context',
|
|
1014
1100
|
long: 'long-context',
|
|
@@ -1050,6 +1136,65 @@ class ProxyServer {
|
|
|
1050
1136
|
((_a = body === null || body === void 0 ? void 0 : body.reasoning) === null || _a === void 0 ? void 0 : _a.effort) ||
|
|
1051
1137
|
((_b = body === null || body === void 0 ? void 0 : body.reasoning) === null || _b === void 0 ? void 0 : _b.enabled));
|
|
1052
1138
|
}
|
|
1139
|
+
hasHighIqSignal(body) {
|
|
1140
|
+
const messages = body === null || body === void 0 ? void 0 : body.messages;
|
|
1141
|
+
if (!Array.isArray(messages)) {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
for (const message of messages) {
|
|
1145
|
+
if ((message === null || message === void 0 ? void 0 : message.role) !== 'user')
|
|
1146
|
+
continue;
|
|
1147
|
+
const content = message === null || message === void 0 ? void 0 : message.content;
|
|
1148
|
+
// 处理字符串类型的 content
|
|
1149
|
+
if (typeof content === 'string') {
|
|
1150
|
+
if (content.trim().startsWith('!!')) {
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
// 处理数组类型的 content
|
|
1155
|
+
else if (Array.isArray(content)) {
|
|
1156
|
+
for (const block of content) {
|
|
1157
|
+
if ((block === null || block === void 0 ? void 0 : block.type) === 'text' && typeof block.text === 'string') {
|
|
1158
|
+
if (block.text.trim().startsWith('!!')) {
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
removeHighIqPrefix(body) {
|
|
1168
|
+
if (!(body === null || body === void 0 ? void 0 : body.messages) || !Array.isArray(body.messages)) {
|
|
1169
|
+
return body;
|
|
1170
|
+
}
|
|
1171
|
+
// 深拷贝 body 以避免修改原始对象
|
|
1172
|
+
const processedBody = JSON.parse(JSON.stringify(body));
|
|
1173
|
+
for (const message of processedBody.messages) {
|
|
1174
|
+
if ((message === null || message === void 0 ? void 0 : message.role) !== 'user')
|
|
1175
|
+
continue;
|
|
1176
|
+
const content = message === null || message === void 0 ? void 0 : message.content;
|
|
1177
|
+
// 处理字符串类型的 content
|
|
1178
|
+
if (typeof content === 'string') {
|
|
1179
|
+
if (content.trim().startsWith('!!')) {
|
|
1180
|
+
// 移除 !! 前缀并执行 trim
|
|
1181
|
+
message.content = content.replace(/^!!\s*/, '').trim();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// 处理数组类型的 content
|
|
1185
|
+
else if (Array.isArray(content)) {
|
|
1186
|
+
for (const block of content) {
|
|
1187
|
+
if ((block === null || block === void 0 ? void 0 : block.type) === 'text' && typeof block.text === 'string') {
|
|
1188
|
+
if (block.text.trim().startsWith('!!')) {
|
|
1189
|
+
// 移除 !! 前缀并执行 trim
|
|
1190
|
+
block.text = block.text.replace(/^!!\s*/, '').trim();
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return processedBody;
|
|
1197
|
+
}
|
|
1053
1198
|
hasBackgroundSignal(body) {
|
|
1054
1199
|
var _a, _b, _c;
|
|
1055
1200
|
// 检测 count tokens 请求:messages 只有一条,role 为 "user",content 为 "count"
|
|
@@ -1171,15 +1316,19 @@ class ProxyServer {
|
|
|
1171
1316
|
}
|
|
1172
1317
|
/** 判断是否为 Claude 相关类型(使用 x-api-key 认证) */
|
|
1173
1318
|
isClaudeSource(sourceType) {
|
|
1174
|
-
return sourceType === 'claude-chat' || sourceType === 'claude
|
|
1319
|
+
return sourceType === 'claude-chat' || sourceType === 'claude';
|
|
1175
1320
|
}
|
|
1176
1321
|
isOpenAIChatSource(sourceType) {
|
|
1177
|
-
return sourceType === 'openai-chat' || sourceType === 'openai
|
|
1322
|
+
return sourceType === 'openai-chat' || sourceType === 'openai' || sourceType === 'deepseek-reasoning-chat';
|
|
1178
1323
|
}
|
|
1179
1324
|
/** 判断是否为 Gemini 类型 */
|
|
1180
1325
|
isGeminiSource(sourceType) {
|
|
1181
1326
|
return sourceType === 'gemini';
|
|
1182
1327
|
}
|
|
1328
|
+
/** 判断是否为 Gemini Chat 类型 */
|
|
1329
|
+
isGeminiChatSource(sourceType) {
|
|
1330
|
+
return sourceType === 'gemini-chat';
|
|
1331
|
+
}
|
|
1183
1332
|
isChatType(sourceType) {
|
|
1184
1333
|
return sourceType.endsWith('-chat') || sourceType === 'gemini';
|
|
1185
1334
|
}
|
|
@@ -1301,8 +1450,8 @@ class ProxyServer {
|
|
|
1301
1450
|
// 向下兼容:检测旧数据的 'auto' 值
|
|
1302
1451
|
// TODO: 删除
|
|
1303
1452
|
const isAuto = authType === 'auto';
|
|
1304
|
-
// 使用 x-goog-api-key 认证(适用于 Google Gemini API)
|
|
1305
|
-
if (authType === types_1.AuthType.G_API_KEY || (isAuto && this.isGeminiSource(sourceType))) {
|
|
1453
|
+
// 使用 x-goog-api-key 认证(适用于 Google Gemini API 和 Gemini Chat)
|
|
1454
|
+
if (authType === types_1.AuthType.G_API_KEY || (isAuto && (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType)))) {
|
|
1306
1455
|
headers['x-goog-api-key'] = service.apiKey;
|
|
1307
1456
|
}
|
|
1308
1457
|
// 使用 x-api-key 认证(适用于 claude-chat, claude-code 及某些需要 x-api-key 的 openai-chat 兼容 API)
|
|
@@ -1559,13 +1708,15 @@ class ProxyServer {
|
|
|
1559
1708
|
// 默认:直接返回原始路径
|
|
1560
1709
|
return originalPath;
|
|
1561
1710
|
}
|
|
1562
|
-
proxyRequest(req, res, route, rule, service) {
|
|
1711
|
+
proxyRequest(req, res, route, rule, service, options) {
|
|
1563
1712
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1564
|
-
var _a, _b, _c, _d, _e
|
|
1713
|
+
var _a, _b, _c, _d, _e;
|
|
1565
1714
|
res.locals.skipLog = true;
|
|
1566
1715
|
const startTime = Date.now();
|
|
1567
1716
|
const sourceType = (service.sourceType || 'openai-chat');
|
|
1568
1717
|
const targetType = route.targetType;
|
|
1718
|
+
const failoverEnabled = (options === null || options === void 0 ? void 0 : options.failoverEnabled) === true;
|
|
1719
|
+
const forwardedToServiceName = options === null || options === void 0 ? void 0 : options.forwardedToServiceName;
|
|
1569
1720
|
let requestBody = req.body || {};
|
|
1570
1721
|
let usageForLog;
|
|
1571
1722
|
let logged = false;
|
|
@@ -1686,6 +1837,11 @@ class ProxyServer {
|
|
|
1686
1837
|
useMCPProcessing = false;
|
|
1687
1838
|
}
|
|
1688
1839
|
}
|
|
1840
|
+
// 高智商请求处理:移除 !! 前缀
|
|
1841
|
+
if (rule.contentType === 'high-iq' && requestBody.messages) {
|
|
1842
|
+
requestBody = this.removeHighIqPrefix(requestBody);
|
|
1843
|
+
console.log('[HIGH-IQ] Removed !! prefix from user messages');
|
|
1844
|
+
}
|
|
1689
1845
|
// 用于收集响应数据的变量
|
|
1690
1846
|
let responseHeadersForLog;
|
|
1691
1847
|
let responseBodyForLog;
|
|
@@ -1801,6 +1957,62 @@ class ProxyServer {
|
|
|
1801
1957
|
console.log(`[MCP] Cleaned up ${tempImageFiles.length} temporary image files`);
|
|
1802
1958
|
}
|
|
1803
1959
|
});
|
|
1960
|
+
const handleUpstreamHttpError = (statusCode, responseData, responseHeaders, contentType) => __awaiter(this, void 0, void 0, function* () {
|
|
1961
|
+
var _a, _b;
|
|
1962
|
+
usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
|
|
1963
|
+
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
1964
|
+
let errorDetail;
|
|
1965
|
+
if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.error) === 'string') {
|
|
1966
|
+
errorDetail = responseData.error;
|
|
1967
|
+
}
|
|
1968
|
+
else if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.message) === 'string') {
|
|
1969
|
+
errorDetail = responseData.message;
|
|
1970
|
+
}
|
|
1971
|
+
else if (responseData === null || responseData === void 0 ? void 0 : responseData.error) {
|
|
1972
|
+
errorDetail = JSON.stringify(responseData.error);
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
errorDetail = JSON.stringify(responseData);
|
|
1976
|
+
}
|
|
1977
|
+
const failoverHint = failoverEnabled ? this.buildFailoverHint(forwardedToServiceName) : '';
|
|
1978
|
+
const upstreamErrorMessage = `Upstream API returned ${statusCode}: ${errorDetail}${failoverHint}`;
|
|
1979
|
+
const vendors = this.dbManager.getVendors();
|
|
1980
|
+
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
1981
|
+
yield this.dbManager.addErrorLog({
|
|
1982
|
+
timestamp: Date.now(),
|
|
1983
|
+
method: req.method,
|
|
1984
|
+
path: req.path,
|
|
1985
|
+
statusCode,
|
|
1986
|
+
errorMessage: upstreamErrorMessage,
|
|
1987
|
+
errorStack: undefined,
|
|
1988
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
1989
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
1990
|
+
responseHeaders: responseHeadersForLog,
|
|
1991
|
+
responseBody: responseBodyForLog,
|
|
1992
|
+
ruleId: rule.id,
|
|
1993
|
+
targetType,
|
|
1994
|
+
targetServiceId: service.id,
|
|
1995
|
+
targetServiceName: service.name,
|
|
1996
|
+
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
1997
|
+
vendorId: service.vendorId,
|
|
1998
|
+
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
1999
|
+
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
2000
|
+
upstreamRequest: upstreamRequestForLog,
|
|
2001
|
+
responseTime: Date.now() - startTime,
|
|
2002
|
+
});
|
|
2003
|
+
if (failoverEnabled) {
|
|
2004
|
+
yield finalizeLog(statusCode, upstreamErrorMessage);
|
|
2005
|
+
throw this.createFailoverError(upstreamErrorMessage, statusCode);
|
|
2006
|
+
}
|
|
2007
|
+
this.copyResponseHeaders(responseHeaders, res);
|
|
2008
|
+
if (contentType.includes('application/json')) {
|
|
2009
|
+
res.status(statusCode).json(responseData);
|
|
2010
|
+
}
|
|
2011
|
+
else {
|
|
2012
|
+
res.status(statusCode).send(responseData);
|
|
2013
|
+
}
|
|
2014
|
+
yield finalizeLog(res.statusCode);
|
|
2015
|
+
});
|
|
1804
2016
|
try {
|
|
1805
2017
|
if (targetType === 'claude-code') {
|
|
1806
2018
|
if (this.isClaudeSource(sourceType)) {
|
|
@@ -1809,7 +2021,7 @@ class ProxyServer {
|
|
|
1809
2021
|
else if (this.isOpenAIChatSource(sourceType)) {
|
|
1810
2022
|
requestBody = (0, claude_openai_1.transformClaudeRequestToOpenAIChat)(requestBody, rule.targetModel);
|
|
1811
2023
|
}
|
|
1812
|
-
else if (this.isGeminiSource(sourceType)) {
|
|
2024
|
+
else if (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType)) {
|
|
1813
2025
|
requestBody = (0, gemini_1.transformClaudeRequestToGemini)(requestBody);
|
|
1814
2026
|
}
|
|
1815
2027
|
else {
|
|
@@ -1825,7 +2037,7 @@ class ProxyServer {
|
|
|
1825
2037
|
else if (this.isClaudeSource(sourceType)) {
|
|
1826
2038
|
requestBody = (0, claude_openai_1.transformClaudeRequestToOpenAIChat)(requestBody, rule.targetModel);
|
|
1827
2039
|
}
|
|
1828
|
-
else if (this.isGeminiSource(sourceType)) {
|
|
2040
|
+
else if (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType)) {
|
|
1829
2041
|
requestBody = (0, gemini_1.transformOpenAIChatRequestToGemini)(requestBody);
|
|
1830
2042
|
}
|
|
1831
2043
|
else {
|
|
@@ -1854,7 +2066,8 @@ class ProxyServer {
|
|
|
1854
2066
|
const model = requestBody.model || rule.targetModel || 'gemini-pro';
|
|
1855
2067
|
upstreamUrl = this.buildGeminiUrl(service.apiUrl, model, streamRequested);
|
|
1856
2068
|
}
|
|
1857
|
-
else if (this.isChatType(sourceType)) {
|
|
2069
|
+
else if (this.isChatType(sourceType) || this.isGeminiChatSource(sourceType)) {
|
|
2070
|
+
// Chat 类型(包括 gemini-chat)直接使用用户配置的完整 URL
|
|
1858
2071
|
upstreamUrl = service.apiUrl;
|
|
1859
2072
|
}
|
|
1860
2073
|
else {
|
|
@@ -1922,6 +2135,17 @@ class ProxyServer {
|
|
|
1922
2135
|
const responseHeaders = response.headers || {};
|
|
1923
2136
|
const contentType = typeof responseHeaders['content-type'] === 'string' ? responseHeaders['content-type'] : '';
|
|
1924
2137
|
const isEventStream = streamRequested && contentType.includes('text/event-stream');
|
|
2138
|
+
// 先处理 4xx/5xx:在故障切换模式下抛错,由上层继续切换下一服务
|
|
2139
|
+
if (response.status >= 400) {
|
|
2140
|
+
let errorResponseData = response.data;
|
|
2141
|
+
if (streamRequested && response.data && typeof response.data.on === 'function') {
|
|
2142
|
+
const raw = yield this.readStreamBody(response.data);
|
|
2143
|
+
errorResponseData = (_a = this.safeJsonParse(raw)) !== null && _a !== void 0 ? _a : raw;
|
|
2144
|
+
}
|
|
2145
|
+
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
2146
|
+
yield handleUpstreamHttpError(response.status, errorResponseData, responseHeaders, contentType);
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
1925
2149
|
if (isEventStream && response.data) {
|
|
1926
2150
|
res.status(response.status);
|
|
1927
2151
|
if (targetType === 'claude-code' && this.isOpenAIChatSource(sourceType)) {
|
|
@@ -2126,8 +2350,8 @@ class ProxyServer {
|
|
|
2126
2350
|
}));
|
|
2127
2351
|
return;
|
|
2128
2352
|
}
|
|
2129
|
-
// Gemini -> Claude Code 流式转换
|
|
2130
|
-
if (targetType === 'claude-code' && this.isGeminiSource(sourceType)) {
|
|
2353
|
+
// Gemini / Gemini Chat -> Claude Code 流式转换
|
|
2354
|
+
if (targetType === 'claude-code' && (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType))) {
|
|
2131
2355
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
2132
2356
|
res.setHeader('Cache-Control', 'no-cache');
|
|
2133
2357
|
res.setHeader('Connection', 'keep-alive');
|
|
@@ -2222,8 +2446,8 @@ class ProxyServer {
|
|
|
2222
2446
|
}));
|
|
2223
2447
|
return;
|
|
2224
2448
|
}
|
|
2225
|
-
// Gemini -> Codex 流式转换
|
|
2226
|
-
if (targetType === 'codex' && this.isGeminiSource(sourceType)) {
|
|
2449
|
+
// Gemini / Gemini Chat -> Codex 流式转换
|
|
2450
|
+
if (targetType === 'codex' && (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType))) {
|
|
2227
2451
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
2228
2452
|
res.setHeader('Cache-Control', 'no-cache');
|
|
2229
2453
|
res.setHeader('Connection', 'keep-alive');
|
|
@@ -2379,65 +2603,10 @@ class ProxyServer {
|
|
|
2379
2603
|
let responseData = response.data;
|
|
2380
2604
|
if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
|
|
2381
2605
|
const raw = yield this.readStreamBody(response.data);
|
|
2382
|
-
responseData = (
|
|
2606
|
+
responseData = (_b = this.safeJsonParse(raw)) !== null && _b !== void 0 ? _b : raw;
|
|
2383
2607
|
}
|
|
2384
2608
|
// 收集响应头
|
|
2385
2609
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
2386
|
-
if (response.status >= 400) {
|
|
2387
|
-
usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
|
|
2388
|
-
// 记录错误响应体
|
|
2389
|
-
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
2390
|
-
// 将 4xx/5xx 错误记录到错误日志
|
|
2391
|
-
// 确保 errorDetail 总是字符串类型
|
|
2392
|
-
let errorDetail;
|
|
2393
|
-
if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.error) === 'string') {
|
|
2394
|
-
errorDetail = responseData.error;
|
|
2395
|
-
}
|
|
2396
|
-
else if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.message) === 'string') {
|
|
2397
|
-
errorDetail = responseData.message;
|
|
2398
|
-
}
|
|
2399
|
-
else if (responseData === null || responseData === void 0 ? void 0 : responseData.error) {
|
|
2400
|
-
errorDetail = JSON.stringify(responseData.error);
|
|
2401
|
-
}
|
|
2402
|
-
else {
|
|
2403
|
-
errorDetail = JSON.stringify(responseData);
|
|
2404
|
-
}
|
|
2405
|
-
// 获取供应商信息
|
|
2406
|
-
const vendors = this.dbManager.getVendors();
|
|
2407
|
-
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
2408
|
-
yield this.dbManager.addErrorLog({
|
|
2409
|
-
timestamp: Date.now(),
|
|
2410
|
-
method: req.method,
|
|
2411
|
-
path: req.path,
|
|
2412
|
-
statusCode: response.status,
|
|
2413
|
-
errorMessage: `Upstream API returned ${response.status}: ${errorDetail}`,
|
|
2414
|
-
errorStack: undefined,
|
|
2415
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
2416
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
2417
|
-
responseHeaders: responseHeadersForLog,
|
|
2418
|
-
responseBody: responseBodyForLog,
|
|
2419
|
-
// 添加请求详情和实际转发信息
|
|
2420
|
-
ruleId: rule.id,
|
|
2421
|
-
targetType,
|
|
2422
|
-
targetServiceId: service.id,
|
|
2423
|
-
targetServiceName: service.name,
|
|
2424
|
-
targetModel: rule.targetModel || ((_b = req.body) === null || _b === void 0 ? void 0 : _b.model),
|
|
2425
|
-
vendorId: service.vendorId,
|
|
2426
|
-
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
2427
|
-
requestModel: (_c = req.body) === null || _c === void 0 ? void 0 : _c.model,
|
|
2428
|
-
upstreamRequest: upstreamRequestForLog,
|
|
2429
|
-
responseTime: Date.now() - startTime,
|
|
2430
|
-
});
|
|
2431
|
-
this.copyResponseHeaders(responseHeaders, res);
|
|
2432
|
-
if (contentType.includes('application/json')) {
|
|
2433
|
-
res.status(response.status).json(responseData);
|
|
2434
|
-
}
|
|
2435
|
-
else {
|
|
2436
|
-
res.status(response.status).send(responseData);
|
|
2437
|
-
}
|
|
2438
|
-
yield finalizeLog(res.statusCode);
|
|
2439
|
-
return;
|
|
2440
|
-
}
|
|
2441
2610
|
if (targetType === 'claude-code' && this.isOpenAIChatSource(sourceType)) {
|
|
2442
2611
|
const converted = (0, claude_openai_1.transformOpenAIChatResponseToClaude)(responseData);
|
|
2443
2612
|
usageForLog = (0, claude_openai_1.extractTokenUsageFromOpenAIUsage)(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
|
|
@@ -2445,7 +2614,7 @@ class ProxyServer {
|
|
|
2445
2614
|
responseBodyForLog = JSON.stringify(converted);
|
|
2446
2615
|
res.status(response.status).json(converted);
|
|
2447
2616
|
}
|
|
2448
|
-
else if (targetType === 'claude-code' && this.isGeminiSource(sourceType)) {
|
|
2617
|
+
else if (targetType === 'claude-code' && (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType))) {
|
|
2449
2618
|
const converted = (0, gemini_1.transformGeminiResponseToClaude)(responseData, rule.targetModel);
|
|
2450
2619
|
usageForLog = (0, gemini_1.extractTokenUsageFromGeminiUsage)(responseData === null || responseData === void 0 ? void 0 : responseData.usageMetadata);
|
|
2451
2620
|
responseBodyForLog = JSON.stringify(converted);
|
|
@@ -2457,7 +2626,7 @@ class ProxyServer {
|
|
|
2457
2626
|
responseBodyForLog = JSON.stringify(converted);
|
|
2458
2627
|
res.status(response.status).json(converted);
|
|
2459
2628
|
}
|
|
2460
|
-
else if (targetType === 'codex' && this.isGeminiSource(sourceType)) {
|
|
2629
|
+
else if (targetType === 'codex' && (this.isGeminiSource(sourceType) || this.isGeminiChatSource(sourceType))) {
|
|
2461
2630
|
const converted = (0, gemini_1.transformGeminiResponseToOpenAIChat)(responseData, rule.targetModel);
|
|
2462
2631
|
usageForLog = (0, gemini_1.extractTokenUsageFromGeminiUsage)(responseData === null || responseData === void 0 ? void 0 : responseData.usageMetadata);
|
|
2463
2632
|
responseBodyForLog = JSON.stringify(converted);
|
|
@@ -2479,14 +2648,19 @@ class ProxyServer {
|
|
|
2479
2648
|
yield finalizeLog(res.statusCode);
|
|
2480
2649
|
}
|
|
2481
2650
|
catch (error) {
|
|
2651
|
+
if (failoverEnabled && (error === null || error === void 0 ? void 0 : error.isFailoverCandidate)) {
|
|
2652
|
+
throw error;
|
|
2653
|
+
}
|
|
2482
2654
|
console.error('Proxy error:', error);
|
|
2483
2655
|
// 检测是否是 timeout 错误
|
|
2484
2656
|
const isTimeout = error.code === 'ECONNABORTED' ||
|
|
2485
|
-
((
|
|
2657
|
+
((_c = error.message) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes('timeout')) ||
|
|
2486
2658
|
(error.errno && error.errno === 'ETIMEDOUT');
|
|
2487
|
-
const
|
|
2659
|
+
const baseErrorMessage = isTimeout
|
|
2488
2660
|
? 'Request timeout - the upstream API took too long to respond'
|
|
2489
2661
|
: (error.message || 'Internal server error');
|
|
2662
|
+
const failoverHint = failoverEnabled ? this.buildFailoverHint(forwardedToServiceName) : '';
|
|
2663
|
+
const errorMessage = `${baseErrorMessage}${failoverHint}`;
|
|
2490
2664
|
// 将错误记录到错误日志 - 包含请求详情和实际转发信息
|
|
2491
2665
|
// 获取供应商信息
|
|
2492
2666
|
const vendors = this.dbManager.getVendors();
|
|
@@ -2505,14 +2679,17 @@ class ProxyServer {
|
|
|
2505
2679
|
targetType,
|
|
2506
2680
|
targetServiceId: service.id,
|
|
2507
2681
|
targetServiceName: service.name,
|
|
2508
|
-
targetModel: rule.targetModel || ((
|
|
2682
|
+
targetModel: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
|
|
2509
2683
|
vendorId: service.vendorId,
|
|
2510
2684
|
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
2511
|
-
requestModel: (
|
|
2685
|
+
requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
|
|
2512
2686
|
upstreamRequest: upstreamRequestForLog,
|
|
2513
2687
|
responseTime: Date.now() - startTime,
|
|
2514
2688
|
});
|
|
2515
2689
|
yield finalizeLog(isTimeout ? 504 : 500, errorMessage);
|
|
2690
|
+
if (failoverEnabled) {
|
|
2691
|
+
throw this.createFailoverError(errorMessage, isTimeout ? 504 : 500, error);
|
|
2692
|
+
}
|
|
2516
2693
|
// 根据请求类型返回适当格式的错误响应
|
|
2517
2694
|
const streamRequested = this.isStreamRequested(req, req.body || {});
|
|
2518
2695
|
if (route.targetType === 'claude-code') {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.migrateSourceType = migrateSourceType;
|
|
4
|
+
exports.downgradeSourceType = downgradeSourceType;
|
|
5
|
+
exports.isLegacySourceType = isLegacySourceType;
|
|
6
|
+
exports.normalizeSourceType = normalizeSourceType;
|
|
7
|
+
/**
|
|
8
|
+
* 旧类型 → 新类型映射表
|
|
9
|
+
*/
|
|
10
|
+
const SOURCE_TYPE_MIGRATION_MAP = {
|
|
11
|
+
'openai-chat': 'openai-chat',
|
|
12
|
+
'openai-responses': 'openai', // 重命名
|
|
13
|
+
'claude-chat': 'claude-chat',
|
|
14
|
+
'claude-code': 'claude', // 重命名
|
|
15
|
+
'deepseek-reasoning-chat': 'deepseek-reasoning-chat',
|
|
16
|
+
'gemini': 'gemini',
|
|
17
|
+
'gemini-chat': 'gemini-chat',
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* 新类型 → 旧类型映射表
|
|
21
|
+
* 用于向下兼容导出
|
|
22
|
+
*/
|
|
23
|
+
const SOURCE_TYPE_REVERSE_MAP = {
|
|
24
|
+
'openai-chat': 'openai-chat',
|
|
25
|
+
'openai': 'openai-responses',
|
|
26
|
+
'claude-chat': 'claude-chat',
|
|
27
|
+
'claude': 'claude-code',
|
|
28
|
+
'deepseek-reasoning-chat': 'deepseek-reasoning-chat',
|
|
29
|
+
'gemini': 'gemini',
|
|
30
|
+
'gemini-chat': 'gemini-chat',
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* 将旧类型转换为新类型
|
|
34
|
+
* @param legacyType 旧的数据源类型
|
|
35
|
+
* @returns 新的数据源类型
|
|
36
|
+
*/
|
|
37
|
+
function migrateSourceType(legacyType) {
|
|
38
|
+
return SOURCE_TYPE_MIGRATION_MAP[legacyType];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 将新类型转换为旧类型
|
|
42
|
+
* 用于向下兼容导出
|
|
43
|
+
* @param newType 新的数据源类型
|
|
44
|
+
* @returns 旧的数据源类型
|
|
45
|
+
*/
|
|
46
|
+
function downgradeSourceType(newType) {
|
|
47
|
+
return SOURCE_TYPE_REVERSE_MAP[newType];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 检查是否为旧类型
|
|
51
|
+
* @param type 类型字符串
|
|
52
|
+
* @returns 是否为旧类型
|
|
53
|
+
*/
|
|
54
|
+
function isLegacySourceType(type) {
|
|
55
|
+
return type === 'openai-responses' || type === 'claude-code';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 标准化类型
|
|
59
|
+
* 自动处理新旧类型,将旧类型转换为新类型,新类型保持不变
|
|
60
|
+
* @param type 类型字符串(可能是旧类型或新类型)
|
|
61
|
+
* @returns 标准化后的新类型
|
|
62
|
+
*/
|
|
63
|
+
function normalizeSourceType(type) {
|
|
64
|
+
if (isLegacySourceType(type)) {
|
|
65
|
+
return migrateSourceType(type);
|
|
66
|
+
}
|
|
67
|
+
return type;
|
|
68
|
+
}
|