aicodeswitch 4.0.2 → 4.0.4
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 -1
- package/dist/server/fs-database.js +30 -2
- package/dist/server/proxy-server.js +318 -108
- package/dist/ui/assets/index-Dl-B9pXM.js +514 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-BRHavnvn.js +0 -514
|
@@ -195,9 +195,8 @@ class ProxyServer {
|
|
|
195
195
|
// 如果原始配置也不可用,返回错误
|
|
196
196
|
return res.status(404).json({ error: 'No matching route found and no original config available' });
|
|
197
197
|
}
|
|
198
|
-
//
|
|
198
|
+
// 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
|
|
199
199
|
const forcedContentType = yield this.prepareHighIqRouting(req, route, route.targetType);
|
|
200
|
-
// 检查是否启用故障切换
|
|
201
200
|
const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
|
|
202
201
|
if (!enableFailover) {
|
|
203
202
|
// 故障切换已禁用,使用传统的单一规则匹配
|
|
@@ -294,7 +293,7 @@ class ProxyServer {
|
|
|
294
293
|
}
|
|
295
294
|
else {
|
|
296
295
|
// HTTP错误,检查状态码
|
|
297
|
-
const statusCode = (
|
|
296
|
+
const statusCode = this.getErrorStatusCode(error, 500);
|
|
298
297
|
if (statusCode >= 400) {
|
|
299
298
|
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode, 'http');
|
|
300
299
|
console.log(`Service ${service.name} added to blacklist due to HTTP error ${statusCode} (${route.id}:${rule.contentType}:${service.id})`);
|
|
@@ -340,20 +339,28 @@ class ProxyServer {
|
|
|
340
339
|
});
|
|
341
340
|
// 确定目标类型
|
|
342
341
|
const targetType = req.path.startsWith('/claude-code/') ? 'claude-code' : 'codex';
|
|
343
|
-
// 记录错误日志 -
|
|
342
|
+
// 记录错误日志 - 包含请求详情和最后失败的服务信息
|
|
343
|
+
const _lastFailedVendor = lastFailedService ? this.dbManager.getVendorByServiceId(lastFailedService.id) : undefined;
|
|
344
344
|
yield this.dbManager.addErrorLog({
|
|
345
345
|
timestamp: Date.now(),
|
|
346
346
|
method: req.method,
|
|
347
347
|
path: req.path,
|
|
348
348
|
statusCode: 503,
|
|
349
|
-
errorMessage: 'All services failed',
|
|
349
|
+
errorMessage: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
|
|
350
350
|
errorStack: lastError === null || lastError === void 0 ? void 0 : lastError.stack,
|
|
351
351
|
requestHeaders: this.normalizeHeaders(req.headers),
|
|
352
352
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
353
353
|
// 添加请求详情
|
|
354
354
|
targetType,
|
|
355
|
-
requestModel: (
|
|
356
|
-
responseTime:
|
|
355
|
+
requestModel: (_c = req.body) === null || _c === void 0 ? void 0 : _c.model,
|
|
356
|
+
responseTime: Date.now() - requestStartAt,
|
|
357
|
+
// 添加最后失败的服务信息
|
|
358
|
+
ruleId: lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.id,
|
|
359
|
+
targetServiceId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.id,
|
|
360
|
+
targetServiceName: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.name,
|
|
361
|
+
targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
|
|
362
|
+
vendorId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.vendorId,
|
|
363
|
+
vendorName: _lastFailedVendor === null || _lastFailedVendor === void 0 ? void 0 : _lastFailedVendor.name,
|
|
357
364
|
});
|
|
358
365
|
// 根据路径判断目标类型并返回适当的错误格式
|
|
359
366
|
const isClaudeCode = req.path.startsWith('/claude-code/');
|
|
@@ -400,7 +407,7 @@ class ProxyServer {
|
|
|
400
407
|
// 添加请求详情
|
|
401
408
|
targetType,
|
|
402
409
|
requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
|
|
403
|
-
responseTime:
|
|
410
|
+
responseTime: Date.now() - requestStartAt,
|
|
404
411
|
});
|
|
405
412
|
// 根据路径判断目标类型并返回适当的错误格式
|
|
406
413
|
const isClaudeCode = req.path.startsWith('/claude-code/');
|
|
@@ -462,7 +469,7 @@ class ProxyServer {
|
|
|
462
469
|
});
|
|
463
470
|
return res.status(404).json({ error: `No active route found for target type: ${targetType}` });
|
|
464
471
|
}
|
|
465
|
-
//
|
|
472
|
+
// 高智商请求判定:存在规则时从消息末尾往前搜索 [!]/[x] 标记
|
|
466
473
|
const forcedContentType = yield this.prepareHighIqRouting(req, route, targetType);
|
|
467
474
|
// 检查是否启用故障切换
|
|
468
475
|
const enableFailover = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enableFailover) !== false; // 默认为 true
|
|
@@ -561,7 +568,7 @@ class ProxyServer {
|
|
|
561
568
|
}
|
|
562
569
|
else {
|
|
563
570
|
// HTTP错误,检查状态码
|
|
564
|
-
const statusCode = (
|
|
571
|
+
const statusCode = this.getErrorStatusCode(error, 500);
|
|
565
572
|
if (statusCode >= 400) {
|
|
566
573
|
yield this.dbManager.addToBlacklist(service.id, route.id, rule.contentType, error.message, statusCode, 'http');
|
|
567
574
|
console.log(`Service ${service.name} added to blacklist due to HTTP error ${statusCode} (${route.id}:${rule.contentType}:${service.id})`);
|
|
@@ -605,20 +612,28 @@ class ProxyServer {
|
|
|
605
612
|
error: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
|
|
606
613
|
tags: this.buildRelayTags(hasRelayAttempt),
|
|
607
614
|
});
|
|
608
|
-
// 记录错误日志 -
|
|
615
|
+
// 记录错误日志 - 包含请求详情和最后失败的服务信息(使用函数参数 targetType)
|
|
616
|
+
const _lastFailedVendor2 = lastFailedService ? this.dbManager.getVendorByServiceId(lastFailedService.id) : undefined;
|
|
609
617
|
yield this.dbManager.addErrorLog({
|
|
610
618
|
timestamp: Date.now(),
|
|
611
619
|
method: req.method,
|
|
612
620
|
path: req.path,
|
|
613
621
|
statusCode: 503,
|
|
614
|
-
errorMessage: 'All services failed',
|
|
622
|
+
errorMessage: (lastError === null || lastError === void 0 ? void 0 : lastError.message) || 'All services failed',
|
|
615
623
|
errorStack: lastError === null || lastError === void 0 ? void 0 : lastError.stack,
|
|
616
624
|
requestHeaders: this.normalizeHeaders(req.headers),
|
|
617
625
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
618
626
|
// 添加请求详情
|
|
619
627
|
targetType,
|
|
620
|
-
requestModel: (
|
|
621
|
-
responseTime:
|
|
628
|
+
requestModel: (_c = req.body) === null || _c === void 0 ? void 0 : _c.model,
|
|
629
|
+
responseTime: Date.now() - requestStartAt,
|
|
630
|
+
// 添加最后失败的服务信息
|
|
631
|
+
ruleId: lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.id,
|
|
632
|
+
targetServiceId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.id,
|
|
633
|
+
targetServiceName: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.name,
|
|
634
|
+
targetModel: (lastFailedRule === null || lastFailedRule === void 0 ? void 0 : lastFailedRule.targetModel) || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
|
|
635
|
+
vendorId: lastFailedService === null || lastFailedService === void 0 ? void 0 : lastFailedService.vendorId,
|
|
636
|
+
vendorName: _lastFailedVendor2 === null || _lastFailedVendor2 === void 0 ? void 0 : _lastFailedVendor2.name,
|
|
622
637
|
});
|
|
623
638
|
// 根据路径判断目标类型并返回适当的错误格式
|
|
624
639
|
const isClaudeCode = req.path.startsWith('/claude-code/');
|
|
@@ -664,7 +679,7 @@ class ProxyServer {
|
|
|
664
679
|
// 添加请求详情
|
|
665
680
|
targetType,
|
|
666
681
|
requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
|
|
667
|
-
responseTime:
|
|
682
|
+
responseTime: Date.now() - requestStartAt,
|
|
668
683
|
});
|
|
669
684
|
if (this.isResponseCommitted(res)) {
|
|
670
685
|
return;
|
|
@@ -882,15 +897,62 @@ class ProxyServer {
|
|
|
882
897
|
}
|
|
883
898
|
return `;已自动转发给 ${forwardedToServiceName} 服务继续处理`;
|
|
884
899
|
}
|
|
900
|
+
/**
|
|
901
|
+
* 解析规则的有效超时时间(毫秒)。
|
|
902
|
+
* 优先级:rule.timeout > config.ruleGlobalTimeout * 1000 > 300000(5分钟)
|
|
903
|
+
*/
|
|
904
|
+
resolveEffectiveTimeout(rule) {
|
|
905
|
+
if (rule.timeout && rule.timeout > 0) {
|
|
906
|
+
return rule.timeout;
|
|
907
|
+
}
|
|
908
|
+
const config = this.dbManager.getConfig();
|
|
909
|
+
if (config.ruleGlobalTimeout && config.ruleGlobalTimeout > 0) {
|
|
910
|
+
return config.ruleGlobalTimeout * 1000;
|
|
911
|
+
}
|
|
912
|
+
return 300000;
|
|
913
|
+
}
|
|
885
914
|
createFailoverError(message, statusCode, originalError) {
|
|
886
915
|
const failoverError = new Error(message);
|
|
887
916
|
failoverError.isFailoverCandidate = true;
|
|
917
|
+
failoverError.statusCode = statusCode;
|
|
888
918
|
failoverError.response = { status: statusCode };
|
|
889
919
|
if (originalError === null || originalError === void 0 ? void 0 : originalError.stack) {
|
|
890
920
|
failoverError.stack = originalError.stack;
|
|
891
921
|
}
|
|
892
922
|
return failoverError;
|
|
893
923
|
}
|
|
924
|
+
getErrorStatusCode(error, fallbackStatusCode = 500) {
|
|
925
|
+
var _a, _b, _c;
|
|
926
|
+
const statusCode = (_c = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : error === null || error === void 0 ? void 0 : error.statusCode) !== null && _c !== void 0 ? _c : error === null || error === void 0 ? void 0 : error.status;
|
|
927
|
+
if (typeof statusCode === 'number' && Number.isFinite(statusCode)) {
|
|
928
|
+
return statusCode;
|
|
929
|
+
}
|
|
930
|
+
return fallbackStatusCode;
|
|
931
|
+
}
|
|
932
|
+
detectStreamFailure(events) {
|
|
933
|
+
var _a, _b;
|
|
934
|
+
for (const event of events) {
|
|
935
|
+
const eventType = (_a = event.event) === null || _a === void 0 ? void 0 : _a.trim();
|
|
936
|
+
if (!eventType)
|
|
937
|
+
continue;
|
|
938
|
+
if (eventType !== 'response.failed' && eventType !== 'error') {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
const parsed = event.data ? this.safeJsonParse(event.data) : null;
|
|
942
|
+
const errorObj = ((_b = parsed === null || parsed === void 0 ? void 0 : parsed.response) === null || _b === void 0 ? void 0 : _b.error) || (parsed === null || parsed === void 0 ? void 0 : parsed.error) || parsed;
|
|
943
|
+
const errorCode = errorObj === null || errorObj === void 0 ? void 0 : errorObj.code;
|
|
944
|
+
const errorMessage = (errorObj === null || errorObj === void 0 ? void 0 : errorObj.message)
|
|
945
|
+
|| (parsed === null || parsed === void 0 ? void 0 : parsed.message)
|
|
946
|
+
|| `Upstream stream returned ${eventType}`;
|
|
947
|
+
const normalizedMessage = `Upstream stream returned ${eventType}: ${errorMessage}`;
|
|
948
|
+
const statusCode = errorCode === 'server_is_overloaded' ? 503 : 502;
|
|
949
|
+
return {
|
|
950
|
+
statusCode,
|
|
951
|
+
errorMessage: normalizedMessage,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
894
956
|
isDownstreamClosed(res) {
|
|
895
957
|
return res.destroyed || res.writableEnded || !res.writable;
|
|
896
958
|
}
|
|
@@ -1390,7 +1452,7 @@ class ProxyServer {
|
|
|
1390
1452
|
},
|
|
1391
1453
|
{
|
|
1392
1454
|
type: 'high-iq',
|
|
1393
|
-
match: (_req, body) => this.hasHighIqSignal(body),
|
|
1455
|
+
match: (_req, body, _sessionId, routeId) => this.hasHighIqSignal(body, routeId),
|
|
1394
1456
|
},
|
|
1395
1457
|
{
|
|
1396
1458
|
type: 'long-context',
|
|
@@ -1534,11 +1596,19 @@ class ProxyServer {
|
|
|
1534
1596
|
((_a = body === null || body === void 0 ? void 0 : body.reasoning) === null || _a === void 0 ? void 0 : _a.effort) ||
|
|
1535
1597
|
((_b = body === null || body === void 0 ? void 0 : body.reasoning) === null || _b === void 0 ? void 0 : _b.enabled));
|
|
1536
1598
|
}
|
|
1537
|
-
|
|
1599
|
+
hasHighIqRuleForRoute(routeId) {
|
|
1600
|
+
var _a;
|
|
1601
|
+
const rules = this.getRulesByRouteId(routeId);
|
|
1602
|
+
return (_a = rules === null || rules === void 0 ? void 0 : rules.some(rule => rule.contentType === 'high-iq' && !rule.isDisabled)) !== null && _a !== void 0 ? _a : false;
|
|
1603
|
+
}
|
|
1604
|
+
hasHighIqSignal(body, routeId) {
|
|
1605
|
+
if (routeId && !this.hasHighIqRuleForRoute(routeId))
|
|
1606
|
+
return false;
|
|
1538
1607
|
return this.inferHighIqRouting(body, false).shouldUseHighIq;
|
|
1539
1608
|
}
|
|
1540
1609
|
inferHighIqRouting(body, previousMode) {
|
|
1541
1610
|
const messages = this.extractConversationMessages(body);
|
|
1611
|
+
// 从消息列表末尾往前查找 [!] 或 [x] 标记,普通消息跳过继续搜索
|
|
1542
1612
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1543
1613
|
const message = messages[i];
|
|
1544
1614
|
if ((message === null || message === void 0 ? void 0 : message.role) !== 'user') {
|
|
@@ -1548,17 +1618,22 @@ class ProxyServer {
|
|
|
1548
1618
|
if (!signal.hasHumanText) {
|
|
1549
1619
|
continue;
|
|
1550
1620
|
}
|
|
1621
|
+
// [x] 优先:同一消息中 [x] 覆盖 [!]
|
|
1622
|
+
if (signal.hasCancelPrefix) {
|
|
1623
|
+
return {
|
|
1624
|
+
shouldUseHighIq: false,
|
|
1625
|
+
decisionSource: 'human',
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1551
1628
|
if (signal.hasHighIqPrefix) {
|
|
1552
1629
|
return {
|
|
1553
1630
|
shouldUseHighIq: true,
|
|
1554
1631
|
decisionSource: 'human',
|
|
1555
1632
|
};
|
|
1556
1633
|
}
|
|
1557
|
-
|
|
1558
|
-
shouldUseHighIq: false,
|
|
1559
|
-
decisionSource: 'human',
|
|
1560
|
-
};
|
|
1634
|
+
// 普通消息(无 [!] 或 [x] 前缀),继续向前搜索
|
|
1561
1635
|
}
|
|
1636
|
+
// 未找到 [!] 或 [x] 标记,回退到 session 持久化状态
|
|
1562
1637
|
if (previousMode) {
|
|
1563
1638
|
return {
|
|
1564
1639
|
shouldUseHighIq: true,
|
|
@@ -1572,6 +1647,10 @@ class ProxyServer {
|
|
|
1572
1647
|
}
|
|
1573
1648
|
prepareHighIqRouting(req, route, targetType) {
|
|
1574
1649
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1650
|
+
// 无高智商规则时直接跳过,避免每次都检查消息前缀
|
|
1651
|
+
if (!this.hasHighIqRuleForRoute(route.id)) {
|
|
1652
|
+
return undefined;
|
|
1653
|
+
}
|
|
1575
1654
|
const sessionId = this.defaultExtractSessionId(req, targetType);
|
|
1576
1655
|
const session = sessionId ? this.dbManager.getSession(sessionId) : null;
|
|
1577
1656
|
const previousMode = (session === null || session === void 0 ? void 0 : session.highIqMode) === true;
|
|
@@ -1583,7 +1662,7 @@ class ProxyServer {
|
|
|
1583
1662
|
highIqRuleId: undefined,
|
|
1584
1663
|
lastRequestAt: Date.now(),
|
|
1585
1664
|
});
|
|
1586
|
-
console.log(`[HIGH-IQ] Session ${sessionId}
|
|
1665
|
+
console.log(`[HIGH-IQ] Session ${sessionId} cancelled by [x] prefix`);
|
|
1587
1666
|
}
|
|
1588
1667
|
return undefined;
|
|
1589
1668
|
}
|
|
@@ -1641,6 +1720,7 @@ class ProxyServer {
|
|
|
1641
1720
|
analyzeUserMessageForHighIq(message) {
|
|
1642
1721
|
let hasHumanText = false;
|
|
1643
1722
|
let hasHighIqPrefix = false;
|
|
1723
|
+
let hasCancelPrefix = false;
|
|
1644
1724
|
const scanText = (text, treatAsHuman) => {
|
|
1645
1725
|
const trimmed = text.trim();
|
|
1646
1726
|
if (!trimmed) {
|
|
@@ -1652,11 +1732,14 @@ class ProxyServer {
|
|
|
1652
1732
|
if (treatAsHuman && trimmed.startsWith('[!]')) {
|
|
1653
1733
|
hasHighIqPrefix = true;
|
|
1654
1734
|
}
|
|
1735
|
+
if (treatAsHuman && /^\[x]/i.test(trimmed)) {
|
|
1736
|
+
hasCancelPrefix = true;
|
|
1737
|
+
}
|
|
1655
1738
|
};
|
|
1656
1739
|
const content = message === null || message === void 0 ? void 0 : message.content;
|
|
1657
1740
|
if (typeof content === 'string') {
|
|
1658
1741
|
scanText(content, true);
|
|
1659
|
-
return { hasHumanText, hasHighIqPrefix };
|
|
1742
|
+
return { hasHumanText, hasHighIqPrefix, hasCancelPrefix };
|
|
1660
1743
|
}
|
|
1661
1744
|
const blocks = Array.isArray(content) ? content : [content];
|
|
1662
1745
|
for (const block of blocks) {
|
|
@@ -1692,7 +1775,7 @@ class ProxyServer {
|
|
|
1692
1775
|
}
|
|
1693
1776
|
}
|
|
1694
1777
|
}
|
|
1695
|
-
return { hasHumanText, hasHighIqPrefix };
|
|
1778
|
+
return { hasHumanText, hasHighIqPrefix, hasCancelPrefix };
|
|
1696
1779
|
}
|
|
1697
1780
|
/**
|
|
1698
1781
|
* 查找可用的高智商规则
|
|
@@ -2134,7 +2217,7 @@ class ProxyServer {
|
|
|
2134
2217
|
const headers = {};
|
|
2135
2218
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
2136
2219
|
// 排除原始认证头,防止与代理设置的认证头冲突
|
|
2137
|
-
if (['host', 'content-length', 'authorization', 'x-api-key', 'x-anthropic-api-key', 'anthropic-api-key', 'x-goog-api-key'].includes(key.toLowerCase())) {
|
|
2220
|
+
if (['host', 'content-length', 'authorization', 'x-api-key', 'x-anthropic-api-key', 'anthropic-api-key', 'x-goog-api-key', 'accept-encoding'].includes(key.toLowerCase())) {
|
|
2138
2221
|
continue;
|
|
2139
2222
|
}
|
|
2140
2223
|
if (typeof value === 'string') {
|
|
@@ -2169,6 +2252,10 @@ class ProxyServer {
|
|
|
2169
2252
|
if (streamRequested && !headers.accept) {
|
|
2170
2253
|
headers.accept = 'text/event-stream';
|
|
2171
2254
|
}
|
|
2255
|
+
// 流式场景显式禁用压缩,避免上游返回压缩字节流导致下游出现乱码
|
|
2256
|
+
if (streamRequested) {
|
|
2257
|
+
headers['accept-encoding'] = 'identity';
|
|
2258
|
+
}
|
|
2172
2259
|
if (!headers.connection) {
|
|
2173
2260
|
if (streamRequested) {
|
|
2174
2261
|
headers.connection = 'keep-alive';
|
|
@@ -2280,6 +2367,26 @@ class ProxyServer {
|
|
|
2280
2367
|
return null;
|
|
2281
2368
|
}
|
|
2282
2369
|
}
|
|
2370
|
+
cloneRequestBody(data) {
|
|
2371
|
+
if (data === null || data === undefined) {
|
|
2372
|
+
return data;
|
|
2373
|
+
}
|
|
2374
|
+
try {
|
|
2375
|
+
return JSON.parse(JSON.stringify(data));
|
|
2376
|
+
}
|
|
2377
|
+
catch (_a) {
|
|
2378
|
+
return data;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
isEmptyResponse(data) {
|
|
2382
|
+
if (data === null || data === undefined)
|
|
2383
|
+
return true;
|
|
2384
|
+
if (typeof data === 'string' && data.trim() === '')
|
|
2385
|
+
return true;
|
|
2386
|
+
if (typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length === 0)
|
|
2387
|
+
return true;
|
|
2388
|
+
return false;
|
|
2389
|
+
}
|
|
2283
2390
|
/**
|
|
2284
2391
|
* 从请求中提取 session ID(默认方法)
|
|
2285
2392
|
* Claude Code: metadata.user_id
|
|
@@ -2692,7 +2799,7 @@ class ProxyServer {
|
|
|
2692
2799
|
}
|
|
2693
2800
|
proxyRequest(req, res, route, rule, service, options) {
|
|
2694
2801
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2695
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
2802
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
2696
2803
|
res.locals.skipLog = true;
|
|
2697
2804
|
const startTime = Date.now();
|
|
2698
2805
|
const rawSourceType = service.sourceType || 'openai-chat';
|
|
@@ -2706,7 +2813,8 @@ class ProxyServer {
|
|
|
2706
2813
|
const forwardedToServiceName = options === null || options === void 0 ? void 0 : options.forwardedToServiceName;
|
|
2707
2814
|
const useOriginalConfig = (options === null || options === void 0 ? void 0 : options.useOriginalConfig) === true;
|
|
2708
2815
|
let relayedForLog = !useOriginalConfig;
|
|
2709
|
-
|
|
2816
|
+
const originalToolRequestBody = this.cloneRequestBody(req.body || {});
|
|
2817
|
+
let requestBody = this.cloneRequestBody(originalToolRequestBody) || {};
|
|
2710
2818
|
let usageForLog;
|
|
2711
2819
|
let logged = false;
|
|
2712
2820
|
const extraTagsForLog = [];
|
|
@@ -3065,8 +3173,9 @@ class ProxyServer {
|
|
|
3065
3173
|
res.once('close', onResponseClosed);
|
|
3066
3174
|
try {
|
|
3067
3175
|
// 使用统一的请求转换方法
|
|
3068
|
-
const
|
|
3069
|
-
|
|
3176
|
+
const payloadForTransform = this.cloneRequestBody(originalToolRequestBody);
|
|
3177
|
+
const transformedRequestBody = this.transformRequestToUpstream(targetType, sourceType, payloadForTransform, rule.targetModel);
|
|
3178
|
+
requestBody = (_b = transformedRequestBody !== null && transformedRequestBody !== void 0 ? transformedRequestBody : this.cloneRequestBody(originalToolRequestBody)) !== null && _b !== void 0 ? _b : {};
|
|
3070
3179
|
// 应用 max_output_tokens 限制
|
|
3071
3180
|
requestBody = this.applyMaxOutputTokensLimit(requestBody, service);
|
|
3072
3181
|
if (this.shouldDefaultStreamingForClaudeBridge(req, targetType, sourceType, requestBody)
|
|
@@ -3091,7 +3200,7 @@ class ProxyServer {
|
|
|
3091
3200
|
method: req.method,
|
|
3092
3201
|
url: upstreamUrl,
|
|
3093
3202
|
headers: this.buildUpstreamHeaders(req, service, sourceType, streamRequested, requestBody),
|
|
3094
|
-
timeout: rule
|
|
3203
|
+
timeout: this.resolveEffectiveTimeout(rule),
|
|
3095
3204
|
validateStatus: () => true,
|
|
3096
3205
|
responseType: streamRequested ? 'stream' : 'json',
|
|
3097
3206
|
signal: upstreamAbortController.signal,
|
|
@@ -3242,6 +3351,7 @@ class ProxyServer {
|
|
|
3242
3351
|
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3243
3352
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3244
3353
|
upstreamRequest: upstreamRequestForLog,
|
|
3354
|
+
responseHeaders: responseHeadersForLog,
|
|
3245
3355
|
// 添加请求详情
|
|
3246
3356
|
ruleId: rule.id,
|
|
3247
3357
|
targetType,
|
|
@@ -3347,6 +3457,7 @@ class ProxyServer {
|
|
|
3347
3457
|
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3348
3458
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3349
3459
|
upstreamRequest: upstreamRequestForLog,
|
|
3460
|
+
responseHeaders: responseHeadersForLog,
|
|
3350
3461
|
// 添加请求详情
|
|
3351
3462
|
ruleId: rule.id,
|
|
3352
3463
|
targetType,
|
|
@@ -3441,6 +3552,7 @@ class ProxyServer {
|
|
|
3441
3552
|
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3442
3553
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3443
3554
|
upstreamRequest: upstreamRequestForLog,
|
|
3555
|
+
responseHeaders: responseHeadersForLog,
|
|
3444
3556
|
ruleId: rule.id,
|
|
3445
3557
|
targetType,
|
|
3446
3558
|
targetServiceId: service.id,
|
|
@@ -3537,6 +3649,7 @@ class ProxyServer {
|
|
|
3537
3649
|
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3538
3650
|
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3539
3651
|
upstreamRequest: upstreamRequestForLog,
|
|
3652
|
+
responseHeaders: responseHeadersForLog,
|
|
3540
3653
|
ruleId: rule.id,
|
|
3541
3654
|
targetType,
|
|
3542
3655
|
targetServiceId: service.id,
|
|
@@ -3599,94 +3712,142 @@ class ProxyServer {
|
|
|
3599
3712
|
else if (extractedUsage) {
|
|
3600
3713
|
usageForLog = this.extractTokenUsageFromResponse(extractedUsage, sourceType);
|
|
3601
3714
|
}
|
|
3602
|
-
void finalizeLog(res.statusCode);
|
|
3603
3715
|
};
|
|
3604
|
-
// 在下游 chunk 收集器完成后执行,确保拿到真正下发给客户端的完整文本
|
|
3605
|
-
downstreamChunkCollector.on('finish', () => {
|
|
3606
|
-
finalizeChunks();
|
|
3607
|
-
});
|
|
3608
|
-
// 备用:如果eventCollector的finish没有触发,监听res的finish
|
|
3609
|
-
res.on('finish', () => {
|
|
3610
|
-
if (!streamChunksForLog) {
|
|
3611
|
-
finalizeChunks();
|
|
3612
|
-
}
|
|
3613
|
-
});
|
|
3614
3716
|
// 监听 res 的错误事件
|
|
3615
3717
|
res.on('error', (err) => {
|
|
3616
3718
|
console.error('[Proxy] Response stream error:', err);
|
|
3617
3719
|
});
|
|
3618
|
-
|
|
3619
|
-
if (converter) {
|
|
3720
|
+
const runStreamPipeline = () => __awaiter(this, void 0, void 0, function* () {
|
|
3620
3721
|
ensureResponseWritable();
|
|
3621
|
-
|
|
3622
|
-
if (
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
yield this.dbManager.addErrorLog({
|
|
3632
|
-
timestamp: Date.now(),
|
|
3633
|
-
method: req.method,
|
|
3634
|
-
path: req.path,
|
|
3635
|
-
statusCode: 500,
|
|
3636
|
-
errorMessage: error.message || 'Stream processing error',
|
|
3637
|
-
errorStack: error.stack,
|
|
3638
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3639
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3640
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3641
|
-
});
|
|
3642
|
-
}
|
|
3643
|
-
catch (logError) {
|
|
3644
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3645
|
-
}
|
|
3646
|
-
yield finalizeLog(500, error.message);
|
|
3722
|
+
return yield new Promise((resolve, reject) => {
|
|
3723
|
+
if (converter) {
|
|
3724
|
+
(0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, downstreamChunkCollector, res, (error) => {
|
|
3725
|
+
if (error) {
|
|
3726
|
+
reject(error);
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
resolve();
|
|
3730
|
+
});
|
|
3731
|
+
return;
|
|
3647
3732
|
}
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
ensureResponseWritable();
|
|
3652
|
-
(0, stream_1.pipeline)(response.data, parser, eventCollector, serializer, downstreamChunkCollector, res, (error) => __awaiter(this, void 0, void 0, function* () {
|
|
3653
|
-
if (error) {
|
|
3654
|
-
if (this.isClientDisconnectError(error, res)) {
|
|
3655
|
-
console.warn('[Proxy] Default stream pipeline closed because client disconnected');
|
|
3656
|
-
yield finalizeLog(499, 'Client disconnected');
|
|
3733
|
+
(0, stream_1.pipeline)(response.data, parser, eventCollector, serializer, downstreamChunkCollector, res, (error) => {
|
|
3734
|
+
if (error) {
|
|
3735
|
+
reject(error);
|
|
3657
3736
|
return;
|
|
3658
3737
|
}
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
path: req.path,
|
|
3666
|
-
statusCode: 500,
|
|
3667
|
-
errorMessage: error.message || 'Stream processing error',
|
|
3668
|
-
errorStack: error.stack,
|
|
3669
|
-
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3670
|
-
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3671
|
-
upstreamRequest: upstreamRequestForLog,
|
|
3672
|
-
});
|
|
3673
|
-
}
|
|
3674
|
-
catch (logError) {
|
|
3675
|
-
console.error('[Proxy] Failed to log error:', logError);
|
|
3676
|
-
}
|
|
3677
|
-
yield finalizeLog(500, error.message);
|
|
3678
|
-
}
|
|
3679
|
-
}));
|
|
3738
|
+
resolve();
|
|
3739
|
+
});
|
|
3740
|
+
});
|
|
3741
|
+
});
|
|
3742
|
+
try {
|
|
3743
|
+
yield runStreamPipeline();
|
|
3680
3744
|
}
|
|
3745
|
+
catch (error) {
|
|
3746
|
+
if (this.isClientDisconnectError(error, res)) {
|
|
3747
|
+
console.warn('[Proxy] Default stream pipeline closed because client disconnected');
|
|
3748
|
+
yield finalizeLog(499, 'Client disconnected');
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
console.error('[Proxy] Pipeline error (default stream):', error);
|
|
3752
|
+
// 记录到错误日志
|
|
3753
|
+
try {
|
|
3754
|
+
yield this.dbManager.addErrorLog({
|
|
3755
|
+
timestamp: Date.now(),
|
|
3756
|
+
method: req.method,
|
|
3757
|
+
path: req.path,
|
|
3758
|
+
statusCode: 500,
|
|
3759
|
+
errorMessage: error.message || 'Stream processing error',
|
|
3760
|
+
errorStack: error.stack,
|
|
3761
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3762
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3763
|
+
upstreamRequest: upstreamRequestForLog,
|
|
3764
|
+
responseHeaders: responseHeadersForLog,
|
|
3765
|
+
ruleId: rule.id,
|
|
3766
|
+
targetType,
|
|
3767
|
+
targetServiceId: service.id,
|
|
3768
|
+
targetServiceName: service.name,
|
|
3769
|
+
targetModel: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
|
|
3770
|
+
vendorId: service.vendorId,
|
|
3771
|
+
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3772
|
+
requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
|
|
3773
|
+
responseTime: Date.now() - startTime,
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
catch (logError) {
|
|
3777
|
+
console.error('[Proxy] Failed to log error:', logError);
|
|
3778
|
+
}
|
|
3779
|
+
yield finalizeLog(500, error.message);
|
|
3780
|
+
if (failoverEnabled && !this.isResponseCommitted(res)) {
|
|
3781
|
+
throw this.createFailoverError(error.message || 'Stream processing error', 500, error);
|
|
3782
|
+
}
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
finalizeChunks();
|
|
3786
|
+
// 检测空流:上游返回 SSE Content-Type 但没有发送任何事件数据
|
|
3787
|
+
const collectedEvents = eventCollector.getEvents();
|
|
3788
|
+
if (collectedEvents.length === 0) {
|
|
3789
|
+
const emptyStreamMsg = 'Upstream API returned an empty stream (HTTP 200, no SSE events)';
|
|
3790
|
+
console.warn(`[Proxy] ${emptyStreamMsg}`);
|
|
3791
|
+
yield finalizeLog(200, emptyStreamMsg);
|
|
3792
|
+
if (!res.writableEnded) {
|
|
3793
|
+
res.end();
|
|
3794
|
+
}
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3797
|
+
// 关键修复:识别 stream 内部的 response.failed / error 事件,归类为错误并触发 failover 交接
|
|
3798
|
+
const streamFailure = this.detectStreamFailure(collectedEvents);
|
|
3799
|
+
if (streamFailure) {
|
|
3800
|
+
try {
|
|
3801
|
+
yield this.dbManager.addErrorLog({
|
|
3802
|
+
timestamp: Date.now(),
|
|
3803
|
+
method: req.method,
|
|
3804
|
+
path: req.path,
|
|
3805
|
+
statusCode: streamFailure.statusCode,
|
|
3806
|
+
errorMessage: streamFailure.errorMessage,
|
|
3807
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3808
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3809
|
+
upstreamRequest: upstreamRequestForLog,
|
|
3810
|
+
responseHeaders: responseHeadersForLog,
|
|
3811
|
+
responseBody: responseBodyForLog,
|
|
3812
|
+
ruleId: rule.id,
|
|
3813
|
+
targetType,
|
|
3814
|
+
targetServiceId: service.id,
|
|
3815
|
+
targetServiceName: service.name,
|
|
3816
|
+
targetModel: rule.targetModel || ((_f = req.body) === null || _f === void 0 ? void 0 : _f.model),
|
|
3817
|
+
vendorId: service.vendorId,
|
|
3818
|
+
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3819
|
+
requestModel: (_g = req.body) === null || _g === void 0 ? void 0 : _g.model,
|
|
3820
|
+
responseTime: Date.now() - startTime,
|
|
3821
|
+
});
|
|
3822
|
+
}
|
|
3823
|
+
catch (logError) {
|
|
3824
|
+
console.error('[Proxy] Failed to log stream failure:', logError);
|
|
3825
|
+
}
|
|
3826
|
+
yield finalizeLog(streamFailure.statusCode, streamFailure.errorMessage);
|
|
3827
|
+
if (failoverEnabled && !this.isResponseCommitted(res)) {
|
|
3828
|
+
throw this.createFailoverError(streamFailure.errorMessage, streamFailure.statusCode);
|
|
3829
|
+
}
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
yield finalizeLog(res.statusCode);
|
|
3681
3833
|
return;
|
|
3682
3834
|
}
|
|
3683
3835
|
let responseData = response.data;
|
|
3684
3836
|
if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
|
|
3685
3837
|
const raw = yield this.readStreamBody(response.data);
|
|
3686
|
-
responseData = (
|
|
3838
|
+
responseData = (_h = this.safeJsonParse(raw)) !== null && _h !== void 0 ? _h : raw;
|
|
3687
3839
|
}
|
|
3688
3840
|
// 收集响应头
|
|
3689
3841
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3842
|
+
// 检测上游空响应(HTTP 200 但 body 为空)— 透传 200
|
|
3843
|
+
if (this.isEmptyResponse(responseData)) {
|
|
3844
|
+
const emptyInfoMsg = 'Upstream API returned an empty response (HTTP 200), passing through';
|
|
3845
|
+
console.warn(`[Proxy] ${emptyInfoMsg}`);
|
|
3846
|
+
responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
|
|
3847
|
+
yield finalizeLog(200, emptyInfoMsg);
|
|
3848
|
+
res.status(200).end();
|
|
3849
|
+
return;
|
|
3850
|
+
}
|
|
3690
3851
|
// 使用统一的响应转换方法
|
|
3691
3852
|
const converted = this.transformResponseToTool(targetType, sourceType, responseData);
|
|
3692
3853
|
// 提取 token usage(从原始响应数据中提取)
|
|
@@ -3719,14 +3880,62 @@ class ProxyServer {
|
|
|
3719
3880
|
yield finalizeLog(499, 'Client disconnected');
|
|
3720
3881
|
return;
|
|
3721
3882
|
}
|
|
3883
|
+
// 特殊处理:count_tokens 请求无论如何都返回 200
|
|
3884
|
+
const isCountTokensRequest = this.isCountTokensPath(req.path) || this.isCountTokensPath(req.originalUrl);
|
|
3885
|
+
if (isCountTokensRequest) {
|
|
3886
|
+
console.warn('[Proxy] count_tokens request failed, falling back to local estimation:', error.message);
|
|
3887
|
+
// 使用本地估算返回结果
|
|
3888
|
+
const inputTokens = this.estimateClaudeCountTokens(requestBody);
|
|
3889
|
+
const localTokenResponse = { input_tokens: inputTokens };
|
|
3890
|
+
usageForLog = {
|
|
3891
|
+
inputTokens,
|
|
3892
|
+
outputTokens: 0,
|
|
3893
|
+
totalTokens: inputTokens,
|
|
3894
|
+
};
|
|
3895
|
+
responseHeadersForLog = {
|
|
3896
|
+
'content-type': 'application/json; charset=utf-8',
|
|
3897
|
+
};
|
|
3898
|
+
responseBodyForLog = JSON.stringify(localTokenResponse);
|
|
3899
|
+
streamChunksForLog = undefined;
|
|
3900
|
+
relayedForLog = false;
|
|
3901
|
+
extraTagsForLog.push('上游失败-本地计算Token');
|
|
3902
|
+
// 记录错误日志(但不影响响应)
|
|
3903
|
+
const vendors = this.dbManager.getVendors();
|
|
3904
|
+
const vendor = vendors.find(v => v.id === service.vendorId);
|
|
3905
|
+
yield this.dbManager.addErrorLog({
|
|
3906
|
+
timestamp: Date.now(),
|
|
3907
|
+
method: req.method,
|
|
3908
|
+
path: req.path,
|
|
3909
|
+
statusCode: 200, // 实际返回 200
|
|
3910
|
+
errorMessage: `count_tokens upstream failed, used local estimation: ${error.message}`,
|
|
3911
|
+
errorStack: error.stack,
|
|
3912
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3913
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3914
|
+
ruleId: rule.id,
|
|
3915
|
+
targetType,
|
|
3916
|
+
targetServiceId: service.id,
|
|
3917
|
+
targetServiceName: service.name,
|
|
3918
|
+
targetModel: rule.targetModel || ((_j = req.body) === null || _j === void 0 ? void 0 : _j.model),
|
|
3919
|
+
vendorId: service.vendorId,
|
|
3920
|
+
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3921
|
+
requestModel: (_k = req.body) === null || _k === void 0 ? void 0 : _k.model,
|
|
3922
|
+
upstreamRequest: upstreamRequestForLog,
|
|
3923
|
+
responseTime: Date.now() - startTime,
|
|
3924
|
+
});
|
|
3925
|
+
// 返回 200 状态码和本地估算结果
|
|
3926
|
+
res.status(200).json(localTokenResponse);
|
|
3927
|
+
yield finalizeLog(200);
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3722
3930
|
if (failoverEnabled && (error === null || error === void 0 ? void 0 : error.isFailoverCandidate)) {
|
|
3723
3931
|
throw error;
|
|
3724
3932
|
}
|
|
3725
3933
|
console.error('Proxy error:', error);
|
|
3726
3934
|
// 检测是否是 timeout 错误
|
|
3727
3935
|
const isTimeout = error.code === 'ECONNABORTED' ||
|
|
3728
|
-
((
|
|
3936
|
+
((_l = error.message) === null || _l === void 0 ? void 0 : _l.toLowerCase().includes('timeout')) ||
|
|
3729
3937
|
(error.errno && error.errno === 'ETIMEDOUT');
|
|
3938
|
+
const statusCode = isTimeout ? 504 : this.getErrorStatusCode(error, 500);
|
|
3730
3939
|
const baseErrorMessage = isTimeout
|
|
3731
3940
|
? 'Request timeout - the upstream API took too long to respond'
|
|
3732
3941
|
: (error.message || 'Internal server error');
|
|
@@ -3740,7 +3949,7 @@ class ProxyServer {
|
|
|
3740
3949
|
timestamp: Date.now(),
|
|
3741
3950
|
method: req.method,
|
|
3742
3951
|
path: req.path,
|
|
3743
|
-
statusCode
|
|
3952
|
+
statusCode,
|
|
3744
3953
|
errorMessage: errorMessage,
|
|
3745
3954
|
errorStack: error.stack,
|
|
3746
3955
|
requestHeaders: this.normalizeHeaders(req.headers),
|
|
@@ -3750,16 +3959,17 @@ class ProxyServer {
|
|
|
3750
3959
|
targetType,
|
|
3751
3960
|
targetServiceId: service.id,
|
|
3752
3961
|
targetServiceName: service.name,
|
|
3753
|
-
targetModel: rule.targetModel || ((
|
|
3962
|
+
targetModel: rule.targetModel || ((_m = req.body) === null || _m === void 0 ? void 0 : _m.model),
|
|
3754
3963
|
vendorId: service.vendorId,
|
|
3755
3964
|
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3756
|
-
requestModel: (
|
|
3965
|
+
requestModel: (_o = req.body) === null || _o === void 0 ? void 0 : _o.model,
|
|
3757
3966
|
upstreamRequest: upstreamRequestForLog,
|
|
3967
|
+
responseHeaders: responseHeadersForLog,
|
|
3758
3968
|
responseTime: Date.now() - startTime,
|
|
3759
3969
|
});
|
|
3760
|
-
yield finalizeLog(
|
|
3970
|
+
yield finalizeLog(statusCode, errorMessage);
|
|
3761
3971
|
if (failoverEnabled) {
|
|
3762
|
-
throw this.createFailoverError(errorMessage,
|
|
3972
|
+
throw this.createFailoverError(errorMessage, statusCode, error);
|
|
3763
3973
|
}
|
|
3764
3974
|
if (this.isResponseCommitted(res)) {
|
|
3765
3975
|
return;
|
|
@@ -3788,12 +3998,12 @@ class ProxyServer {
|
|
|
3788
3998
|
}
|
|
3789
3999
|
else {
|
|
3790
4000
|
// 非流式请求:返回 JSON 格式
|
|
3791
|
-
res.status(
|
|
4001
|
+
res.status(statusCode).json(claudeError);
|
|
3792
4002
|
}
|
|
3793
4003
|
}
|
|
3794
4004
|
else {
|
|
3795
4005
|
// 对于 Codex,返回 JSON 格式的错误响应
|
|
3796
|
-
res.status(
|
|
4006
|
+
res.status(statusCode).json({ error: errorMessage });
|
|
3797
4007
|
}
|
|
3798
4008
|
}
|
|
3799
4009
|
finally {
|