aicodeswitch 2.0.7 → 2.0.9

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/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### 2.0.9 (2026-02-02)
6
+
7
+ ### 2.0.8 (2026-02-02)
8
+
5
9
  ### 2.0.7 (2026-02-02)
6
10
 
7
11
  ### 2.0.6 (2026-02-01)
package/CLAUDE.md CHANGED
@@ -165,6 +165,12 @@ aicos version # Show current version information
165
165
  - Access logs: System access records
166
166
  - Error logs: Error and exception records
167
167
 
168
+ ### Usage Limits Auto-Sync
169
+ - **Service-Level Limits**: API services can have token and request count limits configured
170
+ - **Auto-Sync to Rules**: When an API service's usage limits are modified, all rules using that service are automatically updated with the new limits
171
+ - **Inheritance Detection**: When editing a rule, the system detects if the rule's limits match the service's limits and displays them as "inherited" (read-only)
172
+ - **Manual Override**: Rules can be configured with custom limits that differ from the service defaults
173
+
168
174
  ## Development Tips
169
175
 
170
176
  1. **Environment Variables**: Copy `.env.example` to `.env` and modify as needed
package/bin/stop.js CHANGED
@@ -18,9 +18,8 @@ const stop = async (options = {}) => {
18
18
 
19
19
  // 第一步:如果 PID 文件存在,优先通过 PID 文件停止服务器
20
20
  if (fs.existsSync(PID_FILE)) {
21
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
21
22
  try {
22
- const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
23
-
24
23
  const processInfo = await getProcessInfo(pid);
25
24
  if (!silent) {
26
25
  console.log('\n' + chalk.gray(`Process found: ${chalk.white(pid)} (${chalk.gray(processInfo)})`));
@@ -515,8 +515,47 @@ class DatabaseManager {
515
515
  if (result.changes > 0 && process.env.NODE_ENV === 'development') {
516
516
  console.log(`[DB] Updated service ${id}: ${service.name} -> ${service.apiUrl}`);
517
517
  }
518
+ // 如果更新成功,检查是否需要同步更新关联规则的超量限制
519
+ if (result.changes > 0) {
520
+ this.syncRulesWithServiceLimits(id, service);
521
+ }
518
522
  return result.changes > 0;
519
523
  }
524
+ /**
525
+ * 同步更新使用该服务的规则的超量限制
526
+ * 当API服务的超量限制修改时,自动更新所有使用该服务的规则
527
+ */
528
+ syncRulesWithServiceLimits(serviceId, service) {
529
+ // 获取所有使用该服务的规则
530
+ const rules = this.db.prepare('SELECT id FROM rules WHERE target_service_id = ?').all(serviceId);
531
+ if (rules.length === 0) {
532
+ return; // 没有规则使用此服务,无需同步
533
+ }
534
+ const now = Date.now();
535
+ const ruleIds = rules.map(r => r.id);
536
+ // Token超量限制同步
537
+ if (service.enableTokenLimit !== undefined || service.tokenLimit !== undefined ||
538
+ service.tokenResetInterval !== undefined || service.tokenResetBaseTime !== undefined) {
539
+ // 获取当前服务的最新配置
540
+ const currentService = this.db.prepare('SELECT enable_token_limit, token_limit, token_reset_interval, token_reset_base_time FROM api_services WHERE id = ?').get(serviceId);
541
+ if (currentService && currentService.enable_token_limit === 1) {
542
+ // 启用了Token超量限制,同步到所有规则
543
+ this.db.prepare('UPDATE rules SET token_limit = ?, reset_interval = ?, token_reset_base_time = ?, updated_at = ? WHERE target_service_id = ?').run(currentService.token_limit, currentService.token_reset_interval, currentService.token_reset_base_time, now, serviceId);
544
+ console.log(`[DB] Synced token limits for ${ruleIds.length} rule(s) using service ${serviceId}`);
545
+ }
546
+ }
547
+ // 请求次数超量限制同步
548
+ if (service.enableRequestLimit !== undefined || service.requestCountLimit !== undefined ||
549
+ service.requestResetInterval !== undefined || service.requestResetBaseTime !== undefined) {
550
+ // 获取当前服务的最新配置
551
+ const currentService = this.db.prepare('SELECT enable_request_limit, request_count_limit, request_reset_interval, request_reset_base_time FROM api_services WHERE id = ?').get(serviceId);
552
+ if (currentService && currentService.enable_request_limit === 1) {
553
+ // 启用了请求次数超量限制,同步到所有规则
554
+ this.db.prepare('UPDATE rules SET request_count_limit = ?, request_reset_interval = ?, request_reset_base_time = ?, updated_at = ? WHERE target_service_id = ?').run(currentService.request_count_limit, currentService.request_reset_interval, currentService.request_reset_base_time, now, serviceId);
555
+ console.log(`[DB] Synced request count limits for ${ruleIds.length} rule(s) using service ${serviceId}`);
556
+ }
557
+ }
558
+ }
520
559
  deleteAPIService(id) {
521
560
  const result = this.db.prepare('DELETE FROM api_services WHERE id = ?').run(id);
522
561
  return result.changes > 0;
@@ -27,13 +27,15 @@ const version_check_1 = require("./version-check");
27
27
  const utils_1 = require("./utils");
28
28
  const config_metadata_1 = require("./config-metadata");
29
29
  const config_1 = require("./config");
30
- const dotenvPath = path_1.default.resolve(os_1.default.homedir(), '.aicodeswitch/aicodeswitch.conf');
30
+ const appDir = path_1.default.join(os_1.default.homedir(), '.aicodeswitch');
31
+ const dataDir = path_1.default.join(appDir, 'data');
32
+ const dotenvPath = path_1.default.resolve(appDir, 'aicodeswitch.conf');
33
+ const migrationHashFilePath = path_1.default.join(appDir, 'migration-hash');
31
34
  if (fs_1.default.existsSync(dotenvPath)) {
32
35
  dotenv_1.default.config({ path: dotenvPath });
33
36
  }
34
37
  const host = process.env.HOST || '127.0.0.1';
35
38
  const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
36
- const dataDir = process.env.DATA_DIR ? path_1.default.resolve(process.cwd(), process.env.DATA_DIR) : path_1.default.join(os_1.default.homedir(), '.aicodeswitch/data');
37
39
  let globalProxyConfig = null;
38
40
  function updateProxyConfig(config) {
39
41
  if (config.proxyEnabled && config.proxyUrl) {
@@ -67,8 +69,8 @@ function getProxyAgent() {
67
69
  }
68
70
  const app = (0, express_1.default)();
69
71
  app.use((0, cors_1.default)());
70
- app.use(express_1.default.json({ limit: '10mb' }));
71
- app.use(express_1.default.urlencoded({ extended: true }));
72
+ app.use(express_1.default.json({ limit: 'Infinity' }));
73
+ app.use(express_1.default.urlencoded({ extended: true, limit: 'Infinity' }));
72
74
  const asyncHandler = (handler) => (req, res, next) => {
73
75
  Promise.resolve(handler(req, res, next)).catch(next);
74
76
  };
@@ -116,16 +118,17 @@ const writeClaudeConfig = (dbManager) => __awaiter(void 0, void 0, void 0, funct
116
118
  fs_1.default.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2));
117
119
  // Claude Code .claude.json
118
120
  const claudeJsonPath = path_1.default.join(homeDir, '.claude.json');
119
- // 同样处理 .claude.json 的备份
121
+ // 先读取原文件内容(如果存在)
122
+ let claudeJson = {};
123
+ if (fs_1.default.existsSync(claudeJsonPath)) {
124
+ claudeJson = JSON.parse(fs_1.default.readFileSync(claudeJsonPath, 'utf8'));
125
+ }
126
+ // 然后处理备份
120
127
  if (!fs_1.default.existsSync(claudeJsonBakPath)) {
121
128
  if (fs_1.default.existsSync(claudeJsonPath)) {
122
129
  fs_1.default.renameSync(claudeJsonPath, claudeJsonBakPath);
123
130
  }
124
131
  }
125
- let claudeJson = {};
126
- if (fs_1.default.existsSync(claudeJsonPath)) {
127
- claudeJson = JSON.parse(fs_1.default.readFileSync(claudeJsonPath, 'utf8'));
128
- }
129
132
  claudeJson.hasCompletedOnboarding = true;
130
133
  fs_1.default.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
131
134
  // 保存元数据
@@ -1288,8 +1291,6 @@ ${instruction}
1288
1291
  const text = yield resp.text();
1289
1292
  res.type('text/plain').send(text);
1290
1293
  })));
1291
- // Migration 相关端点
1292
- const getMigrationHashPath = () => path_1.default.join(dataDir, '.migration-hash');
1293
1294
  // 查找 migration.md 文件的路径
1294
1295
  const findMigrationPath = () => {
1295
1296
  // 可能的路径列表
@@ -1314,20 +1315,18 @@ ${instruction}
1314
1315
  res.json({ shouldShow: false, content: '' });
1315
1316
  return;
1316
1317
  }
1317
- const content = fs_1.default.readFileSync(migrationPath, 'utf-8');
1318
+ const content = fs_1.default.readFileSync(migrationPath, 'utf-8').trim();
1318
1319
  // 计算当前内容的 hash
1319
1320
  const currentHash = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
1320
- // 读取之前保存的 hash
1321
- const hashPath = getMigrationHashPath();
1322
1321
  // 如果 hash 文件不存在,说明是第一次安装
1323
- if (!fs_1.default.existsSync(hashPath)) {
1322
+ if (!fs_1.default.existsSync(migrationHashFilePath)) {
1324
1323
  // 第一次安装,直接保存当前 hash,不显示弹窗
1325
- fs_1.default.writeFileSync(hashPath, currentHash, 'utf-8');
1324
+ fs_1.default.writeFileSync(migrationHashFilePath, currentHash, 'utf-8');
1326
1325
  res.json({ shouldShow: false, content: '' });
1327
1326
  return;
1328
1327
  }
1329
1328
  // 读取已保存的 hash
1330
- const savedHash = fs_1.default.readFileSync(hashPath, 'utf-8').trim();
1329
+ const savedHash = fs_1.default.readFileSync(migrationHashFilePath, 'utf-8').trim();
1331
1330
  // 如果 hash 不同,需要显示弹窗
1332
1331
  const shouldShow = savedHash !== currentHash;
1333
1332
  res.json({ shouldShow, content: shouldShow ? content : '' });
@@ -1345,11 +1344,10 @@ ${instruction}
1345
1344
  res.json({ success: false });
1346
1345
  return;
1347
1346
  }
1348
- const content = fs_1.default.readFileSync(migrationPath, 'utf-8');
1347
+ const content = fs_1.default.readFileSync(migrationPath, 'utf-8').trim();
1349
1348
  const hash = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
1350
1349
  // 保存 hash 到文件
1351
- const hashPath = getMigrationHashPath();
1352
- fs_1.default.writeFileSync(hashPath, hash, 'utf-8');
1350
+ fs_1.default.writeFileSync(migrationHashFilePath, hash, 'utf-8');
1353
1351
  res.json({ success: true });
1354
1352
  }
1355
1353
  catch (error) {
@@ -1393,6 +1391,16 @@ app.use((err, _req, res, _next) => {
1393
1391
  console.error(err);
1394
1392
  res.status(500).json({ error: err.message || 'Internal server error' });
1395
1393
  });
1394
+ // 全局未捕获异常处理 - 防止服务崩溃
1395
+ process.on('uncaughtException', (error) => {
1396
+ console.error('[Uncaught Exception] 服务遇到未捕获的异常:', error);
1397
+ console.error('[Uncaught Exception] 堆栈信息:', error.stack);
1398
+ // 不退出进程,继续运行
1399
+ });
1400
+ process.on('unhandledRejection', (reason) => {
1401
+ console.error('[Unhandled Rejection] 服务遇到未处理的 Promise 拒绝:', reason);
1402
+ // 不退出进程,继续运行
1403
+ });
1396
1404
  start().catch((error) => {
1397
1405
  console.error('Failed to start server:', error);
1398
1406
  process.exit(1);
@@ -643,8 +643,8 @@ class ProxyServer {
643
643
  // 检查并重置到期的规则
644
644
  this.dbManager.checkAndResetRuleIfNeeded(rule.id);
645
645
  this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
646
- // 检查token限制
647
- if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
646
+ // 检查token限制(tokenLimit单位是k,需要乘以1000转换为实际token数)
647
+ if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit * 1000) {
648
648
  continue; // 跳过超限规则
649
649
  }
650
650
  // 检查请求次数限制
@@ -666,8 +666,8 @@ class ProxyServer {
666
666
  // 检查并重置到期的规则
667
667
  this.dbManager.checkAndResetRuleIfNeeded(rule.id);
668
668
  this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
669
- // 检查token限制
670
- if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
669
+ // 检查token限制(tokenLimit单位是k,需要乘以1000转换为实际token数)
670
+ if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit * 1000) {
671
671
  continue; // 跳过超限规则
672
672
  }
673
673
  // 检查请求次数限制
@@ -687,8 +687,8 @@ class ProxyServer {
687
687
  // 检查并重置到期的规则
688
688
  this.dbManager.checkAndResetRuleIfNeeded(rule.id);
689
689
  this.dbManager.checkAndResetRequestCountIfNeeded(rule.id);
690
- // 检查token限制
691
- if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit) {
690
+ // 检查token限制(tokenLimit单位是k,需要乘以1000转换为实际token数)
691
+ if (rule.tokenLimit && rule.totalTokensUsed !== undefined && rule.totalTokensUsed >= rule.tokenLimit * 1000) {
692
692
  continue; // 跳过超限规则
693
693
  }
694
694
  // 检查请求次数限制
@@ -729,9 +729,9 @@ class ProxyServer {
729
729
  // 5. 过滤掉超过限制的规则(仅在有多个候选规则时)
730
730
  if (candidates.length > 1) {
731
731
  const filteredCandidates = candidates.filter(rule => {
732
- // 检查token限制
732
+ // 检查token限制(tokenLimit单位是k,需要乘以1000转换为实际token数)
733
733
  if (rule.tokenLimit && rule.totalTokensUsed !== undefined) {
734
- if (rule.totalTokensUsed >= rule.tokenLimit) {
734
+ if (rule.totalTokensUsed >= rule.tokenLimit * 1000) {
735
735
  return false;
736
736
  }
737
737
  }
@@ -1535,11 +1535,49 @@ class ProxyServer {
1535
1535
  streamChunksForLog = eventCollector.getChunks();
1536
1536
  void finalizeLog(res.statusCode);
1537
1537
  });
1538
- (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => {
1538
+ // 监听 res 的错误事件
1539
+ res.on('error', (err) => {
1540
+ console.error('[Proxy] Response stream error:', err);
1541
+ });
1542
+ (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
1539
1543
  if (error) {
1540
- void finalizeLog(500, error.message);
1544
+ console.error('[Proxy] Pipeline error for claude-code:', error);
1545
+ // 记录到错误日志
1546
+ try {
1547
+ yield this.dbManager.addErrorLog({
1548
+ timestamp: Date.now(),
1549
+ method: req.method,
1550
+ path: req.path,
1551
+ statusCode: 500,
1552
+ errorMessage: error.message || 'Stream processing error',
1553
+ errorStack: error.stack,
1554
+ requestHeaders: this.normalizeHeaders(req.headers),
1555
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
1556
+ });
1557
+ }
1558
+ catch (logError) {
1559
+ console.error('[Proxy] Failed to log error:', logError);
1560
+ }
1561
+ // 尝试向客户端发送错误事件
1562
+ try {
1563
+ if (!res.writableEnded) {
1564
+ const errorEvent = `event: error\ndata: ${JSON.stringify({
1565
+ type: 'error',
1566
+ error: {
1567
+ type: 'api_error',
1568
+ message: 'Stream processing error occurred'
1569
+ }
1570
+ })}\n\n`;
1571
+ res.write(errorEvent);
1572
+ res.end();
1573
+ }
1574
+ }
1575
+ catch (writeError) {
1576
+ console.error('[Proxy] Failed to send error event:', writeError);
1577
+ }
1578
+ yield finalizeLog(500, error.message);
1541
1579
  }
1542
- });
1580
+ }));
1543
1581
  return;
1544
1582
  }
1545
1583
  if (targetType === 'codex' && this.isClaudeSource(sourceType)) {
@@ -1566,17 +1604,55 @@ class ProxyServer {
1566
1604
  streamChunksForLog = eventCollector.getChunks();
1567
1605
  void finalizeLog(res.statusCode);
1568
1606
  });
1569
- (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => {
1607
+ // 监听 res 的错误事件
1608
+ res.on('error', (err) => {
1609
+ console.error('[Proxy] Response stream error:', err);
1610
+ });
1611
+ (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
1570
1612
  if (error) {
1571
- void finalizeLog(500, error.message);
1613
+ console.error('[Proxy] Pipeline error for codex:', error);
1614
+ // 记录到错误日志
1615
+ try {
1616
+ yield this.dbManager.addErrorLog({
1617
+ timestamp: Date.now(),
1618
+ method: req.method,
1619
+ path: req.path,
1620
+ statusCode: 500,
1621
+ errorMessage: error.message || 'Stream processing error',
1622
+ errorStack: error.stack,
1623
+ requestHeaders: this.normalizeHeaders(req.headers),
1624
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
1625
+ });
1626
+ }
1627
+ catch (logError) {
1628
+ console.error('[Proxy] Failed to log error:', logError);
1629
+ }
1630
+ // 尝试向客户端发送错误事件
1631
+ try {
1632
+ if (!res.writableEnded) {
1633
+ const errorEvent = `data: ${JSON.stringify({
1634
+ error: 'Stream processing error occurred'
1635
+ })}\n\n`;
1636
+ res.write(errorEvent);
1637
+ res.end();
1638
+ }
1639
+ }
1640
+ catch (writeError) {
1641
+ console.error('[Proxy] Failed to send error event:', writeError);
1642
+ }
1643
+ yield finalizeLog(500, error.message);
1572
1644
  }
1573
- });
1645
+ }));
1574
1646
  return;
1575
1647
  }
1576
1648
  // 默认stream处理(无转换)
1577
1649
  const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
1578
1650
  responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
1579
1651
  this.copyResponseHeaders(responseHeaders, res);
1652
+ // 监听 res 的错误事件
1653
+ res.on('error', (err) => {
1654
+ console.error('[Proxy] Response stream error:', err);
1655
+ });
1580
1656
  res.on('finish', () => {
1581
1657
  streamChunksForLog = eventCollector.getChunks();
1582
1658
  // 尝试从event collector中提取usage信息
@@ -1586,11 +1662,28 @@ class ProxyServer {
1586
1662
  }
1587
1663
  void finalizeLog(res.statusCode);
1588
1664
  });
1589
- (0, stream_1.pipeline)(response.data, eventCollector, res, (error) => {
1665
+ (0, stream_1.pipeline)(response.data, eventCollector, res, (error) => __awaiter(this, void 0, void 0, function* () {
1590
1666
  if (error) {
1591
- void finalizeLog(500, error.message);
1667
+ console.error('[Proxy] Pipeline error (default stream):', error);
1668
+ // 记录到错误日志
1669
+ try {
1670
+ yield this.dbManager.addErrorLog({
1671
+ timestamp: Date.now(),
1672
+ method: req.method,
1673
+ path: req.path,
1674
+ statusCode: 500,
1675
+ errorMessage: error.message || 'Stream processing error',
1676
+ errorStack: error.stack,
1677
+ requestHeaders: this.normalizeHeaders(req.headers),
1678
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
1679
+ });
1680
+ }
1681
+ catch (logError) {
1682
+ console.error('[Proxy] Failed to log error:', logError);
1683
+ }
1684
+ yield finalizeLog(500, error.message);
1592
1685
  }
1593
- });
1686
+ }));
1594
1687
  return;
1595
1688
  }
1596
1689
  let responseData = response.data;
@@ -8,20 +8,45 @@ const stream_1 = require("stream");
8
8
  */
9
9
  class ChunkCollectorTransform extends stream_1.Transform {
10
10
  constructor() {
11
- super();
11
+ super({ writableObjectMode: true, readableObjectMode: true });
12
12
  Object.defineProperty(this, "chunks", {
13
13
  enumerable: true,
14
14
  configurable: true,
15
15
  writable: true,
16
16
  value: []
17
17
  });
18
+ Object.defineProperty(this, "errorEmitted", {
19
+ enumerable: true,
20
+ configurable: true,
21
+ writable: true,
22
+ value: false
23
+ });
24
+ this.on('error', (err) => {
25
+ console.error('[ChunkCollectorTransform] Stream error:', err);
26
+ this.errorEmitted = true;
27
+ });
18
28
  }
19
29
  _transform(chunk, _encoding, callback) {
20
- // 收集chunk数据
21
- this.chunks.push(chunk.toString('utf8'));
22
- // 将chunk传递给下一个stream
23
- this.push(chunk);
24
- callback();
30
+ if (this.errorEmitted) {
31
+ callback();
32
+ return;
33
+ }
34
+ try {
35
+ // 收集chunk数据 - 支持对象和Buffer/string
36
+ if (typeof chunk === 'object' && chunk !== null && !Buffer.isBuffer(chunk)) {
37
+ this.chunks.push(JSON.stringify(chunk));
38
+ }
39
+ else {
40
+ this.chunks.push(chunk.toString('utf8'));
41
+ }
42
+ // 将chunk传递给下一个stream
43
+ this.push(chunk);
44
+ callback();
45
+ }
46
+ catch (error) {
47
+ console.error('[ChunkCollectorTransform] Error in _transform:', error);
48
+ callback();
49
+ }
25
50
  }
26
51
  /**
27
52
  * 获取收集的所有chunks
@@ -44,7 +69,7 @@ exports.ChunkCollectorTransform = ChunkCollectorTransform;
44
69
  */
45
70
  class SSEEventCollectorTransform extends stream_1.Transform {
46
71
  constructor() {
47
- super();
72
+ super({ writableObjectMode: true, readableObjectMode: true });
48
73
  Object.defineProperty(this, "buffer", {
49
74
  enumerable: true,
50
75
  configurable: true,
@@ -66,22 +91,82 @@ class SSEEventCollectorTransform extends stream_1.Transform {
66
91
  writable: true,
67
92
  value: []
68
93
  });
94
+ Object.defineProperty(this, "errorEmitted", {
95
+ enumerable: true,
96
+ configurable: true,
97
+ writable: true,
98
+ value: false
99
+ });
100
+ this.on('error', (err) => {
101
+ console.error('[SSEEventCollectorTransform] Stream error:', err);
102
+ this.errorEmitted = true;
103
+ });
69
104
  }
70
105
  _transform(chunk, _encoding, callback) {
71
- this.buffer += chunk.toString('utf8');
72
- this.processBuffer();
73
- // 将chunk传递给下一个stream
74
- this.push(chunk);
75
- callback();
106
+ if (this.errorEmitted) {
107
+ callback();
108
+ return;
109
+ }
110
+ try {
111
+ // 如果是对象(来自 SSEParserTransform),先转换为字符串格式进行处理
112
+ if (typeof chunk === 'object' && chunk !== null) {
113
+ const sseEvent = chunk;
114
+ const lines = [];
115
+ if (sseEvent.event)
116
+ lines.push(`event: ${sseEvent.event}`);
117
+ if (sseEvent.id)
118
+ lines.push(`id: ${sseEvent.id}`);
119
+ if (sseEvent.data !== undefined) {
120
+ if (typeof sseEvent.data === 'string') {
121
+ lines.push(`data: ${sseEvent.data}`);
122
+ }
123
+ else {
124
+ lines.push(`data: ${JSON.stringify(sseEvent.data)}`);
125
+ }
126
+ }
127
+ if (lines.length > 0) {
128
+ this.currentEvent.rawLines.push(...lines);
129
+ if (sseEvent.event)
130
+ this.currentEvent.event = sseEvent.event;
131
+ if (sseEvent.id)
132
+ this.currentEvent.id = sseEvent.id;
133
+ if (sseEvent.data !== undefined) {
134
+ const dataStr = typeof sseEvent.data === 'string' ? sseEvent.data : JSON.stringify(sseEvent.data);
135
+ this.currentEvent.dataLines.push(dataStr);
136
+ }
137
+ this.flushEvent();
138
+ }
139
+ // 将原始对象传递给下一个stream
140
+ this.push(chunk);
141
+ }
142
+ else {
143
+ // Buffer/string 模式
144
+ this.buffer += chunk.toString('utf8');
145
+ this.processBuffer();
146
+ // 将chunk传递给下一个stream
147
+ this.push(chunk);
148
+ }
149
+ callback();
150
+ }
151
+ catch (error) {
152
+ console.error('[SSEEventCollectorTransform] Error in _transform:', error);
153
+ callback();
154
+ }
76
155
  }
77
156
  _flush(callback) {
78
- // 处理剩余的buffer
79
- if (this.buffer.trim()) {
80
- this.processBuffer();
157
+ try {
158
+ // 处理剩余的buffer
159
+ if (this.buffer.trim()) {
160
+ this.processBuffer();
161
+ }
162
+ // 刷新最后一个事件
163
+ this.flushEvent();
164
+ callback();
165
+ }
166
+ catch (error) {
167
+ console.error('[SSEEventCollectorTransform] Error in _flush:', error);
168
+ callback();
81
169
  }
82
- // 刷新最后一个事件
83
- this.flushEvent();
84
- callback();
85
170
  }
86
171
  processBuffer() {
87
172
  const lines = this.buffer.split('\n');