aicodeswitch 3.0.19 → 3.0.20
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/dist/server/proxy-server.js +128 -66
- package/package.json +1 -1
|
@@ -158,7 +158,8 @@ class ProxyServer {
|
|
|
158
158
|
}
|
|
159
159
|
// 尝试每个规则,直到成功或全部失败
|
|
160
160
|
let lastError = null;
|
|
161
|
-
for (
|
|
161
|
+
for (let index = 0; index < allRules.length; index++) {
|
|
162
|
+
const rule = allRules[index];
|
|
162
163
|
const service = this.getServiceById(rule.targetServiceId);
|
|
163
164
|
if (!service)
|
|
164
165
|
continue;
|
|
@@ -169,8 +170,12 @@ class ProxyServer {
|
|
|
169
170
|
continue;
|
|
170
171
|
}
|
|
171
172
|
try {
|
|
173
|
+
const nextServiceName = yield this.findNextAvailableServiceName(allRules, index + 1, route.id);
|
|
172
174
|
// 尝试代理请求
|
|
173
|
-
yield this.proxyRequest(req, res, route, rule, service
|
|
175
|
+
yield this.proxyRequest(req, res, route, rule, service, {
|
|
176
|
+
failoverEnabled: true,
|
|
177
|
+
forwardedToServiceName: nextServiceName,
|
|
178
|
+
});
|
|
174
179
|
return; // 成功,直接返回
|
|
175
180
|
}
|
|
176
181
|
catch (error) {
|
|
@@ -339,7 +344,8 @@ class ProxyServer {
|
|
|
339
344
|
}
|
|
340
345
|
// 尝试每个规则,直到成功或全部失败
|
|
341
346
|
let lastError = null;
|
|
342
|
-
for (
|
|
347
|
+
for (let index = 0; index < allRules.length; index++) {
|
|
348
|
+
const rule = allRules[index];
|
|
343
349
|
const service = this.getServiceById(rule.targetServiceId);
|
|
344
350
|
if (!service)
|
|
345
351
|
continue;
|
|
@@ -350,8 +356,12 @@ class ProxyServer {
|
|
|
350
356
|
continue;
|
|
351
357
|
}
|
|
352
358
|
try {
|
|
359
|
+
const nextServiceName = yield this.findNextAvailableServiceName(allRules, index + 1, route.id);
|
|
353
360
|
// 尝试代理请求
|
|
354
|
-
yield this.proxyRequest(req, res, route, rule, service
|
|
361
|
+
yield this.proxyRequest(req, res, route, rule, service, {
|
|
362
|
+
failoverEnabled: true,
|
|
363
|
+
forwardedToServiceName: nextServiceName,
|
|
364
|
+
});
|
|
355
365
|
return; // 成功,直接返回
|
|
356
366
|
}
|
|
357
367
|
catch (error) {
|
|
@@ -497,6 +507,36 @@ class ProxyServer {
|
|
|
497
507
|
const activeRoutes = this.getActiveRoutes();
|
|
498
508
|
return activeRoutes.find(route => route.targetType === targetType && route.isActive);
|
|
499
509
|
}
|
|
510
|
+
findNextAvailableServiceName(allRules, startIndex, routeId) {
|
|
511
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
512
|
+
for (let index = startIndex; index < allRules.length; index++) {
|
|
513
|
+
const rule = allRules[index];
|
|
514
|
+
const service = this.getServiceById(rule.targetServiceId);
|
|
515
|
+
if (!service)
|
|
516
|
+
continue;
|
|
517
|
+
const isBlacklisted = yield this.dbManager.isServiceBlacklisted(service.id, routeId, rule.contentType);
|
|
518
|
+
if (isBlacklisted)
|
|
519
|
+
continue;
|
|
520
|
+
return service.name;
|
|
521
|
+
}
|
|
522
|
+
return undefined;
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
buildFailoverHint(forwardedToServiceName) {
|
|
526
|
+
if (!forwardedToServiceName) {
|
|
527
|
+
return '';
|
|
528
|
+
}
|
|
529
|
+
return `;已自动转发给 ${forwardedToServiceName} 服务继续处理`;
|
|
530
|
+
}
|
|
531
|
+
createFailoverError(message, statusCode, originalError) {
|
|
532
|
+
const failoverError = new Error(message);
|
|
533
|
+
failoverError.isFailoverCandidate = true;
|
|
534
|
+
failoverError.response = { status: statusCode };
|
|
535
|
+
if (originalError === null || originalError === void 0 ? void 0 : originalError.stack) {
|
|
536
|
+
failoverError.stack = originalError.stack;
|
|
537
|
+
}
|
|
538
|
+
return failoverError;
|
|
539
|
+
}
|
|
500
540
|
/**
|
|
501
541
|
* 计算请求内容的哈希值,用于去重
|
|
502
542
|
* 基于请求的关键字段生成唯一标识
|
|
@@ -1559,13 +1599,15 @@ class ProxyServer {
|
|
|
1559
1599
|
// 默认:直接返回原始路径
|
|
1560
1600
|
return originalPath;
|
|
1561
1601
|
}
|
|
1562
|
-
proxyRequest(req, res, route, rule, service) {
|
|
1602
|
+
proxyRequest(req, res, route, rule, service, options) {
|
|
1563
1603
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1564
|
-
var _a, _b, _c, _d, _e
|
|
1604
|
+
var _a, _b, _c, _d, _e;
|
|
1565
1605
|
res.locals.skipLog = true;
|
|
1566
1606
|
const startTime = Date.now();
|
|
1567
1607
|
const sourceType = (service.sourceType || 'openai-chat');
|
|
1568
1608
|
const targetType = route.targetType;
|
|
1609
|
+
const failoverEnabled = (options === null || options === void 0 ? void 0 : options.failoverEnabled) === true;
|
|
1610
|
+
const forwardedToServiceName = options === null || options === void 0 ? void 0 : options.forwardedToServiceName;
|
|
1569
1611
|
let requestBody = req.body || {};
|
|
1570
1612
|
let usageForLog;
|
|
1571
1613
|
let logged = false;
|
|
@@ -1801,6 +1843,62 @@ class ProxyServer {
|
|
|
1801
1843
|
console.log(`[MCP] Cleaned up ${tempImageFiles.length} temporary image files`);
|
|
1802
1844
|
}
|
|
1803
1845
|
});
|
|
1846
|
+
const handleUpstreamHttpError = (statusCode, responseData, responseHeaders, contentType) => __awaiter(this, void 0, void 0, function* () {
|
|
1847
|
+
var _a, _b;
|
|
1848
|
+
usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
|
|
1849
|
+
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
1850
|
+
let errorDetail;
|
|
1851
|
+
if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.error) === 'string') {
|
|
1852
|
+
errorDetail = responseData.error;
|
|
1853
|
+
}
|
|
1854
|
+
else if (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.message) === 'string') {
|
|
1855
|
+
errorDetail = responseData.message;
|
|
1856
|
+
}
|
|
1857
|
+
else if (responseData === null || responseData === void 0 ? void 0 : responseData.error) {
|
|
1858
|
+
errorDetail = JSON.stringify(responseData.error);
|
|
1859
|
+
}
|
|
1860
|
+
else {
|
|
1861
|
+
errorDetail = JSON.stringify(responseData);
|
|
1862
|
+
}
|
|
1863
|
+
const failoverHint = failoverEnabled ? this.buildFailoverHint(forwardedToServiceName) : '';
|
|
1864
|
+
const upstreamErrorMessage = `Upstream API returned ${statusCode}: ${errorDetail}${failoverHint}`;
|
|
1865
|
+
const vendors = this.dbManager.getVendors();
|
|
1866
|
+
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
1867
|
+
yield this.dbManager.addErrorLog({
|
|
1868
|
+
timestamp: Date.now(),
|
|
1869
|
+
method: req.method,
|
|
1870
|
+
path: req.path,
|
|
1871
|
+
statusCode,
|
|
1872
|
+
errorMessage: upstreamErrorMessage,
|
|
1873
|
+
errorStack: undefined,
|
|
1874
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
1875
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
1876
|
+
responseHeaders: responseHeadersForLog,
|
|
1877
|
+
responseBody: responseBodyForLog,
|
|
1878
|
+
ruleId: rule.id,
|
|
1879
|
+
targetType,
|
|
1880
|
+
targetServiceId: service.id,
|
|
1881
|
+
targetServiceName: service.name,
|
|
1882
|
+
targetModel: rule.targetModel || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.model),
|
|
1883
|
+
vendorId: service.vendorId,
|
|
1884
|
+
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
1885
|
+
requestModel: (_b = req.body) === null || _b === void 0 ? void 0 : _b.model,
|
|
1886
|
+
upstreamRequest: upstreamRequestForLog,
|
|
1887
|
+
responseTime: Date.now() - startTime,
|
|
1888
|
+
});
|
|
1889
|
+
if (failoverEnabled) {
|
|
1890
|
+
yield finalizeLog(statusCode, upstreamErrorMessage);
|
|
1891
|
+
throw this.createFailoverError(upstreamErrorMessage, statusCode);
|
|
1892
|
+
}
|
|
1893
|
+
this.copyResponseHeaders(responseHeaders, res);
|
|
1894
|
+
if (contentType.includes('application/json')) {
|
|
1895
|
+
res.status(statusCode).json(responseData);
|
|
1896
|
+
}
|
|
1897
|
+
else {
|
|
1898
|
+
res.status(statusCode).send(responseData);
|
|
1899
|
+
}
|
|
1900
|
+
yield finalizeLog(res.statusCode);
|
|
1901
|
+
});
|
|
1804
1902
|
try {
|
|
1805
1903
|
if (targetType === 'claude-code') {
|
|
1806
1904
|
if (this.isClaudeSource(sourceType)) {
|
|
@@ -1922,6 +2020,17 @@ class ProxyServer {
|
|
|
1922
2020
|
const responseHeaders = response.headers || {};
|
|
1923
2021
|
const contentType = typeof responseHeaders['content-type'] === 'string' ? responseHeaders['content-type'] : '';
|
|
1924
2022
|
const isEventStream = streamRequested && contentType.includes('text/event-stream');
|
|
2023
|
+
// 先处理 4xx/5xx:在故障切换模式下抛错,由上层继续切换下一服务
|
|
2024
|
+
if (response.status >= 400) {
|
|
2025
|
+
let errorResponseData = response.data;
|
|
2026
|
+
if (streamRequested && response.data && typeof response.data.on === 'function') {
|
|
2027
|
+
const raw = yield this.readStreamBody(response.data);
|
|
2028
|
+
errorResponseData = (_a = this.safeJsonParse(raw)) !== null && _a !== void 0 ? _a : raw;
|
|
2029
|
+
}
|
|
2030
|
+
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
2031
|
+
yield handleUpstreamHttpError(response.status, errorResponseData, responseHeaders, contentType);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
1925
2034
|
if (isEventStream && response.data) {
|
|
1926
2035
|
res.status(response.status);
|
|
1927
2036
|
if (targetType === 'claude-code' && this.isOpenAIChatSource(sourceType)) {
|
|
@@ -2379,65 +2488,10 @@ class ProxyServer {
|
|
|
2379
2488
|
let responseData = response.data;
|
|
2380
2489
|
if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
|
|
2381
2490
|
const raw = yield this.readStreamBody(response.data);
|
|
2382
|
-
responseData = (
|
|
2491
|
+
responseData = (_b = this.safeJsonParse(raw)) !== null && _b !== void 0 ? _b : raw;
|
|
2383
2492
|
}
|
|
2384
2493
|
// 收集响应头
|
|
2385
2494
|
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
2495
|
if (targetType === 'claude-code' && this.isOpenAIChatSource(sourceType)) {
|
|
2442
2496
|
const converted = (0, claude_openai_1.transformOpenAIChatResponseToClaude)(responseData);
|
|
2443
2497
|
usageForLog = (0, claude_openai_1.extractTokenUsageFromOpenAIUsage)(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
|
|
@@ -2479,14 +2533,19 @@ class ProxyServer {
|
|
|
2479
2533
|
yield finalizeLog(res.statusCode);
|
|
2480
2534
|
}
|
|
2481
2535
|
catch (error) {
|
|
2536
|
+
if (failoverEnabled && (error === null || error === void 0 ? void 0 : error.isFailoverCandidate)) {
|
|
2537
|
+
throw error;
|
|
2538
|
+
}
|
|
2482
2539
|
console.error('Proxy error:', error);
|
|
2483
2540
|
// 检测是否是 timeout 错误
|
|
2484
2541
|
const isTimeout = error.code === 'ECONNABORTED' ||
|
|
2485
|
-
((
|
|
2542
|
+
((_c = error.message) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes('timeout')) ||
|
|
2486
2543
|
(error.errno && error.errno === 'ETIMEDOUT');
|
|
2487
|
-
const
|
|
2544
|
+
const baseErrorMessage = isTimeout
|
|
2488
2545
|
? 'Request timeout - the upstream API took too long to respond'
|
|
2489
2546
|
: (error.message || 'Internal server error');
|
|
2547
|
+
const failoverHint = failoverEnabled ? this.buildFailoverHint(forwardedToServiceName) : '';
|
|
2548
|
+
const errorMessage = `${baseErrorMessage}${failoverHint}`;
|
|
2490
2549
|
// 将错误记录到错误日志 - 包含请求详情和实际转发信息
|
|
2491
2550
|
// 获取供应商信息
|
|
2492
2551
|
const vendors = this.dbManager.getVendors();
|
|
@@ -2505,14 +2564,17 @@ class ProxyServer {
|
|
|
2505
2564
|
targetType,
|
|
2506
2565
|
targetServiceId: service.id,
|
|
2507
2566
|
targetServiceName: service.name,
|
|
2508
|
-
targetModel: rule.targetModel || ((
|
|
2567
|
+
targetModel: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
|
|
2509
2568
|
vendorId: service.vendorId,
|
|
2510
2569
|
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
2511
|
-
requestModel: (
|
|
2570
|
+
requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
|
|
2512
2571
|
upstreamRequest: upstreamRequestForLog,
|
|
2513
2572
|
responseTime: Date.now() - startTime,
|
|
2514
2573
|
});
|
|
2515
2574
|
yield finalizeLog(isTimeout ? 504 : 500, errorMessage);
|
|
2575
|
+
if (failoverEnabled) {
|
|
2576
|
+
throw this.createFailoverError(errorMessage, isTimeout ? 504 : 500, error);
|
|
2577
|
+
}
|
|
2516
2578
|
// 根据请求类型返回适当格式的错误响应
|
|
2517
2579
|
const streamRequested = this.isStreamRequested(req, req.body || {});
|
|
2518
2580
|
if (route.targetType === 'claude-code') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aicodeswitch",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.20",
|
|
4
4
|
"description": "A tool to help you manage AI programming tools to access large language models locally. It allows your Claude Code, Codex and other tools to no longer be limited to official models.",
|
|
5
5
|
"author": "tangshuang",
|
|
6
6
|
"license": "GPL-3.0",
|