aicodeswitch 2.0.9 → 2.0.10

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,8 @@
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.10 (2026-02-03)
6
+
5
7
  ### 2.0.9 (2026-02-02)
6
8
 
7
9
  ### 2.0.8 (2026-02-02)
package/CLAUDE.md CHANGED
@@ -163,7 +163,15 @@ aicos version # Show current version information
163
163
  ### Logging
164
164
  - Request logs: Detailed API call records with token usage
165
165
  - Access logs: System access records
166
- - Error logs: Error and exception records
166
+ - Error logs: Error and exception records (includes upstream request information when available)
167
+ - **Session Management**:
168
+ - Tracks user sessions based on session ID (Claude Code: `metadata.user_id`, Codex: `headers.session_id`)
169
+ - Auto-generates session title from first user message content:
170
+ - Extracts text from first user message
171
+ - Cleans up whitespace and newlines
172
+ - Intelligently truncates at word boundaries (max 100 chars)
173
+ - Adds "..." for truncated titles
174
+ - Records first request time, last request time, request count, and total tokens per session
167
175
 
168
176
  ### Usage Limits Auto-Sync
169
177
  - **Service-Level Limits**: API services can have token and request count limits configured
@@ -203,3 +211,4 @@ aicos version # Show current version information
203
211
  * 所有对话请使用中文。生成代码中的文案及相关注释根据代码原本的语言生成。
204
212
  * 在服务端,直接使用 __dirname 来获取当前目录,不要使用 process.cwd()
205
213
  * 每次有新的变化时,你需要更新 CLAUDE.md 来让文档保持最新。
214
+ * 禁止在项目中使用依赖GPU的css样式处理。
@@ -264,6 +264,13 @@ class DatabaseManager {
264
264
  this.db.exec('ALTER TABLE api_services ADD COLUMN auth_type TEXT DEFAULT NULL;');
265
265
  console.log('[DB] Migration completed: auth_type column added');
266
266
  }
267
+ // 检查rules表是否有is_disabled字段(临时屏蔽功能)
268
+ const hasIsDisabled = rulesColumns.some((col) => col.name === 'is_disabled');
269
+ if (!hasIsDisabled) {
270
+ console.log('[DB] Running migration: Adding is_disabled column to rules table');
271
+ this.db.exec('ALTER TABLE rules ADD COLUMN is_disabled INTEGER DEFAULT 0;');
272
+ console.log('[DB] Migration completed: is_disabled column added');
273
+ }
267
274
  });
268
275
  }
269
276
  migrateMaxOutputTokensToModelLimits() {
@@ -378,10 +385,13 @@ class DatabaseManager {
378
385
  total_tokens_used INTEGER DEFAULT 0,
379
386
  reset_interval INTEGER,
380
387
  last_reset_at INTEGER,
388
+ token_reset_base_time INTEGER,
381
389
  request_count_limit INTEGER,
382
390
  total_requests_used INTEGER DEFAULT 0,
383
391
  request_reset_interval INTEGER,
384
392
  request_last_reset_at INTEGER,
393
+ request_reset_base_time INTEGER,
394
+ is_disabled INTEGER DEFAULT 0,
385
395
  created_at INTEGER NOT NULL,
386
396
  updated_at INTEGER NOT NULL,
387
397
  FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE,
@@ -631,6 +641,7 @@ class DatabaseManager {
631
641
  requestResetInterval: row.request_reset_interval,
632
642
  requestLastResetAt: row.request_last_reset_at,
633
643
  requestResetBaseTime: row.request_reset_base_time,
644
+ isDisabled: row.is_disabled === 1,
634
645
  createdAt: row.created_at,
635
646
  updatedAt: row.updated_at,
636
647
  }));
@@ -658,6 +669,7 @@ class DatabaseManager {
658
669
  requestResetInterval: row.request_reset_interval,
659
670
  requestLastResetAt: row.request_last_reset_at,
660
671
  requestResetBaseTime: row.request_reset_base_time,
672
+ isDisabled: row.is_disabled === 1,
661
673
  createdAt: row.created_at,
662
674
  updatedAt: row.updated_at,
663
675
  };
@@ -666,21 +678,41 @@ class DatabaseManager {
666
678
  const id = crypto_1.default.randomUUID();
667
679
  const now = Date.now();
668
680
  this.db
669
- .prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, timeout, token_limit, total_tokens_used, reset_interval, last_reset_at, token_reset_base_time, request_count_limit, total_requests_used, request_reset_interval, request_last_reset_at, request_reset_base_time, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
670
- .run(id, route.routeId, route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, route.timeout || null, route.tokenLimit || null, route.totalTokensUsed || 0, route.resetInterval || null, route.lastResetAt || null, route.tokenResetBaseTime || null, route.requestCountLimit || null, route.totalRequestsUsed || 0, route.requestResetInterval || null, route.requestLastResetAt || null, route.requestResetBaseTime || null, now, now);
681
+ .prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, timeout, token_limit, total_tokens_used, reset_interval, last_reset_at, token_reset_base_time, request_count_limit, total_requests_used, request_reset_interval, request_last_reset_at, request_reset_base_time, is_disabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
682
+ .run(id, route.routeId, route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, route.timeout || null, route.tokenLimit || null, route.totalTokensUsed || 0, route.resetInterval || null, route.lastResetAt || null, route.tokenResetBaseTime || null, route.requestCountLimit || null, route.totalRequestsUsed || 0, route.requestResetInterval || null, route.requestLastResetAt || null, route.requestResetBaseTime || null, route.isDisabled ? 1 : 0, now, now);
671
683
  return Object.assign(Object.assign({}, route), { id, createdAt: now, updatedAt: now });
672
684
  }
673
685
  updateRule(id, route) {
674
686
  const now = Date.now();
675
687
  const result = this.db
676
- .prepare('UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, replaced_model = ?, sort_order = ?, timeout = ?, token_limit = ?, reset_interval = ?, token_reset_base_time = ?, request_count_limit = ?, request_reset_interval = ?, request_reset_base_time = ?, updated_at = ? WHERE id = ?')
677
- .run(route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, route.timeout !== undefined ? route.timeout : null, route.tokenLimit !== undefined ? route.tokenLimit : null, route.resetInterval !== undefined ? route.resetInterval : null, route.tokenResetBaseTime !== undefined ? route.tokenResetBaseTime : null, route.requestCountLimit !== undefined ? route.requestCountLimit : null, route.requestResetInterval !== undefined ? route.requestResetInterval : null, route.requestResetBaseTime !== undefined ? route.requestResetBaseTime : null, now, id);
688
+ .prepare('UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, replaced_model = ?, sort_order = ?, timeout = ?, token_limit = ?, reset_interval = ?, token_reset_base_time = ?, request_count_limit = ?, request_reset_interval = ?, request_reset_base_time = ?, is_disabled = ?, updated_at = ? WHERE id = ?')
689
+ .run(route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, route.timeout !== undefined ? route.timeout : null, route.tokenLimit !== undefined ? route.tokenLimit : null, route.resetInterval !== undefined ? route.resetInterval : null, route.tokenResetBaseTime !== undefined ? route.tokenResetBaseTime : null, route.requestCountLimit !== undefined ? route.requestCountLimit : null, route.requestResetInterval !== undefined ? route.requestResetInterval : null, route.requestResetBaseTime !== undefined ? route.requestResetBaseTime : null, route.isDisabled !== undefined ? (route.isDisabled ? 1 : 0) : null, now, id);
678
690
  return result.changes > 0;
679
691
  }
680
692
  deleteRule(id) {
681
693
  const result = this.db.prepare('DELETE FROM rules WHERE id = ?').run(id);
682
694
  return result.changes > 0;
683
695
  }
696
+ /**
697
+ * 切换规则的临时屏蔽状态
698
+ * @param ruleId 规则ID
699
+ * @returns 是否成功
700
+ */
701
+ toggleRuleDisabled(ruleId) {
702
+ const rule = this.getRule(ruleId);
703
+ if (!rule) {
704
+ return { success: false, isDisabled: false };
705
+ }
706
+ const now = Date.now();
707
+ const newDisabledState = !rule.isDisabled;
708
+ const result = this.db
709
+ .prepare('UPDATE rules SET is_disabled = ?, updated_at = ? WHERE id = ?')
710
+ .run(newDisabledState ? 1 : 0, now, ruleId);
711
+ return {
712
+ success: result.changes > 0,
713
+ isDisabled: newDisabledState
714
+ };
715
+ }
684
716
  /**
685
717
  * 增加规则的token使用量
686
718
  * @param ruleId 规则ID
@@ -1134,8 +1166,8 @@ class DatabaseManager {
1134
1166
  // Import rules
1135
1167
  for (const rule of importData.rules) {
1136
1168
  this.db
1137
- .prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, timeout, token_limit, total_tokens_used, reset_interval, last_reset_at, token_reset_base_time, request_count_limit, total_requests_used, request_reset_interval, request_last_reset_at, request_reset_base_time, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
1138
- .run(rule.id, rule.routeId, rule.contentType || 'default', rule.targetServiceId, rule.targetModel || null, rule.replacedModel || null, rule.sortOrder || 0, rule.timeout || null, rule.tokenLimit || null, rule.totalTokensUsed || 0, rule.resetInterval || null, rule.lastResetAt || null, rule.tokenResetBaseTime || null, rule.requestCountLimit || null, rule.totalRequestsUsed || 0, rule.requestResetInterval || null, rule.requestLastResetAt || null, rule.requestResetBaseTime || null, rule.createdAt, rule.updatedAt);
1169
+ .prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, timeout, token_limit, total_tokens_used, reset_interval, last_reset_at, token_reset_base_time, request_count_limit, total_requests_used, request_reset_interval, request_last_reset_at, request_reset_base_time, is_disabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
1170
+ .run(rule.id, rule.routeId, rule.contentType || 'default', rule.targetServiceId, rule.targetModel || null, rule.replacedModel || null, rule.sortOrder || 0, rule.timeout || null, rule.tokenLimit || null, rule.totalTokensUsed || 0, rule.resetInterval || null, rule.lastResetAt || null, rule.tokenResetBaseTime || null, rule.requestCountLimit || null, rule.totalRequestsUsed || 0, rule.requestResetInterval || null, rule.requestLastResetAt || null, rule.requestResetBaseTime || null, rule.isDisabled ? 1 : 0, rule.createdAt, rule.updatedAt);
1139
1171
  }
1140
1172
  // Update config
1141
1173
  this.updateConfig(importData.config);
@@ -760,6 +760,7 @@ const registerRoutes = (dbManager, proxyServer) => {
760
760
  app.delete('/api/rules/:id', (req, res) => res.json(dbManager.deleteRule(req.params.id)));
761
761
  app.put('/api/rules/:id/reset-tokens', (req, res) => res.json(dbManager.resetRuleTokenUsage(req.params.id)));
762
762
  app.put('/api/rules/:id/reset-requests', (req, res) => res.json(dbManager.resetRuleRequestCount(req.params.id)));
763
+ app.put('/api/rules/:id/toggle-disable', (req, res) => res.json(dbManager.toggleRuleDisabled(req.params.id)));
763
764
  // 解除规则的黑名单状态
764
765
  app.put('/api/rules/:id/clear-blacklist', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
765
766
  const { id } = req.params;
@@ -627,6 +627,10 @@ class ProxyServer {
627
627
  const rules = this.getRulesByRouteId(routeId);
628
628
  if (!rules || rules.length === 0)
629
629
  return undefined;
630
+ // 过滤掉被屏蔽的规则
631
+ const enabledRules = rules.filter(rule => !rule.isDisabled);
632
+ if (enabledRules.length === 0)
633
+ return undefined;
630
634
  const body = req.body;
631
635
  const requestModel = body === null || body === void 0 ? void 0 : body.model;
632
636
  // 1. 首先查找 model-mapping 类型的规则,按 sortOrder 降序匹配
@@ -704,22 +708,26 @@ class ProxyServer {
704
708
  const rules = this.getRulesByRouteId(routeId);
705
709
  if (!rules || rules.length === 0)
706
710
  return [];
711
+ // 过滤掉被屏蔽的规则
712
+ const enabledRules = rules.filter(rule => !rule.isDisabled);
713
+ if (enabledRules.length === 0)
714
+ return [];
707
715
  const body = req.body;
708
716
  const requestModel = body === null || body === void 0 ? void 0 : body.model;
709
717
  const candidates = [];
710
718
  // 1. Model mapping rules
711
719
  if (requestModel) {
712
- const modelMappingRules = rules.filter(rule => rule.contentType === 'model-mapping' &&
720
+ const modelMappingRules = enabledRules.filter(rule => rule.contentType === 'model-mapping' &&
713
721
  rule.replacedModel &&
714
722
  requestModel.includes(rule.replacedModel));
715
723
  candidates.push(...modelMappingRules);
716
724
  }
717
725
  // 2. Content type specific rules
718
726
  const contentType = this.determineContentType(req);
719
- const contentTypeRules = rules.filter(rule => rule.contentType === contentType);
727
+ const contentTypeRules = enabledRules.filter(rule => rule.contentType === contentType);
720
728
  candidates.push(...contentTypeRules);
721
729
  // 3. Default rules
722
- const defaultRules = rules.filter(rule => rule.contentType === 'default');
730
+ const defaultRules = enabledRules.filter(rule => rule.contentType === 'default');
723
731
  candidates.push(...defaultRules);
724
732
  // 4. 检查并重置到期的规则
725
733
  candidates.forEach(rule => {
@@ -1239,6 +1247,7 @@ class ProxyServer {
1239
1247
  /**
1240
1248
  * 提取会话标题(默认方法)
1241
1249
  * 对于新会话,尝试从第一条消息的内容中提取标题
1250
+ * 优化:使用第一条用户消息的完整内容,并智能截取
1242
1251
  */
1243
1252
  defaultExtractSessionTitle(request, sessionId) {
1244
1253
  var _a;
@@ -1254,21 +1263,51 @@ class ProxyServer {
1254
1263
  const firstUserMessage = messages.find((msg) => msg.role === 'user');
1255
1264
  if (firstUserMessage) {
1256
1265
  const content = firstUserMessage.content;
1266
+ let rawText = '';
1257
1267
  if (typeof content === 'string') {
1258
- // 截取前50个字符作为标题
1259
- return content.slice(0, 50).trim();
1268
+ rawText = content;
1260
1269
  }
1261
1270
  else if (Array.isArray(content)) {
1262
1271
  // 处理结构化内容(如图片+文本)
1263
1272
  const textBlock = content.find((block) => (block === null || block === void 0 ? void 0 : block.type) === 'text');
1264
1273
  if (textBlock === null || textBlock === void 0 ? void 0 : textBlock.text) {
1265
- return textBlock.text.slice(0, 50).trim();
1274
+ rawText = textBlock.text;
1266
1275
  }
1267
1276
  }
1277
+ if (rawText) {
1278
+ return this.formatSessionTitle(rawText);
1279
+ }
1268
1280
  }
1269
1281
  }
1270
1282
  return undefined;
1271
1283
  }
1284
+ /**
1285
+ * 格式化会话标题
1286
+ * - 去除多余空白和换行符
1287
+ * - 智能截取,在单词边界处截断
1288
+ * - 限制最大长度为100个字符
1289
+ */
1290
+ formatSessionTitle(text) {
1291
+ // 去除多余空白和换行符,替换为单个空格
1292
+ let formatted = text
1293
+ .replace(/\s+/g, ' ') // 多个空白字符替换为单个空格
1294
+ .replace(/[\r\n]+/g, ' ') // 换行符替换为空格
1295
+ .trim();
1296
+ // 限制最大长度
1297
+ const maxLength = 100;
1298
+ if (formatted.length <= maxLength) {
1299
+ return formatted;
1300
+ }
1301
+ // 在单词边界处截断
1302
+ let truncated = formatted.slice(0, maxLength);
1303
+ const lastSpaceIndex = truncated.lastIndexOf(' ');
1304
+ if (lastSpaceIndex > maxLength * 0.7) {
1305
+ // 如果最后一个空格位置在长度的70%之后,在空格处截断
1306
+ truncated = truncated.slice(0, lastSpaceIndex);
1307
+ }
1308
+ // 添加省略号
1309
+ return truncated.trim() + '...';
1310
+ }
1272
1311
  /**
1273
1312
  * 根据源工具类型和目标API类型,映射请求路径
1274
1313
  * @param sourceTool 源工具类型 (claude-code 或 codex)
@@ -1533,6 +1572,7 @@ class ProxyServer {
1533
1572
  }
1534
1573
  // 收集stream chunks(每个chunk是一个完整的SSE事件)
1535
1574
  streamChunksForLog = eventCollector.getChunks();
1575
+ console.log('[Proxy] Stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
1536
1576
  void finalizeLog(res.statusCode);
1537
1577
  });
1538
1578
  // 监听 res 的错误事件
@@ -1553,6 +1593,7 @@ class ProxyServer {
1553
1593
  errorStack: error.stack,
1554
1594
  requestHeaders: this.normalizeHeaders(req.headers),
1555
1595
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
1596
+ upstreamRequest: upstreamRequestForLog,
1556
1597
  });
1557
1598
  }
1558
1599
  catch (logError) {
@@ -1602,6 +1643,7 @@ class ProxyServer {
1602
1643
  }
1603
1644
  }
1604
1645
  streamChunksForLog = eventCollector.getChunks();
1646
+ console.log('[Proxy] Codex stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
1605
1647
  void finalizeLog(res.statusCode);
1606
1648
  });
1607
1649
  // 监听 res 的错误事件
@@ -1622,6 +1664,7 @@ class ProxyServer {
1622
1664
  errorStack: error.stack,
1623
1665
  requestHeaders: this.normalizeHeaders(req.headers),
1624
1666
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
1667
+ upstreamRequest: upstreamRequestForLog,
1625
1668
  });
1626
1669
  }
1627
1670
  catch (logError) {
@@ -1655,6 +1698,7 @@ class ProxyServer {
1655
1698
  });
1656
1699
  res.on('finish', () => {
1657
1700
  streamChunksForLog = eventCollector.getChunks();
1701
+ console.log('[Proxy] Default stream request finished, collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
1658
1702
  // 尝试从event collector中提取usage信息
1659
1703
  const extractedUsage = eventCollector.extractUsage();
1660
1704
  if (extractedUsage) {
@@ -1676,6 +1720,7 @@ class ProxyServer {
1676
1720
  errorStack: error.stack,
1677
1721
  requestHeaders: this.normalizeHeaders(req.headers),
1678
1722
  requestBody: req.body ? JSON.stringify(req.body) : undefined,
1723
+ upstreamRequest: upstreamRequestForLog,
1679
1724
  });
1680
1725
  }
1681
1726
  catch (logError) {
@@ -1751,6 +1796,7 @@ class ProxyServer {
1751
1796
  usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
1752
1797
  // 记录原始响应体
1753
1798
  responseBodyForLog = typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
1799
+ console.log('[Proxy] Non-stream response logged, body length:', (responseBodyForLog === null || responseBodyForLog === void 0 ? void 0 : responseBodyForLog.length) || 0);
1754
1800
  this.copyResponseHeaders(responseHeaders, res);
1755
1801
  if (contentType.includes('application/json')) {
1756
1802
  res.status(response.status).json(responseData);
@@ -143,7 +143,9 @@ const transformClaudeRequestToOpenAIChat = (body, targetModel) => {
143
143
  // 映射 system 角色到 developer (如果需要)
144
144
  const mappedRole = (message.role === 'system' && useDeveloperRole) ? 'developer' : message.role;
145
145
  if (typeof message.content === 'string' || message.content === null) {
146
- messages.push({ role: mappedRole, content: message.content });
146
+ // 处理 content null 的情况,使用空字符串替代
147
+ const content = message.content === null ? '' : message.content;
148
+ messages.push({ role: mappedRole, content });
147
149
  continue;
148
150
  }
149
151
  if (Array.isArray(message.content)) {
@@ -179,7 +181,8 @@ const transformClaudeRequestToOpenAIChat = (body, targetModel) => {
179
181
  }
180
182
  }
181
183
  }
182
- const content = textParts.length > 0 ? textParts.join('') : null;
184
+ // 避免 content null,使用空字符串替代
185
+ const content = textParts.length > 0 ? textParts.join('') : '';
183
186
  const openaiMessage = {
184
187
  role: mappedRole,
185
188
  content,