aicodeswitch 5.2.1 → 5.2.2
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/main.js +6 -0
- package/dist/server/proxy-server.js +255 -17
- package/package.json +2 -1
package/dist/server/main.js
CHANGED
|
@@ -1045,6 +1045,12 @@ const listInstalledSkills = () => {
|
|
|
1045
1045
|
const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1046
1046
|
updateProxyConfig(dbManager.getConfig());
|
|
1047
1047
|
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
|
|
1048
|
+
// 数据就绪验证端点(供 Tauri 启动阶段确认后端完全可用)
|
|
1049
|
+
app.get('/api/ready', (_req, res) => {
|
|
1050
|
+
const vendors = dbManager.getVendors();
|
|
1051
|
+
const routes = dbManager.getRoutes();
|
|
1052
|
+
res.json({ ready: true, vendorsCount: vendors.length, routesCount: routes.length });
|
|
1053
|
+
});
|
|
1048
1054
|
// 局域网访问控制中间件:当 enableLanDiscovery 关闭时,仅允许本机访问 /api/* 路由
|
|
1049
1055
|
app.use('/api', (req, res, next) => {
|
|
1050
1056
|
const config = dbManager.getConfig();
|
|
@@ -1385,6 +1385,161 @@ class ProxyServer {
|
|
|
1385
1385
|
isResponseCommitted(res) {
|
|
1386
1386
|
return res.headersSent || this.isDownstreamClosed(res);
|
|
1387
1387
|
}
|
|
1388
|
+
/**
|
|
1389
|
+
* SSE 流预检:在提交响应头之前读取上游流的第一个有意义的 SSE 事件,
|
|
1390
|
+
* 判断上游是否健康。若首事件为错误(response.failed / error),则不提交响应头,
|
|
1391
|
+
* 允许外层故障切换循环尝试下一个候选服务。
|
|
1392
|
+
*
|
|
1393
|
+
* @returns healthy=true 时携带 bufferedRaw(预检期间读取的原始字节),
|
|
1394
|
+
* healthy=false 时携带 failureInfo 用于构建错误信息。
|
|
1395
|
+
*/
|
|
1396
|
+
preflightStream(upstreamStream, options = {}) {
|
|
1397
|
+
const { timeoutMs = 5000 } = options;
|
|
1398
|
+
return new Promise((resolve) => {
|
|
1399
|
+
const chunks = [];
|
|
1400
|
+
const tempParser = new streaming_1.SSEParserTransform();
|
|
1401
|
+
const events = [];
|
|
1402
|
+
let settled = false;
|
|
1403
|
+
let timer;
|
|
1404
|
+
const finish = (result) => {
|
|
1405
|
+
var _a;
|
|
1406
|
+
if (settled)
|
|
1407
|
+
return;
|
|
1408
|
+
settled = true;
|
|
1409
|
+
if (timer)
|
|
1410
|
+
clearTimeout(timer);
|
|
1411
|
+
// 停止从上游流读取,但不 destroy(后续可能还需要用)
|
|
1412
|
+
(_a = upstreamStream.removeAllListeners) === null || _a === void 0 ? void 0 : _a.call(upstreamStream);
|
|
1413
|
+
resolve(result);
|
|
1414
|
+
};
|
|
1415
|
+
// 超时保护
|
|
1416
|
+
timer = setTimeout(() => {
|
|
1417
|
+
finish({
|
|
1418
|
+
healthy: false,
|
|
1419
|
+
failureInfo: { statusCode: 504, errorMessage: 'Stream preflight timed out waiting for first event' },
|
|
1420
|
+
bufferedRaw: Buffer.concat(chunks),
|
|
1421
|
+
});
|
|
1422
|
+
}, timeoutMs);
|
|
1423
|
+
// 将原始数据喂给临时 parser 解析 SSE 事件
|
|
1424
|
+
const onData = (chunk) => {
|
|
1425
|
+
if (settled)
|
|
1426
|
+
return;
|
|
1427
|
+
chunks.push(chunk);
|
|
1428
|
+
// 手动喂给 parser
|
|
1429
|
+
tempParser.write(chunk);
|
|
1430
|
+
drainParserEvents();
|
|
1431
|
+
};
|
|
1432
|
+
const drainParserEvents = () => {
|
|
1433
|
+
var _a, _b;
|
|
1434
|
+
// 从 tempParser 的 readable 侧读出解析后的事件
|
|
1435
|
+
let event;
|
|
1436
|
+
while (null !== (event = tempParser.read())) {
|
|
1437
|
+
if (settled)
|
|
1438
|
+
return;
|
|
1439
|
+
events.push(event);
|
|
1440
|
+
// 跳过无意义的空事件和 done
|
|
1441
|
+
const eventType = (_a = event.event) === null || _a === void 0 ? void 0 : _a.trim();
|
|
1442
|
+
const eventData = event.data;
|
|
1443
|
+
if (!eventType && eventData && typeof eventData === 'object' && eventData.type === 'done')
|
|
1444
|
+
continue;
|
|
1445
|
+
if (!eventType && !eventData)
|
|
1446
|
+
continue;
|
|
1447
|
+
// 检查是否为错误事件
|
|
1448
|
+
if (eventType === 'response.failed' || eventType === 'error') {
|
|
1449
|
+
const parsed = event.data ? this.safeJsonParse(event.data) : null;
|
|
1450
|
+
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;
|
|
1451
|
+
const errorCode = errorObj === null || errorObj === void 0 ? void 0 : errorObj.code;
|
|
1452
|
+
const errorMessage = (errorObj === null || errorObj === void 0 ? void 0 : errorObj.message)
|
|
1453
|
+
|| (parsed === null || parsed === void 0 ? void 0 : parsed.message)
|
|
1454
|
+
|| `Upstream stream returned ${eventType}`;
|
|
1455
|
+
const statusCode = errorCode === 'server_is_overloaded' ? 503 : 502;
|
|
1456
|
+
finish({
|
|
1457
|
+
healthy: false,
|
|
1458
|
+
failureInfo: {
|
|
1459
|
+
statusCode,
|
|
1460
|
+
errorMessage: `Upstream stream returned ${eventType}: ${errorMessage}`,
|
|
1461
|
+
},
|
|
1462
|
+
bufferedRaw: Buffer.concat(chunks),
|
|
1463
|
+
errorData: event.data,
|
|
1464
|
+
});
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
// 首个有意义的正常事件 → 健康通过
|
|
1468
|
+
finish({
|
|
1469
|
+
healthy: true,
|
|
1470
|
+
bufferedRaw: Buffer.concat(chunks),
|
|
1471
|
+
});
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
const onEnd = () => {
|
|
1476
|
+
if (settled)
|
|
1477
|
+
return;
|
|
1478
|
+
// 流结束了但没读到有意义的 event
|
|
1479
|
+
if (events.length === 0 && chunks.length === 0) {
|
|
1480
|
+
finish({
|
|
1481
|
+
healthy: false,
|
|
1482
|
+
failureInfo: { statusCode: 502, errorMessage: 'Upstream stream ended before sending any data' },
|
|
1483
|
+
bufferedRaw: Buffer.concat(chunks),
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
else {
|
|
1487
|
+
// 读到了一些数据但没有明确的错误 → 视为健康
|
|
1488
|
+
finish({
|
|
1489
|
+
healthy: true,
|
|
1490
|
+
bufferedRaw: Buffer.concat(chunks),
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
const onError = (err) => {
|
|
1495
|
+
if (settled)
|
|
1496
|
+
return;
|
|
1497
|
+
finish({
|
|
1498
|
+
healthy: false,
|
|
1499
|
+
failureInfo: { statusCode: 502, errorMessage: `Upstream stream error during preflight: ${err.message}` },
|
|
1500
|
+
bufferedRaw: Buffer.concat(chunks),
|
|
1501
|
+
});
|
|
1502
|
+
};
|
|
1503
|
+
upstreamStream.on('data', onData);
|
|
1504
|
+
upstreamStream.once('end', onEnd);
|
|
1505
|
+
upstreamStream.once('error', onError);
|
|
1506
|
+
// 暂停自动读取 — 我们只需要第一个事件
|
|
1507
|
+
// 注意:不能 pause,因为 axios stream 需要 flow mode 才能获取数据
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* 创建一个组合流:先输出 bufferedRaw 中的原始字节,再透传上游流的剩余数据。
|
|
1512
|
+
* 用于预检通过后无缝衔接后续的 SSE 管道。
|
|
1513
|
+
*/
|
|
1514
|
+
createPreflightCombinedStream(upstreamStream, bufferedRaw) {
|
|
1515
|
+
let pushed = false;
|
|
1516
|
+
const combined = new stream_1.Readable({
|
|
1517
|
+
read() {
|
|
1518
|
+
var _a, _b;
|
|
1519
|
+
if (!pushed) {
|
|
1520
|
+
pushed = true;
|
|
1521
|
+
if (bufferedRaw.length > 0) {
|
|
1522
|
+
this.push(bufferedRaw);
|
|
1523
|
+
}
|
|
1524
|
+
// 将上游流 pipe 到 combined
|
|
1525
|
+
upstreamStream.on('data', (chunk) => {
|
|
1526
|
+
if (!this.push(chunk)) {
|
|
1527
|
+
upstreamStream.pause();
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
upstreamStream.once('end', () => {
|
|
1531
|
+
this.push(null);
|
|
1532
|
+
});
|
|
1533
|
+
upstreamStream.once('error', (err) => {
|
|
1534
|
+
this.destroy(err);
|
|
1535
|
+
});
|
|
1536
|
+
// 如果上游已经暂停了(预检时消费了一些数据),恢复它
|
|
1537
|
+
(_b = (_a = upstreamStream).resume) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1538
|
+
}
|
|
1539
|
+
},
|
|
1540
|
+
});
|
|
1541
|
+
return combined;
|
|
1542
|
+
}
|
|
1388
1543
|
isClientDisconnectError(error, res) {
|
|
1389
1544
|
const code = error === null || error === void 0 ? void 0 : error.code;
|
|
1390
1545
|
if (code === 'CLIENT_DISCONNECTED' || code === 'ERR_CANCELED') {
|
|
@@ -3193,7 +3348,7 @@ class ProxyServer {
|
|
|
3193
3348
|
}
|
|
3194
3349
|
proxyRequest(req, res, route, rule, service, options) {
|
|
3195
3350
|
return __awaiter(this, void 0, void 0, function* () {
|
|
3196
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
3351
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
|
|
3197
3352
|
res.locals.skipLog = true;
|
|
3198
3353
|
const startTime = Date.now();
|
|
3199
3354
|
const rawSourceType = service.sourceType || 'openai-chat';
|
|
@@ -3815,6 +3970,64 @@ class ProxyServer {
|
|
|
3815
3970
|
return;
|
|
3816
3971
|
}
|
|
3817
3972
|
if (isEventStream && response.data) {
|
|
3973
|
+
// ── SSE 预检:在提交响应头之前,先读取第一个 SSE 事件以检测上游错误 ──
|
|
3974
|
+
// 这使得故障切换能在流式场景下生效(首事件为 error 时不提交响应头,允许切换到下一个服务)
|
|
3975
|
+
const preflightResult = yield this.preflightStream(response.data, { timeoutMs: 5000 });
|
|
3976
|
+
if (!preflightResult.healthy) {
|
|
3977
|
+
// 预检失败(首事件是错误 / 超时 / 流提前关闭)
|
|
3978
|
+
const failureInfo = preflightResult.failureInfo;
|
|
3979
|
+
console.warn(`[Proxy] Stream preflight failed: ${(failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.errorMessage) || 'unknown'}`);
|
|
3980
|
+
// 尝试读取完整错误体用于日志
|
|
3981
|
+
let errorBody = preflightResult.errorData;
|
|
3982
|
+
if (!errorBody && preflightResult.bufferedRaw.length > 0) {
|
|
3983
|
+
errorBody = this.safeJsonParse(preflightResult.bufferedRaw.toString('utf8'));
|
|
3984
|
+
}
|
|
3985
|
+
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3986
|
+
const errorMsg = (failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.errorMessage) || 'Stream preflight detected upstream error';
|
|
3987
|
+
// 记录错误日志
|
|
3988
|
+
try {
|
|
3989
|
+
yield this.dbManager.addErrorLog({
|
|
3990
|
+
timestamp: Date.now(),
|
|
3991
|
+
method: req.method,
|
|
3992
|
+
path: req.path,
|
|
3993
|
+
statusCode: (failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502,
|
|
3994
|
+
errorMessage: errorMsg,
|
|
3995
|
+
requestHeaders: this.normalizeHeaders(req.headers),
|
|
3996
|
+
requestBody: req.body ? JSON.stringify(req.body) : undefined,
|
|
3997
|
+
upstreamRequest: upstreamRequestForLog,
|
|
3998
|
+
responseHeaders: responseHeadersForLog,
|
|
3999
|
+
responseBody: preflightResult.bufferedRaw.toString('utf8'),
|
|
4000
|
+
ruleId: rule.id,
|
|
4001
|
+
targetType,
|
|
4002
|
+
targetServiceId: service.id,
|
|
4003
|
+
targetServiceName: service.name,
|
|
4004
|
+
targetModel: rule.targetModel || ((_d = req.body) === null || _d === void 0 ? void 0 : _d.model),
|
|
4005
|
+
vendorId: service.vendorId,
|
|
4006
|
+
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
4007
|
+
requestModel: (_e = req.body) === null || _e === void 0 ? void 0 : _e.model,
|
|
4008
|
+
responseTime: Date.now() - startTime,
|
|
4009
|
+
});
|
|
4010
|
+
}
|
|
4011
|
+
catch (logError) {
|
|
4012
|
+
console.error('[Proxy] Failed to log preflight error:', logError);
|
|
4013
|
+
}
|
|
4014
|
+
yield finalizeLog((failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502, errorMsg);
|
|
4015
|
+
// 销毁上游流
|
|
4016
|
+
if (typeof response.data.destroy === 'function') {
|
|
4017
|
+
response.data.destroy();
|
|
4018
|
+
}
|
|
4019
|
+
// 响应头未提交 → 可以触发故障切换
|
|
4020
|
+
if (failoverEnabled) {
|
|
4021
|
+
throw this.createFailoverError(errorMsg, (failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502);
|
|
4022
|
+
}
|
|
4023
|
+
// 非 failover 模式:直接返回错误给客户端
|
|
4024
|
+
res.status((failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502).json({
|
|
4025
|
+
error: { message: errorMsg, type: 'upstream_error' },
|
|
4026
|
+
});
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
// ── 预检通过:提交响应头,使用组合流继续管道传输 ──
|
|
4030
|
+
const streamSource = this.createPreflightCombinedStream(response.data, preflightResult.bufferedRaw);
|
|
3818
4031
|
res.status(response.status);
|
|
3819
4032
|
// 默认stream处理(无转换)
|
|
3820
4033
|
const parser = new streaming_1.SSEParserTransform();
|
|
@@ -3827,7 +4040,7 @@ class ProxyServer {
|
|
|
3827
4040
|
? new ClaudeCompactResponseSanitizer()
|
|
3828
4041
|
: null;
|
|
3829
4042
|
// 流式 model 回写:将上游返回的 model 改写为客户端请求时的原始模型名
|
|
3830
|
-
const originalModel = (
|
|
4043
|
+
const originalModel = (_f = req.body) === null || _f === void 0 ? void 0 : _f.model;
|
|
3831
4044
|
const modelRewriter = originalModel ? new model_rewrite_transform_1.ModelRewriteTransform(originalModel) : null;
|
|
3832
4045
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
3833
4046
|
// 使用 transformSSEToTool 方法选择转换器
|
|
@@ -3864,7 +4077,7 @@ class ProxyServer {
|
|
|
3864
4077
|
ensureResponseWritable();
|
|
3865
4078
|
return yield new Promise((resolve, reject) => {
|
|
3866
4079
|
if (converter) {
|
|
3867
|
-
const streamStages = [
|
|
4080
|
+
const streamStages = [streamSource, parser, eventCollector, converter];
|
|
3868
4081
|
if (compactResponseSanitizer) {
|
|
3869
4082
|
streamStages.push(compactResponseSanitizer);
|
|
3870
4083
|
}
|
|
@@ -3882,7 +4095,7 @@ class ProxyServer {
|
|
|
3882
4095
|
});
|
|
3883
4096
|
return;
|
|
3884
4097
|
}
|
|
3885
|
-
const streamStages = [
|
|
4098
|
+
const streamStages = [streamSource, parser, eventCollector];
|
|
3886
4099
|
if (compactResponseSanitizer) {
|
|
3887
4100
|
streamStages.push(compactResponseSanitizer);
|
|
3888
4101
|
}
|
|
@@ -3927,10 +4140,10 @@ class ProxyServer {
|
|
|
3927
4140
|
targetType,
|
|
3928
4141
|
targetServiceId: service.id,
|
|
3929
4142
|
targetServiceName: service.name,
|
|
3930
|
-
targetModel: rule.targetModel || ((
|
|
4143
|
+
targetModel: rule.targetModel || ((_g = req.body) === null || _g === void 0 ? void 0 : _g.model),
|
|
3931
4144
|
vendorId: service.vendorId,
|
|
3932
4145
|
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3933
|
-
requestModel: (
|
|
4146
|
+
requestModel: (_h = req.body) === null || _h === void 0 ? void 0 : _h.model,
|
|
3934
4147
|
responseTime: Date.now() - startTime,
|
|
3935
4148
|
});
|
|
3936
4149
|
}
|
|
@@ -3974,10 +4187,10 @@ class ProxyServer {
|
|
|
3974
4187
|
targetType,
|
|
3975
4188
|
targetServiceId: service.id,
|
|
3976
4189
|
targetServiceName: service.name,
|
|
3977
|
-
targetModel: rule.targetModel || ((
|
|
4190
|
+
targetModel: rule.targetModel || ((_j = req.body) === null || _j === void 0 ? void 0 : _j.model),
|
|
3978
4191
|
vendorId: service.vendorId,
|
|
3979
4192
|
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
3980
|
-
requestModel: (
|
|
4193
|
+
requestModel: (_k = req.body) === null || _k === void 0 ? void 0 : _k.model,
|
|
3981
4194
|
responseTime: Date.now() - startTime,
|
|
3982
4195
|
});
|
|
3983
4196
|
}
|
|
@@ -3996,7 +4209,7 @@ class ProxyServer {
|
|
|
3996
4209
|
let responseData = response.data;
|
|
3997
4210
|
if (streamRequested && response.data && typeof response.data.on === 'function' && !isEventStream) {
|
|
3998
4211
|
const raw = yield this.readStreamBody(response.data);
|
|
3999
|
-
responseData = (
|
|
4212
|
+
responseData = (_l = this.safeJsonParse(raw)) !== null && _l !== void 0 ? _l : raw;
|
|
4000
4213
|
}
|
|
4001
4214
|
// 收集响应头
|
|
4002
4215
|
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
@@ -4018,7 +4231,7 @@ class ProxyServer {
|
|
|
4018
4231
|
usageForLog = this.extractTokenUsageFromResponse(responseData, sourceType);
|
|
4019
4232
|
console.log('[Proxy] Non-stream response: extracted usageForLog:', usageForLog);
|
|
4020
4233
|
// 回写 model 字段:将上游返回的 model 改写为客户端请求时的原始模型名
|
|
4021
|
-
const originalModel = (
|
|
4234
|
+
const originalModel = (_m = req.body) === null || _m === void 0 ? void 0 : _m.model;
|
|
4022
4235
|
(0, model_rewrite_transform_1.rewriteResponseModel)(normalizedConverted, originalModel);
|
|
4023
4236
|
(0, model_rewrite_transform_1.rewriteResponseModel)(responseData, originalModel);
|
|
4024
4237
|
this.copyResponseHeaders(responseHeaders, res);
|
|
@@ -4083,10 +4296,10 @@ class ProxyServer {
|
|
|
4083
4296
|
targetType,
|
|
4084
4297
|
targetServiceId: service.id,
|
|
4085
4298
|
targetServiceName: service.name,
|
|
4086
|
-
targetModel: rule.targetModel || ((
|
|
4299
|
+
targetModel: rule.targetModel || ((_o = req.body) === null || _o === void 0 ? void 0 : _o.model),
|
|
4087
4300
|
vendorId: service.vendorId,
|
|
4088
4301
|
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
4089
|
-
requestModel: (
|
|
4302
|
+
requestModel: (_p = req.body) === null || _p === void 0 ? void 0 : _p.model,
|
|
4090
4303
|
upstreamRequest: upstreamRequestForLog,
|
|
4091
4304
|
responseTime: Date.now() - startTime,
|
|
4092
4305
|
});
|
|
@@ -4101,7 +4314,7 @@ class ProxyServer {
|
|
|
4101
4314
|
console.error('Proxy error:', error);
|
|
4102
4315
|
// 检测是否是 timeout 错误
|
|
4103
4316
|
const isTimeout = error.code === 'ECONNABORTED' ||
|
|
4104
|
-
((
|
|
4317
|
+
((_q = error.message) === null || _q === void 0 ? void 0 : _q.toLowerCase().includes('timeout')) ||
|
|
4105
4318
|
(error.errno && error.errno === 'ETIMEDOUT');
|
|
4106
4319
|
const statusCode = isTimeout ? 504 : this.getErrorStatusCode(error, 500);
|
|
4107
4320
|
const baseErrorMessage = isTimeout
|
|
@@ -4127,10 +4340,10 @@ class ProxyServer {
|
|
|
4127
4340
|
targetType,
|
|
4128
4341
|
targetServiceId: service.id,
|
|
4129
4342
|
targetServiceName: service.name,
|
|
4130
|
-
targetModel: rule.targetModel || ((
|
|
4343
|
+
targetModel: rule.targetModel || ((_r = req.body) === null || _r === void 0 ? void 0 : _r.model),
|
|
4131
4344
|
vendorId: service.vendorId,
|
|
4132
4345
|
vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
|
|
4133
|
-
requestModel: (
|
|
4346
|
+
requestModel: (_s = req.body) === null || _s === void 0 ? void 0 : _s.model,
|
|
4134
4347
|
upstreamRequest: upstreamRequestForLog,
|
|
4135
4348
|
responseHeaders: responseHeadersForLog,
|
|
4136
4349
|
responseTime: Date.now() - startTime,
|
|
@@ -4605,6 +4818,31 @@ class ProxyServer {
|
|
|
4605
4818
|
return;
|
|
4606
4819
|
}
|
|
4607
4820
|
if (isEventStream && response.data) {
|
|
4821
|
+
// ── SSE 预检:在提交响应头之前,先读取第一个 SSE 事件以检测上游错误 ──
|
|
4822
|
+
const preflightResult = yield this.preflightStream(response.data, { timeoutMs: 5000 });
|
|
4823
|
+
if (!preflightResult.healthy) {
|
|
4824
|
+
const failureInfo = preflightResult.failureInfo;
|
|
4825
|
+
console.warn(`[ApiPathProxy] Stream preflight failed: ${(failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.errorMessage) || 'unknown'}`);
|
|
4826
|
+
let errorBody = preflightResult.errorData;
|
|
4827
|
+
if (!errorBody && preflightResult.bufferedRaw.length > 0) {
|
|
4828
|
+
errorBody = this.safeJsonParse(preflightResult.bufferedRaw.toString('utf8'));
|
|
4829
|
+
}
|
|
4830
|
+
responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
|
|
4831
|
+
const errorMsg = (failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.errorMessage) || 'Stream preflight detected upstream error';
|
|
4832
|
+
yield finalizeLog((failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502, errorMsg);
|
|
4833
|
+
if (typeof response.data.destroy === 'function') {
|
|
4834
|
+
response.data.destroy();
|
|
4835
|
+
}
|
|
4836
|
+
if (failoverEnabled) {
|
|
4837
|
+
throw this.createFailoverError(errorMsg, (failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502);
|
|
4838
|
+
}
|
|
4839
|
+
res.status((failureInfo === null || failureInfo === void 0 ? void 0 : failureInfo.statusCode) || 502).json({
|
|
4840
|
+
error: { message: errorMsg, type: 'upstream_error' },
|
|
4841
|
+
});
|
|
4842
|
+
return;
|
|
4843
|
+
}
|
|
4844
|
+
// ── 预检通过:使用组合流继续管道传输 ──
|
|
4845
|
+
const streamSource = this.createPreflightCombinedStream(response.data, preflightResult.bufferedRaw);
|
|
4608
4846
|
// Stream pipeline
|
|
4609
4847
|
const parser = new streaming_1.SSEParserTransform();
|
|
4610
4848
|
const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
|
|
@@ -4646,7 +4884,7 @@ class ProxyServer {
|
|
|
4646
4884
|
return stages;
|
|
4647
4885
|
};
|
|
4648
4886
|
if (converter) {
|
|
4649
|
-
const stages = buildStages(
|
|
4887
|
+
const stages = buildStages(streamSource, parser, eventCollector, converter);
|
|
4650
4888
|
stream_1.pipeline(...stages, (error) => {
|
|
4651
4889
|
if (error) {
|
|
4652
4890
|
reject(error);
|
|
@@ -4656,7 +4894,7 @@ class ProxyServer {
|
|
|
4656
4894
|
});
|
|
4657
4895
|
}
|
|
4658
4896
|
else {
|
|
4659
|
-
const stages = buildStages(
|
|
4897
|
+
const stages = buildStages(streamSource, parser, eventCollector);
|
|
4660
4898
|
stream_1.pipeline(...stages, (error) => {
|
|
4661
4899
|
if (error) {
|
|
4662
4900
|
reject(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aicodeswitch",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.2",
|
|
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",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"prepublishOnly": "npm run build",
|
|
47
47
|
"release": "standard-version --no-changelog",
|
|
48
48
|
"tauri:dev": "tauri dev",
|
|
49
|
+
"tauri:start": "yarn build && node tauri/prepare-resources.js && cd tauri && cargo run",
|
|
49
50
|
"tauri:build": "tauri build && node tauri/move-bundle.js",
|
|
50
51
|
"tauri:icon": "tauri icon src/ui/assets/logo.png"
|
|
51
52
|
},
|