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.
@@ -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 = (_d = req.body) === null || _d === void 0 ? void 0 : _d.model;
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 = [response.data, parser, eventCollector, converter];
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 = [response.data, parser, eventCollector];
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 || ((_e = req.body) === null || _e === void 0 ? void 0 : _e.model),
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: (_f = req.body) === null || _f === void 0 ? void 0 : _f.model,
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 || ((_g = req.body) === null || _g === void 0 ? void 0 : _g.model),
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: (_h = req.body) === null || _h === void 0 ? void 0 : _h.model,
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 = (_j = this.safeJsonParse(raw)) !== null && _j !== void 0 ? _j : raw;
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 = (_k = req.body) === null || _k === void 0 ? void 0 : _k.model;
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 || ((_l = req.body) === null || _l === void 0 ? void 0 : _l.model),
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: (_m = req.body) === null || _m === void 0 ? void 0 : _m.model,
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
- ((_o = error.message) === null || _o === void 0 ? void 0 : _o.toLowerCase().includes('timeout')) ||
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 || ((_p = req.body) === null || _p === void 0 ? void 0 : _p.model),
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: (_q = req.body) === null || _q === void 0 ? void 0 : _q.model,
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(response.data, parser, eventCollector, converter);
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(response.data, parser, eventCollector);
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.1",
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
  },