aicodeswitch 4.0.4 → 5.1.0

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.
Files changed (77) hide show
  1. package/README.md +6 -5
  2. package/UPGRADE.md +5 -6
  3. package/dist/server/coding-plan.js +94 -0
  4. package/dist/server/config-managed-fields.js +1 -0
  5. package/dist/server/conversions/compact.js +613 -0
  6. package/dist/server/conversions/detector.js +70 -0
  7. package/dist/server/conversions/index.js +290 -0
  8. package/dist/server/conversions/pairs/claude-completions/request.js +167 -0
  9. package/dist/server/conversions/pairs/claude-completions/response.js +56 -0
  10. package/dist/server/conversions/pairs/claude-completions/streaming.js +259 -0
  11. package/dist/server/conversions/pairs/claude-gemini/request.js +130 -0
  12. package/dist/server/conversions/pairs/claude-gemini/response.js +65 -0
  13. package/dist/server/conversions/pairs/claude-gemini/streaming.js +199 -0
  14. package/dist/server/conversions/pairs/claude-responses/request.js +190 -0
  15. package/dist/server/conversions/pairs/claude-responses/response.js +89 -0
  16. package/dist/server/conversions/pairs/claude-responses/streaming.js +266 -0
  17. package/dist/server/conversions/pairs/completions-claude/request.js +111 -0
  18. package/dist/server/conversions/pairs/completions-claude/response.js +67 -0
  19. package/dist/server/conversions/pairs/completions-claude/streaming.js +165 -0
  20. package/dist/server/conversions/pairs/completions-gemini/request.js +169 -0
  21. package/dist/server/conversions/pairs/completions-gemini/response.js +70 -0
  22. package/dist/server/conversions/pairs/completions-gemini/streaming.js +132 -0
  23. package/dist/server/conversions/pairs/completions-responses/request.js +149 -0
  24. package/dist/server/conversions/pairs/completions-responses/response.js +74 -0
  25. package/dist/server/conversions/pairs/completions-responses/streaming.js +189 -0
  26. package/dist/server/conversions/pairs/gemini-claude/request.js +118 -0
  27. package/dist/server/conversions/pairs/gemini-claude/response.js +45 -0
  28. package/dist/server/conversions/pairs/gemini-claude/streaming.js +146 -0
  29. package/dist/server/conversions/pairs/gemini-completions/request.js +151 -0
  30. package/dist/server/conversions/pairs/gemini-completions/response.js +54 -0
  31. package/dist/server/conversions/pairs/gemini-completions/streaming.js +108 -0
  32. package/dist/server/conversions/pairs/gemini-responses/request.js +18 -0
  33. package/dist/server/conversions/pairs/gemini-responses/response.js +18 -0
  34. package/dist/server/conversions/pairs/gemini-responses/streaming.js +43 -0
  35. package/dist/server/conversions/pairs/responses-claude/request.js +180 -0
  36. package/dist/server/conversions/pairs/responses-claude/response.js +70 -0
  37. package/dist/server/conversions/pairs/responses-claude/streaming.js +345 -0
  38. package/dist/server/conversions/pairs/responses-completions/request.js +207 -0
  39. package/dist/server/conversions/pairs/responses-completions/response.js +96 -0
  40. package/dist/server/conversions/pairs/responses-completions/streaming.js +344 -0
  41. package/dist/server/conversions/pairs/responses-gemini/request.js +18 -0
  42. package/dist/server/conversions/pairs/responses-gemini/response.js +18 -0
  43. package/dist/server/conversions/pairs/responses-gemini/streaming.js +43 -0
  44. package/dist/server/conversions/pairs/responses-responses/request.js +115 -0
  45. package/dist/server/conversions/pipeline.js +296 -0
  46. package/dist/server/conversions/stream-converter-adapter.js +49 -0
  47. package/dist/server/conversions/thinking/effort.js +61 -0
  48. package/dist/server/conversions/thinking/mapper.js +59 -0
  49. package/dist/server/conversions/thinking/providers.js +80 -0
  50. package/dist/server/conversions/types.js +5 -0
  51. package/dist/server/conversions/url-normalizer.js +58 -0
  52. package/dist/server/conversions/utils/format-mappers.js +57 -0
  53. package/dist/server/conversions/utils/id.js +33 -0
  54. package/dist/server/conversions/utils/stop-reasons.js +95 -0
  55. package/dist/server/conversions/utils/streaming-helpers.js +59 -0
  56. package/dist/server/conversions/utils/tool-schema.js +169 -0
  57. package/dist/server/conversions/utils/usage.js +82 -0
  58. package/dist/server/fs-database.js +465 -135
  59. package/dist/server/main.js +93 -33
  60. package/dist/server/original-config-reader.js +1 -1
  61. package/dist/server/proxy-server.js +887 -633
  62. package/dist/server/transformers/chunk-collector.js +5 -1
  63. package/dist/server/transformers/streaming.js +6 -3235
  64. package/dist/server/type-migration.js +2 -3
  65. package/dist/server/utils.js +5 -0
  66. package/dist/ui/assets/{index-C7G0whng.css → index-BHR12ImE.css} +1 -1
  67. package/dist/ui/assets/index-Rwiqttz-.js +517 -0
  68. package/dist/ui/index.html +2 -2
  69. package/package.json +1 -1
  70. package/dist/server/transformers/transformers.js +0 -1767
  71. package/dist/ui/assets/index-Dl-B9pXM.js +0 -514
  72. package/schema/claude.schema.md +0 -946
  73. package/schema/deepseek-chat.schema.md +0 -799
  74. package/schema/gemini.schema.md +0 -1408
  75. package/schema/openai-chat-completions.schema.md +0 -1088
  76. package/schema/openai-responses.schema.md +0 -226196
  77. package/schema/stream.md +0 -2592
@@ -32,9 +32,17 @@ const type_migration_1 = require("./type-migration");
32
32
  const VALID_CODEX_REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
33
33
  const DEFAULT_CODEX_REASONING_EFFORT = 'high';
34
34
  const DEFAULT_FAILOVER_RECOVERY_SECONDS = 30;
35
+ const VALID_CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'max'];
36
+ const DEFAULT_CLAUDE_EFFORT_LEVEL = 'medium';
35
37
  const isCodexReasoningEffort = (value) => {
36
38
  return typeof value === 'string' && VALID_CODEX_REASONING_EFFORTS.includes(value);
37
39
  };
40
+ const isClaudeEffortLevel = (value) => {
41
+ return typeof value === 'string' && VALID_CLAUDE_EFFORT_LEVELS.includes(value);
42
+ };
43
+ const isValidAutocompactPct = (v) => {
44
+ return typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 100;
45
+ };
38
46
  const normalizeFailoverRecoverySeconds = (value) => {
39
47
  const parsed = typeof value === 'number' ? value : Number(value);
40
48
  if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -61,6 +69,8 @@ class FileSystemDatabaseManager {
61
69
  get blacklistFile() { return path_1.default.join(this.dataPath, 'blacklist.json'); }
62
70
  get statisticsFile() { return path_1.default.join(this.dataPath, 'statistics.json'); }
63
71
  get mcpFile() { return path_1.default.join(this.dataPath, 'mcps.json'); }
72
+ get toolBindingsFile() { return path_1.default.join(this.dataPath, 'tool-bindings.json'); }
73
+ get apiPathBindingsFile() { return path_1.default.join(this.dataPath, 'api-path-bindings.json'); }
64
74
  // 创建空的统计数据结构
65
75
  createEmptyStatistics() {
66
76
  return {
@@ -188,6 +198,27 @@ class FileSystemDatabaseManager {
188
198
  writable: true,
189
199
  value: []
190
200
  });
201
+ Object.defineProperty(this, "apiPathBindingsData", {
202
+ enumerable: true,
203
+ configurable: true,
204
+ writable: true,
205
+ value: []
206
+ });
207
+ Object.defineProperty(this, "apiPathModelsData", {
208
+ enumerable: true,
209
+ configurable: true,
210
+ writable: true,
211
+ value: ''
212
+ });
213
+ Object.defineProperty(this, "toolBindings", {
214
+ enumerable: true,
215
+ configurable: true,
216
+ writable: true,
217
+ value: {
218
+ 'claude-code': { tool: 'claude-code', routeId: null },
219
+ 'codex': { tool: 'codex', routeId: null },
220
+ }
221
+ });
191
222
  // 持久化统计数据
192
223
  Object.defineProperty(this, "statistics", {
193
224
  enumerable: true,
@@ -207,6 +238,26 @@ class FileSystemDatabaseManager {
207
238
  writable: true,
208
239
  value: false
209
240
  });
241
+ // 分片写入锁:防止并发读写同一个分片文件导致数据损坏
242
+ Object.defineProperty(this, "shardWriteLocks", {
243
+ enumerable: true,
244
+ configurable: true,
245
+ writable: true,
246
+ value: new Map()
247
+ });
248
+ // 延迟维护标记:启动时跳过耗时操作,后台异步执行
249
+ Object.defineProperty(this, "_needsShardVerification", {
250
+ enumerable: true,
251
+ configurable: true,
252
+ writable: true,
253
+ value: false
254
+ });
255
+ Object.defineProperty(this, "_needsSessionLogIndexBuild", {
256
+ enumerable: true,
257
+ configurable: true,
258
+ writable: true,
259
+ value: false
260
+ });
210
261
  // 缓存机制
211
262
  Object.defineProperty(this, "logsCountCache", {
212
263
  enumerable: true,
@@ -271,6 +322,41 @@ class FileSystemDatabaseManager {
271
322
  yield this.ensureDefaultConfig();
272
323
  });
273
324
  }
325
+ /**
326
+ * 执行延迟的维护任务(启动后异步执行,不阻塞服务启动)
327
+ * 包括:分片一致性校验、损坏修复、旧日志清理、会话索引构建
328
+ */
329
+ deferredMaintenance() {
330
+ return __awaiter(this, void 0, void 0, function* () {
331
+ const tasks = [];
332
+ if (this._needsShardVerification) {
333
+ this._needsShardVerification = false;
334
+ tasks.push((() => __awaiter(this, void 0, void 0, function* () {
335
+ try {
336
+ yield this.verifyShardIndexConsistency();
337
+ yield this.cleanupOldLogShards();
338
+ console.log('[Database] Background shard verification completed');
339
+ }
340
+ catch (err) {
341
+ console.error('[Database] Background shard verification failed:', err);
342
+ }
343
+ }))());
344
+ }
345
+ if (this._needsSessionLogIndexBuild) {
346
+ this._needsSessionLogIndexBuild = false;
347
+ tasks.push((() => __awaiter(this, void 0, void 0, function* () {
348
+ try {
349
+ yield this.buildSessionLogIndex();
350
+ console.log('[Database] Background session log index build completed');
351
+ }
352
+ catch (err) {
353
+ console.error('[Database] Background session log index build failed:', err);
354
+ }
355
+ }))());
356
+ }
357
+ yield Promise.all(tasks);
358
+ });
359
+ }
274
360
  loadAllData() {
275
361
  return __awaiter(this, void 0, void 0, function* () {
276
362
  yield Promise.all([
@@ -279,14 +365,16 @@ class FileSystemDatabaseManager {
279
365
  this.loadRoutes(),
280
366
  this.loadConfig(),
281
367
  this.loadSessions(),
282
- this.loadLogsIndex(),
368
+ this.loadLogsIndex(true), // 启动时跳过耗时校验
283
369
  this.loadErrorLogs(),
284
370
  this.loadBlacklist(),
285
371
  this.loadStatistics(),
286
372
  this.loadMCPs(),
373
+ this.loadApiPathBindings(),
374
+ this.loadToolBindings(),
287
375
  ]);
288
376
  // 会话日志索引依赖 logShardsIndex,必须在 loadLogsIndex 之后
289
- yield this.loadSessionLogIndex();
377
+ yield this.loadSessionLogIndex(true); // 启动时跳过全量构建
290
378
  });
291
379
  }
292
380
  loadVendors() {
@@ -298,9 +386,10 @@ class FileSystemDatabaseManager {
298
386
  this.vendors = Array.isArray(parsed) ? parsed.map((vendor) => {
299
387
  const normalizedServices = Array.isArray(vendor.services)
300
388
  ? vendor.services.map((service) => {
301
- const normalizedService = Object.assign(Object.assign({}, service), { apiKey: typeof service.apiKey === 'string' ? service.apiKey : '', inheritVendorApiKey: service.inheritVendorApiKey === true });
389
+ const normalizedService = Object.assign(Object.assign({}, service), { apiKey: typeof service.apiKey === 'string' ? service.apiKey : '', inheritVendorApiKey: service.inheritVendorApiKey === true, inheritVendorApiBaseUrl: service.inheritVendorApiBaseUrl === true });
302
390
  if (normalizedService.apiKey !== service.apiKey ||
303
- normalizedService.inheritVendorApiKey !== service.inheritVendorApiKey) {
391
+ normalizedService.inheritVendorApiKey !== service.inheritVendorApiKey ||
392
+ normalizedService.inheritVendorApiBaseUrl !== service.inheritVendorApiBaseUrl) {
304
393
  needSave = true;
305
394
  }
306
395
  return normalizedService;
@@ -514,6 +603,31 @@ class FileSystemDatabaseManager {
514
603
  yield this.saveRoutesData();
515
604
  });
516
605
  }
606
+ loadToolBindings() {
607
+ return __awaiter(this, void 0, void 0, function* () {
608
+ try {
609
+ const data = yield promises_1.default.readFile(this.toolBindingsFile, 'utf-8');
610
+ const parsed = JSON.parse(data);
611
+ // Merge with defaults to handle new tools
612
+ this.toolBindings = {
613
+ 'claude-code': parsed['claude-code'] || { tool: 'claude-code', routeId: null },
614
+ 'codex': parsed['codex'] || { tool: 'codex', routeId: null },
615
+ };
616
+ }
617
+ catch (_a) {
618
+ // File doesn't exist yet, use defaults
619
+ this.toolBindings = {
620
+ 'claude-code': { tool: 'claude-code', routeId: null },
621
+ 'codex': { tool: 'codex', routeId: null },
622
+ };
623
+ }
624
+ });
625
+ }
626
+ saveToolBindings() {
627
+ return __awaiter(this, void 0, void 0, function* () {
628
+ yield promises_1.default.writeFile(this.toolBindingsFile, JSON.stringify(this.toolBindings, null, 2));
629
+ });
630
+ }
517
631
  /**
518
632
  * 检测并迁移旧的 rules.json 到 routes.json 的 rules 属性
519
633
  * 旧格式:routes.json + rules.json 分离
@@ -596,7 +710,7 @@ class FileSystemDatabaseManager {
596
710
  });
597
711
  }
598
712
  loadLogsIndex() {
599
- return __awaiter(this, void 0, void 0, function* () {
713
+ return __awaiter(this, arguments, void 0, function* (deferMaintenance = true) {
600
714
  try {
601
715
  const data = yield promises_1.default.readFile(this.logsIndexFile, 'utf-8');
602
716
  this.logShardsIndex = JSON.parse(data);
@@ -607,8 +721,47 @@ class FileSystemDatabaseManager {
607
721
  }
608
722
  // 检查并迁移旧的 logs.json 文件
609
723
  yield this.migrateOldLogsIfNeeded();
610
- // 清理旧日志分片
611
- yield this.cleanupOldLogShards();
724
+ if (deferMaintenance) {
725
+ // 启动时跳过耗时操作,由 deferredMaintenance() 异步执行
726
+ this._needsShardVerification = true;
727
+ }
728
+ else {
729
+ // 校验索引与实际分片数据的一致性
730
+ yield this.verifyShardIndexConsistency();
731
+ // 清理旧日志分片
732
+ yield this.cleanupOldLogShards();
733
+ }
734
+ });
735
+ }
736
+ /**
737
+ * 校验索引中的 count 与实际分片文件中的数据是否一致
738
+ * 修复因并发写入竞争导致的不一致
739
+ */
740
+ verifyShardIndexConsistency() {
741
+ return __awaiter(this, void 0, void 0, function* () {
742
+ let fixedCount = 0;
743
+ for (const shard of this.logShardsIndex) {
744
+ try {
745
+ const shardLogs = yield this.loadLogShard(shard.filename);
746
+ if (shardLogs.length !== shard.count) {
747
+ console.warn(`[Database] Shard count mismatch on startup: ${shard.filename} index=${shard.count} actual=${shardLogs.length}, correcting`);
748
+ shard.count = shardLogs.length;
749
+ fixedCount++;
750
+ }
751
+ }
752
+ catch (_a) {
753
+ // 分片文件无法读取,将 count 设为 0
754
+ if (shard.count > 0) {
755
+ console.warn(`[Database] Shard file unreadable on startup: ${shard.filename}, setting count to 0`);
756
+ shard.count = 0;
757
+ fixedCount++;
758
+ }
759
+ }
760
+ }
761
+ if (fixedCount > 0) {
762
+ console.log(`[Database] Fixed ${fixedCount} shard index count mismatch(es)`);
763
+ yield this.saveLogsIndex();
764
+ }
612
765
  });
613
766
  }
614
767
  saveLogsIndex() {
@@ -617,10 +770,10 @@ class FileSystemDatabaseManager {
617
770
  });
618
771
  }
619
772
  /**
620
- * 加载会话日志索引,若不存在则从现有日志全量构建
773
+ * 加载会话日志索引,若不存在则标记为延迟构建
621
774
  */
622
775
  loadSessionLogIndex() {
623
- return __awaiter(this, void 0, void 0, function* () {
776
+ return __awaiter(this, arguments, void 0, function* (deferBuild = true) {
624
777
  try {
625
778
  const data = yield promises_1.default.readFile(this.sessionLogIndexFile, 'utf-8');
626
779
  const parsed = JSON.parse(data);
@@ -628,9 +781,16 @@ class FileSystemDatabaseManager {
628
781
  console.log(`[Database] Session log index loaded: ${this.sessionLogIndex.size} sessions`);
629
782
  }
630
783
  catch (_a) {
631
- // 索引文件不存在,从现有日志全量构建
632
- console.log('[Database] Session log index not found, building from existing logs...');
633
- yield this.buildSessionLogIndex();
784
+ if (deferBuild) {
785
+ // 启动时跳过全量构建,由 deferredMaintenance() 异步执行
786
+ console.log('[Database] Session log index not found, will build in background...');
787
+ this._needsSessionLogIndexBuild = true;
788
+ }
789
+ else {
790
+ // 索引文件不存在,从现有日志全量构建
791
+ console.log('[Database] Session log index not found, building from existing logs...');
792
+ yield this.buildSessionLogIndex();
793
+ }
634
794
  }
635
795
  });
636
796
  }
@@ -863,8 +1023,20 @@ class FileSystemDatabaseManager {
863
1023
  return __awaiter(this, void 0, void 0, function* () {
864
1024
  const filepath = path_1.default.join(this.logsDir, filename);
865
1025
  try {
866
- const data = yield promises_1.default.readFile(filepath, 'utf-8');
867
- return JSON.parse(data);
1026
+ let content = yield promises_1.default.readFile(filepath, 'utf-8');
1027
+ // 检测并修复空字节损坏(并发写入竞争可能导致文件中间出现大量 \x00)
1028
+ const nullIndex = content.indexOf('\x00');
1029
+ if (nullIndex !== -1) {
1030
+ console.warn(`[Database] Detected corrupted shard file: ${filename}, truncating at null byte (pos ${nullIndex})`);
1031
+ content = content.substring(0, nullIndex);
1032
+ // 尝试补全被截断的 JSON 数组
1033
+ const openBrackets = (content.match(/\[/g) || []).length;
1034
+ const closeBrackets = (content.match(/\]/g) || []).length;
1035
+ if (openBrackets > closeBrackets) {
1036
+ content += ']';
1037
+ }
1038
+ }
1039
+ return JSON.parse(content);
868
1040
  }
869
1041
  catch (_a) {
870
1042
  return [];
@@ -874,8 +1046,12 @@ class FileSystemDatabaseManager {
874
1046
  saveLogShard(filename, logs) {
875
1047
  return __awaiter(this, void 0, void 0, function* () {
876
1048
  const filepath = path_1.default.join(this.logsDir, filename);
1049
+ const tempFile = path_1.default.join(this.logsDir, `.tmp_${filename}`);
877
1050
  yield promises_1.default.mkdir(this.logsDir, { recursive: true });
878
- yield promises_1.default.writeFile(filepath, JSON.stringify(logs, null, 2));
1051
+ const content = JSON.stringify(logs, null, 2);
1052
+ // 先写临时文件,再原子重命名,避免写入中途被并发操作导致文件损坏
1053
+ yield promises_1.default.writeFile(tempFile, content, 'utf-8');
1054
+ yield promises_1.default.rename(tempFile, filepath);
879
1055
  });
880
1056
  }
881
1057
  loadErrorLogs() {
@@ -970,6 +1146,51 @@ class FileSystemDatabaseManager {
970
1146
  }
971
1147
  });
972
1148
  }
1149
+ loadApiPathBindings() {
1150
+ return __awaiter(this, void 0, void 0, function* () {
1151
+ const defaults = [
1152
+ { apiPath: '/v1/messages', routeId: null },
1153
+ { apiPath: '/v1/responses', routeId: null },
1154
+ { apiPath: '/v1/chat/completions', routeId: null },
1155
+ { apiPath: '/v1beta/models', routeId: null },
1156
+ { apiPath: '/v1/models', routeId: null },
1157
+ ];
1158
+ try {
1159
+ const data = yield promises_1.default.readFile(this.apiPathBindingsFile, 'utf-8');
1160
+ const parsed = JSON.parse(data);
1161
+ this.apiPathBindingsData = parsed.bindings || defaults;
1162
+ this.apiPathModelsData = parsed.models || '';
1163
+ }
1164
+ catch (_a) {
1165
+ this.apiPathBindingsData = defaults;
1166
+ this.apiPathModelsData = '';
1167
+ yield this.saveApiPathBindings();
1168
+ }
1169
+ });
1170
+ }
1171
+ saveApiPathBindings() {
1172
+ return __awaiter(this, void 0, void 0, function* () {
1173
+ yield promises_1.default.writeFile(this.apiPathBindingsFile, JSON.stringify({
1174
+ bindings: this.apiPathBindingsData,
1175
+ models: this.apiPathModelsData,
1176
+ }, null, 2));
1177
+ });
1178
+ }
1179
+ getApiPathBindings() {
1180
+ return this.apiPathBindingsData;
1181
+ }
1182
+ getApiPathModels() {
1183
+ return this.apiPathModelsData;
1184
+ }
1185
+ updateApiPathBindings(bindings, models) {
1186
+ return __awaiter(this, void 0, void 0, function* () {
1187
+ this.apiPathBindingsData = bindings;
1188
+ if (models !== undefined) {
1189
+ this.apiPathModelsData = models;
1190
+ }
1191
+ yield this.saveApiPathBindings();
1192
+ });
1193
+ }
973
1194
  saveMCPs() {
974
1195
  return __awaiter(this, void 0, void 0, function* () {
975
1196
  yield promises_1.default.writeFile(this.mcpFile, JSON.stringify(this.mcps, null, 2));
@@ -977,28 +1198,43 @@ class FileSystemDatabaseManager {
977
1198
  }
978
1199
  ensureDefaultConfig() {
979
1200
  return __awaiter(this, void 0, void 0, function* () {
980
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
981
1201
  const current = this.config;
982
- this.config = {
983
- enableLogging: (_a = current === null || current === void 0 ? void 0 : current.enableLogging) !== null && _a !== void 0 ? _a : true,
984
- logRetentionDays: (_b = current === null || current === void 0 ? void 0 : current.logRetentionDays) !== null && _b !== void 0 ? _b : 30,
985
- maxLogSize: (_c = current === null || current === void 0 ? void 0 : current.maxLogSize) !== null && _c !== void 0 ? _c : 100000,
986
- apiKey: (_d = current === null || current === void 0 ? void 0 : current.apiKey) !== null && _d !== void 0 ? _d : '',
987
- enableFailover: (_e = current === null || current === void 0 ? void 0 : current.enableFailover) !== null && _e !== void 0 ? _e : true,
988
- failoverRecoverySeconds: normalizeFailoverRecoverySeconds(current === null || current === void 0 ? void 0 : current.failoverRecoverySeconds),
989
- ruleGlobalTimeout: typeof (current === null || current === void 0 ? void 0 : current.ruleGlobalTimeout) === 'number' && current.ruleGlobalTimeout > 0
990
- ? current.ruleGlobalTimeout
991
- : undefined,
992
- enableAgentTeams: (_f = current === null || current === void 0 ? void 0 : current.enableAgentTeams) !== null && _f !== void 0 ? _f : false,
993
- enableBypassPermissionsSupport: (_g = current === null || current === void 0 ? void 0 : current.enableBypassPermissionsSupport) !== null && _g !== void 0 ? _g : false,
994
- codexModelReasoningEffort: isCodexReasoningEffort(current === null || current === void 0 ? void 0 : current.codexModelReasoningEffort)
995
- ? current.codexModelReasoningEffort
996
- : DEFAULT_CODEX_REASONING_EFFORT,
997
- proxyEnabled: (_h = current === null || current === void 0 ? void 0 : current.proxyEnabled) !== null && _h !== void 0 ? _h : false,
998
- proxyUrl: (_j = current === null || current === void 0 ? void 0 : current.proxyUrl) !== null && _j !== void 0 ? _j : '',
999
- proxyUsername: (_k = current === null || current === void 0 ? void 0 : current.proxyUsername) !== null && _k !== void 0 ? _k : '',
1000
- proxyPassword: (_l = current === null || current === void 0 ? void 0 : current.proxyPassword) !== null && _l !== void 0 ? _l : '',
1202
+ const defaults = {
1203
+ enableLogging: true,
1204
+ logRetentionDays: 30,
1205
+ maxLogSize: 100000,
1206
+ apiKey: '',
1207
+ enableFailover: true,
1208
+ failoverRecoverySeconds: DEFAULT_FAILOVER_RECOVERY_SECONDS,
1209
+ ruleGlobalTimeout: undefined,
1210
+ enableAgentTeams: false,
1211
+ enableBypassPermissionsSupport: false,
1212
+ claudeEffortLevel: DEFAULT_CLAUDE_EFFORT_LEVEL,
1213
+ autocompactPctOverride: undefined,
1214
+ claudeDefaultModel: undefined,
1215
+ codexModelReasoningEffort: DEFAULT_CODEX_REASONING_EFFORT,
1216
+ codexDefaultModel: undefined,
1217
+ proxyEnabled: false,
1218
+ proxyUrl: '',
1219
+ proxyUsername: '',
1220
+ proxyPassword: '',
1001
1221
  };
1222
+ // spread: current 覆盖 defaults,未来新增字段自动保留
1223
+ this.config = Object.assign(Object.assign({}, defaults), current);
1224
+ // 校验归一化(与 updateConfig 保持一致)
1225
+ if (!isCodexReasoningEffort(this.config.codexModelReasoningEffort)) {
1226
+ this.config.codexModelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT;
1227
+ }
1228
+ if (!isClaudeEffortLevel(this.config.claudeEffortLevel)) {
1229
+ this.config.claudeEffortLevel = DEFAULT_CLAUDE_EFFORT_LEVEL;
1230
+ }
1231
+ if (typeof this.config.autocompactPctOverride !== 'undefined' && !isValidAutocompactPct(this.config.autocompactPctOverride)) {
1232
+ this.config.autocompactPctOverride = undefined;
1233
+ }
1234
+ this.config.failoverRecoverySeconds = normalizeFailoverRecoverySeconds(this.config.failoverRecoverySeconds);
1235
+ if (typeof this.config.ruleGlobalTimeout !== 'number' || this.config.ruleGlobalTimeout <= 0) {
1236
+ this.config.ruleGlobalTimeout = undefined;
1237
+ }
1002
1238
  // 仅在首次创建或存在字段补齐时落盘
1003
1239
  if (!current || JSON.stringify(current) !== JSON.stringify(this.config)) {
1004
1240
  yield this.saveConfig();
@@ -1007,22 +1243,28 @@ class FileSystemDatabaseManager {
1007
1243
  }
1008
1244
  migrateRouteToolSettingsToGlobalConfig() {
1009
1245
  return __awaiter(this, void 0, void 0, function* () {
1246
+ var _a;
1247
+ const rawRoutes = this.routes;
1248
+ // ---------------------------------------------------------------------------
1249
+ // Step 1: Migrate legacy route-level tool settings (enableAgentTeams, etc.)
1250
+ // to global AppConfig — only if AppConfig doesn't already have them.
1251
+ // ---------------------------------------------------------------------------
1010
1252
  const hasGlobalToolConfig = !!this.config &&
1011
1253
  (Object.prototype.hasOwnProperty.call(this.config, 'enableAgentTeams') ||
1012
1254
  Object.prototype.hasOwnProperty.call(this.config, 'enableBypassPermissionsSupport') ||
1013
1255
  Object.prototype.hasOwnProperty.call(this.config, 'codexModelReasoningEffort'));
1014
- const getPreferredRoute = (targetType) => {
1015
- const activeRoute = this.routes.find(route => route.targetType === targetType && route.isActive);
1016
- if (activeRoute) {
1017
- return activeRoute;
1018
- }
1019
- return this.routes.find(route => route.targetType === targetType);
1020
- };
1021
- let configUpdated = false;
1022
1256
  if (!hasGlobalToolConfig) {
1257
+ const getPreferredRoute = (targetType) => {
1258
+ // Prefer isActive=true route, fall back to first match
1259
+ const active = rawRoutes.find(r => r.targetType === targetType && r.isActive === true);
1260
+ if (active)
1261
+ return active;
1262
+ return rawRoutes.find(r => r.targetType === targetType);
1263
+ };
1023
1264
  const preferredClaudeRoute = getPreferredRoute('claude-code');
1024
1265
  const preferredCodexRoute = getPreferredRoute('codex');
1025
1266
  const nextConfig = Object.assign({}, (this.config || {}));
1267
+ let configUpdated = false;
1026
1268
  if (typeof (preferredClaudeRoute === null || preferredClaudeRoute === void 0 ? void 0 : preferredClaudeRoute.enableAgentTeams) === 'boolean') {
1027
1269
  nextConfig.enableAgentTeams = preferredClaudeRoute.enableAgentTeams;
1028
1270
  configUpdated = true;
@@ -1038,28 +1280,68 @@ class FileSystemDatabaseManager {
1038
1280
  if (configUpdated) {
1039
1281
  this.config = nextConfig;
1040
1282
  yield this.saveConfig();
1041
- console.log('[ConfigMigration] Migrated route-level tool settings to global app config');
1042
- }
1043
- }
1044
- // 清理路由中的旧字段,避免后续重复歧义
1045
- let routesUpdated = false;
1046
- this.routes = this.routes.map((route) => {
1047
- const hasLegacyFields = Object.prototype.hasOwnProperty.call(route, 'enableAgentTeams') ||
1048
- Object.prototype.hasOwnProperty.call(route, 'enableBypassPermissionsSupport') ||
1049
- Object.prototype.hasOwnProperty.call(route, 'codexModelReasoningEffort');
1050
- if (!hasLegacyFields) {
1051
- return route;
1052
- }
1053
- routesUpdated = true;
1054
- const cleanedRoute = Object.assign({}, route);
1055
- delete cleanedRoute.enableAgentTeams;
1056
- delete cleanedRoute.enableBypassPermissionsSupport;
1057
- delete cleanedRoute.codexModelReasoningEffort;
1058
- return cleanedRoute;
1059
- });
1060
- if (routesUpdated) {
1061
- yield this.saveRoutes();
1062
- console.log('[ConfigMigration] Removed deprecated route-level tool settings');
1283
+ console.log('[Migration] Migrated route-level tool settings to global AppConfig');
1284
+ }
1285
+ }
1286
+ // ---------------------------------------------------------------------------
1287
+ // Step 2: Migrate route.targetType + route.isActive → tool-bindings
1288
+ //
1289
+ // This step is the core migration for the Route Activation UX Refactor.
1290
+ // It reads the old Route.isActive and Route.targetType fields from routes.json
1291
+ // and writes equivalent entries into tool-bindings.json.
1292
+ //
1293
+ // Idempotency:
1294
+ // - If tool-bindings.json already has a non-null routeId for a tool, and
1295
+ // the routes no longer carry isActive/targetType (already migrated), this
1296
+ // step is a no-op.
1297
+ // - If routes still carry the old fields (first run after upgrade), they
1298
+ // are migrated and then cleaned.
1299
+ // ---------------------------------------------------------------------------
1300
+ const hasLegacyRouteFields = rawRoutes.some(r => Object.prototype.hasOwnProperty.call(r, 'isActive') ||
1301
+ Object.prototype.hasOwnProperty.call(r, 'targetType'));
1302
+ if (hasLegacyRouteFields) {
1303
+ let toolBindingsUpdated = false;
1304
+ for (const route of rawRoutes) {
1305
+ // Only migrate routes that are explicitly active and have a targetType
1306
+ if (route.isActive === true && route.targetType) {
1307
+ const tool = route.targetType;
1308
+ if (tool === 'claude-code' || tool === 'codex') {
1309
+ // Only write if tool-bindings doesn't already have a binding
1310
+ // (avoid overwriting user's newer tool-binding choices)
1311
+ if (!((_a = this.toolBindings[tool]) === null || _a === void 0 ? void 0 : _a.routeId)) {
1312
+ this.toolBindings[tool] = { tool, routeId: route.id };
1313
+ toolBindingsUpdated = true;
1314
+ console.log(`[Migration] Binding tool '${tool}' → route '${route.id}' (${route.name || 'unnamed'})`);
1315
+ }
1316
+ }
1317
+ }
1318
+ }
1319
+ if (toolBindingsUpdated) {
1320
+ yield this.saveToolBindings();
1321
+ console.log('[Migration] Saved migrated tool-bindings to tool-bindings.json');
1322
+ }
1323
+ // Clean legacy fields from all route objects
1324
+ let routesUpdated = false;
1325
+ this.routes = rawRoutes.map((route) => {
1326
+ const hasLegacy = Object.prototype.hasOwnProperty.call(route, 'targetType') ||
1327
+ Object.prototype.hasOwnProperty.call(route, 'isActive') ||
1328
+ Object.prototype.hasOwnProperty.call(route, 'enableAgentTeams') ||
1329
+ Object.prototype.hasOwnProperty.call(route, 'enableBypassPermissionsSupport') ||
1330
+ Object.prototype.hasOwnProperty.call(route, 'codexModelReasoningEffort');
1331
+ if (!hasLegacy)
1332
+ return route;
1333
+ routesUpdated = true;
1334
+ const { targetType, isActive, enableAgentTeams, enableBypassPermissionsSupport, codexModelReasoningEffort } = route, cleanedRoute = __rest(route, ["targetType", "isActive", "enableAgentTeams", "enableBypassPermissionsSupport", "codexModelReasoningEffort"]);
1335
+ return cleanedRoute;
1336
+ });
1337
+ if (routesUpdated) {
1338
+ yield this.saveRoutes();
1339
+ console.log('[Migration] Cleaned legacy fields (targetType, isActive, deprecated tool settings) from routes.json');
1340
+ }
1341
+ }
1342
+ else {
1343
+ // No legacy fields found — either already migrated or fresh install
1344
+ console.log('[Migration] No legacy route fields found, skipping tool-bindings migration');
1063
1345
  }
1064
1346
  });
1065
1347
  }
@@ -1081,7 +1363,7 @@ class FileSystemDatabaseManager {
1081
1363
  console.log('[数据库] 创建供应商,输入数据:', JSON.stringify(vendor, null, 2));
1082
1364
  const id = crypto_1.default.randomUUID();
1083
1365
  const now = Date.now();
1084
- const newVendor = Object.assign(Object.assign({}, vendor), { apiKey: typeof vendor.apiKey === 'string' ? vendor.apiKey : '', id, services: vendor.services || [], createdAt: now, updatedAt: now });
1366
+ const newVendor = Object.assign(Object.assign({}, vendor), { apiKey: typeof vendor.apiKey === 'string' ? vendor.apiKey : '', apiBaseUrl: typeof vendor.apiBaseUrl === 'string' ? vendor.apiBaseUrl : '', id, services: vendor.services || [], createdAt: now, updatedAt: now });
1085
1367
  console.log('[数据库] 创建供应商,返回数据:', JSON.stringify(newVendor, null, 2));
1086
1368
  this.vendors.push(newVendor);
1087
1369
  yield this.saveVendors();
@@ -1096,7 +1378,9 @@ class FileSystemDatabaseManager {
1096
1378
  const now = Date.now();
1097
1379
  this.vendors[index] = Object.assign(Object.assign(Object.assign({}, this.vendors[index]), vendor), { id, apiKey: typeof vendor.apiKey === 'string'
1098
1380
  ? vendor.apiKey
1099
- : (this.vendors[index].apiKey || ''),
1381
+ : (this.vendors[index].apiKey || ''), apiBaseUrl: typeof vendor.apiBaseUrl === 'string'
1382
+ ? vendor.apiBaseUrl
1383
+ : (this.vendors[index].apiBaseUrl || ''),
1100
1384
  // 供应商服务应通过 create/update/deleteAPIService 单独维护,避免编辑供应商时误覆盖
1101
1385
  services: this.vendors[index].services, updatedAt: now });
1102
1386
  yield this.saveVendors();
@@ -1181,7 +1465,7 @@ class FileSystemDatabaseManager {
1181
1465
  const _a = service, { vendorId: _ } = _a, serviceData = __rest(_a, ["vendorId"]);
1182
1466
  const id = crypto_1.default.randomUUID();
1183
1467
  const now = Date.now();
1184
- const newService = Object.assign(Object.assign({}, serviceData), { apiKey: typeof serviceData.apiKey === 'string' ? serviceData.apiKey : '', inheritVendorApiKey: serviceData.inheritVendorApiKey === true, id, createdAt: now, updatedAt: now });
1468
+ const newService = Object.assign(Object.assign({}, serviceData), { apiKey: typeof serviceData.apiKey === 'string' ? serviceData.apiKey : '', inheritVendorApiKey: serviceData.inheritVendorApiKey === true, inheritVendorApiBaseUrl: serviceData.inheritVendorApiBaseUrl === true, id, createdAt: now, updatedAt: now });
1185
1469
  console.log('[数据库] 创建服务,最终数据:', JSON.stringify(newService, null, 2));
1186
1470
  if (!vendor.services) {
1187
1471
  vendor.services = [];
@@ -1206,7 +1490,9 @@ class FileSystemDatabaseManager {
1206
1490
  const now = Date.now();
1207
1491
  vendor.services[index] = Object.assign(Object.assign(Object.assign({}, vendor.services[index]), service), { id, apiKey: typeof service.apiKey === 'string' ? service.apiKey : (vendor.services[index].apiKey || ''), inheritVendorApiKey: service.inheritVendorApiKey !== undefined
1208
1492
  ? service.inheritVendorApiKey === true
1209
- : vendor.services[index].inheritVendorApiKey === true, updatedAt: now });
1493
+ : vendor.services[index].inheritVendorApiKey === true, inheritVendorApiBaseUrl: service.inheritVendorApiBaseUrl !== undefined
1494
+ ? service.inheritVendorApiBaseUrl === true
1495
+ : vendor.services[index].inheritVendorApiBaseUrl === true, updatedAt: now });
1210
1496
  // 更新供应商的 updatedAt 时间
1211
1497
  vendor.updatedAt = now;
1212
1498
  yield this.saveVendors();
@@ -1280,7 +1566,7 @@ class FileSystemDatabaseManager {
1280
1566
  return __awaiter(this, void 0, void 0, function* () {
1281
1567
  const id = crypto_1.default.randomUUID();
1282
1568
  const now = Date.now();
1283
- const newRoute = Object.assign(Object.assign({}, route), { id, createdAt: now, updatedAt: now });
1569
+ const newRoute = { name: route.name, description: route.description, id, createdAt: now, updatedAt: now };
1284
1570
  this.routes.push(newRoute);
1285
1571
  yield this.saveRoutes();
1286
1572
  return newRoute;
@@ -1302,6 +1588,10 @@ class FileSystemDatabaseManager {
1302
1588
  const index = this.routes.findIndex(r => r.id === id);
1303
1589
  if (index === -1)
1304
1590
  return false;
1591
+ // 检查该路由是否被工具绑定
1592
+ if (this.isRouteBound(id)) {
1593
+ return false;
1594
+ }
1305
1595
  // 删除关联的规则
1306
1596
  this.rules = this.rules.filter(r => r.routeId !== id);
1307
1597
  yield this.saveRules();
@@ -1310,46 +1600,56 @@ class FileSystemDatabaseManager {
1310
1600
  return true;
1311
1601
  });
1312
1602
  }
1313
- activateRoute(id) {
1603
+ getRoute(id) {
1604
+ return this.routes.find(r => r.id === id);
1605
+ }
1606
+ // ToolBindings operations
1607
+ getToolBindings() {
1608
+ return this.toolBindings;
1609
+ }
1610
+ getActiveRouteIdForTool(tool) {
1611
+ var _a, _b;
1612
+ return (_b = (_a = this.toolBindings[tool]) === null || _a === void 0 ? void 0 : _a.routeId) !== null && _b !== void 0 ? _b : null;
1613
+ }
1614
+ activateToolRoute(tool, routeId) {
1314
1615
  return __awaiter(this, void 0, void 0, function* () {
1315
- const route = this.routes.find(r => r.id === id);
1616
+ const route = this.routes.find(r => r.id === routeId);
1316
1617
  if (!route)
1317
1618
  return false;
1318
- // 停用同类型的其他路由
1319
- for (const r of this.routes) {
1320
- if (r.targetType === route.targetType) {
1321
- r.isActive = r.id === id;
1322
- }
1323
- }
1324
- yield this.saveRoutes();
1619
+ this.toolBindings[tool] = { tool, routeId };
1620
+ yield this.saveToolBindings();
1325
1621
  return true;
1326
1622
  });
1327
1623
  }
1328
- deactivateRoute(id) {
1624
+ deactivateToolRoute(tool) {
1329
1625
  return __awaiter(this, void 0, void 0, function* () {
1330
- const route = this.routes.find(r => r.id === id);
1331
- if (!route)
1332
- return false;
1333
- route.isActive = false;
1334
- yield this.saveRoutes();
1626
+ this.toolBindings[tool] = { tool, routeId: null };
1627
+ yield this.saveToolBindings();
1335
1628
  return true;
1336
1629
  });
1337
1630
  }
1338
- deactivateAllRoutes() {
1631
+ deactivateAllToolRoutes() {
1339
1632
  return __awaiter(this, void 0, void 0, function* () {
1340
1633
  let count = 0;
1341
- for (const route of this.routes) {
1342
- if (route.isActive) {
1343
- route.isActive = false;
1634
+ for (const tool of Object.keys(this.toolBindings)) {
1635
+ if (this.toolBindings[tool].routeId) {
1636
+ this.toolBindings[tool] = { tool, routeId: null };
1344
1637
  count++;
1345
1638
  }
1346
1639
  }
1347
1640
  if (count > 0) {
1348
- yield this.saveRoutes();
1641
+ yield this.saveToolBindings();
1349
1642
  }
1350
1643
  return count;
1351
1644
  });
1352
1645
  }
1646
+ isRouteBound(routeId) {
1647
+ for (const tool of Object.keys(this.toolBindings)) {
1648
+ if (this.toolBindings[tool].routeId === routeId)
1649
+ return true;
1650
+ }
1651
+ return false;
1652
+ }
1353
1653
  // Rule operations
1354
1654
  getRules(routeId) {
1355
1655
  const rules = routeId
@@ -1543,29 +1843,47 @@ class FileSystemDatabaseManager {
1543
1843
  const logWithId = Object.assign(Object.assign({}, log), { contentType, id });
1544
1844
  // 获取目标分片文件名
1545
1845
  const filename = yield this.getLogShardFilename(logWithId.timestamp);
1546
- // 加载现有分片数据
1547
- let shardLogs = yield this.loadLogShard(filename);
1548
- // 添加新日志
1549
- shardLogs.push(logWithId);
1550
- // 保存分片
1551
- yield this.saveLogShard(filename, shardLogs);
1552
- // 更新索引
1553
- const date = new Date(logWithId.timestamp).toISOString().split('T')[0];
1554
- let shardIndex = this.logShardsIndex.find(s => s.filename === filename);
1555
- if (shardIndex) {
1556
- shardIndex.count = shardLogs.length;
1557
- shardIndex.endTime = Math.max(shardIndex.endTime, logWithId.timestamp);
1846
+ // 使用分片级别的写入锁,防止并发 read-modify-write 竞争条件
1847
+ const previousLock = this.shardWriteLocks.get(filename) || Promise.resolve();
1848
+ let shardLogsLength = 0;
1849
+ const currentWrite = previousLock.then(() => __awaiter(this, void 0, void 0, function* () {
1850
+ const shardLogs = yield this.loadLogShard(filename);
1851
+ shardLogs.push(logWithId);
1852
+ shardLogsLength = shardLogs.length;
1853
+ yield this.saveLogShard(filename, shardLogs);
1854
+ // 更新索引(在锁内完成,保证一致性)
1855
+ const date = new Date(logWithId.timestamp).toISOString().split('T')[0];
1856
+ const shardIndex = this.logShardsIndex.find(s => s.filename === filename);
1857
+ if (shardIndex) {
1858
+ shardIndex.count = shardLogs.length;
1859
+ shardIndex.endTime = Math.max(shardIndex.endTime, logWithId.timestamp);
1860
+ }
1861
+ else {
1862
+ this.logShardsIndex.push({
1863
+ filename,
1864
+ date,
1865
+ startTime: logWithId.timestamp,
1866
+ endTime: logWithId.timestamp,
1867
+ count: 1
1868
+ });
1869
+ }
1870
+ yield this.saveLogsIndex();
1871
+ }));
1872
+ this.shardWriteLocks.set(filename, currentWrite);
1873
+ try {
1874
+ yield currentWrite;
1558
1875
  }
1559
- else {
1560
- this.logShardsIndex.push({
1561
- filename,
1562
- date,
1563
- startTime: logWithId.timestamp,
1564
- endTime: logWithId.timestamp,
1565
- count: 1
1566
- });
1876
+ catch (error) {
1877
+ // 写入失败时清理锁
1878
+ if (this.shardWriteLocks.get(filename) === currentWrite) {
1879
+ this.shardWriteLocks.delete(filename);
1880
+ }
1881
+ throw error;
1882
+ }
1883
+ // 如果当前锁已执行完毕且没有被后续锁覆盖,清理它
1884
+ if (this.shardWriteLocks.get(filename) === currentWrite) {
1885
+ this.shardWriteLocks.delete(filename);
1567
1886
  }
1568
- yield this.saveLogsIndex();
1569
1887
  // 同时更新统计数据
1570
1888
  yield this.updateStatistics(logWithId);
1571
1889
  // 清除计数缓存
@@ -1578,7 +1896,7 @@ class FileSystemDatabaseManager {
1578
1896
  refs = [];
1579
1897
  this.sessionLogIndex.set(sessionId, refs);
1580
1898
  }
1581
- refs.push({ filename, index: shardLogs.length - 1, timestamp: logWithId.timestamp });
1899
+ refs.push({ filename, index: shardLogsLength - 1, timestamp: logWithId.timestamp });
1582
1900
  this.scheduleSessionLogIndexFlush();
1583
1901
  }
1584
1902
  });
@@ -1592,11 +1910,16 @@ class FileSystemDatabaseManager {
1592
1910
  // 遍历分片直到收集足够的日志
1593
1911
  for (const shard of sortedShards) {
1594
1912
  if (currentOffset + shard.count <= offset) {
1595
- // 跳过整个分片
1913
+ // 跳过整个分片(使用索引中的 count 做快速跳过判断,避免不必要的磁盘IO)
1596
1914
  currentOffset += shard.count;
1597
1915
  continue;
1598
1916
  }
1599
1917
  const shardLogs = yield this.loadLogShard(shard.filename);
1918
+ // 修正索引中的计数(如果发现不一致)
1919
+ if (shardLogs.length !== shard.count) {
1920
+ console.warn(`[Database] Shard count mismatch: ${shard.filename} index=${shard.count} actual=${shardLogs.length}, correcting`);
1921
+ shard.count = shardLogs.length;
1922
+ }
1600
1923
  // 计算需要从该分片取出的日志范围
1601
1924
  let startIndex = 0;
1602
1925
  if (currentOffset < offset) {
@@ -1606,7 +1929,7 @@ class FileSystemDatabaseManager {
1606
1929
  const endIndex = Math.min(startIndex + remainingCount, shardLogs.length);
1607
1930
  // 添加日志到结果
1608
1931
  allLogs.push(...shardLogs.slice(startIndex, endIndex));
1609
- currentOffset += shard.count;
1932
+ currentOffset += shardLogs.length;
1610
1933
  if (allLogs.length >= limit) {
1611
1934
  break;
1612
1935
  }
@@ -1976,9 +2299,16 @@ class FileSystemDatabaseManager {
1976
2299
  updateConfig(config) {
1977
2300
  return __awaiter(this, void 0, void 0, function* () {
1978
2301
  const merged = Object.assign(Object.assign({}, (this.config || {})), config);
2302
+ // 校验归一化(与 ensureDefaultConfig 保持一致)
1979
2303
  if (!isCodexReasoningEffort(merged.codexModelReasoningEffort)) {
1980
2304
  merged.codexModelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT;
1981
2305
  }
2306
+ if (!isClaudeEffortLevel(merged.claudeEffortLevel)) {
2307
+ merged.claudeEffortLevel = DEFAULT_CLAUDE_EFFORT_LEVEL;
2308
+ }
2309
+ if (typeof merged.autocompactPctOverride !== 'undefined' && !isValidAutocompactPct(merged.autocompactPctOverride)) {
2310
+ merged.autocompactPctOverride = undefined;
2311
+ }
1982
2312
  merged.failoverRecoverySeconds = normalizeFailoverRecoverySeconds(merged.failoverRecoverySeconds);
1983
2313
  if (typeof merged.ruleGlobalTimeout !== 'number' || merged.ruleGlobalTimeout <= 0) {
1984
2314
  merged.ruleGlobalTimeout = undefined;
@@ -2019,7 +2349,9 @@ class FileSystemDatabaseManager {
2019
2349
  return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] 缺少有效的 name 字段` };
2020
2350
  }
2021
2351
  if (!service.apiUrl || typeof service.apiUrl !== 'string') {
2022
- return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] 缺少有效的 apiUrl 字段` };
2352
+ if (service.inheritVendorApiBaseUrl !== true) {
2353
+ return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] 缺少有效的 apiUrl 字段` };
2354
+ }
2023
2355
  }
2024
2356
  if (!service.apiKey || typeof service.apiKey !== 'string') {
2025
2357
  if (service.inheritVendorApiKey !== true) {
@@ -2029,6 +2361,9 @@ class FileSystemDatabaseManager {
2029
2361
  if (service.inheritVendorApiKey !== undefined && typeof service.inheritVendorApiKey !== 'boolean') {
2030
2362
  return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] inheritVendorApiKey 必须是布尔值` };
2031
2363
  }
2364
+ if (service.inheritVendorApiBaseUrl !== undefined && typeof service.inheritVendorApiBaseUrl !== 'boolean') {
2365
+ return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] inheritVendorApiBaseUrl 必须是布尔值` };
2366
+ }
2032
2367
  }
2033
2368
  return { valid: true };
2034
2369
  }
@@ -2045,12 +2380,7 @@ class FileSystemDatabaseManager {
2045
2380
  if (!route.name || typeof route.name !== 'string') {
2046
2381
  return { valid: false, error: `路由[${index}](${route.id}) 缺少有效的 name 字段` };
2047
2382
  }
2048
- if (!route.targetType || !['claude-code', 'codex'].includes(route.targetType)) {
2049
- return { valid: false, error: `路由[${index}](${route.id}) 的 targetType 必须是 'claude-code' 或 'codex'` };
2050
- }
2051
- if (typeof route.isActive !== 'boolean') {
2052
- return { valid: false, error: `路由[${index}](${route.id}) 的 isActive 必须是布尔值` };
2053
- }
2383
+ // targetType and isActive are no longer part of Route (migrated to tool-bindings)
2054
2384
  return { valid: true };
2055
2385
  }
2056
2386
  /**
@@ -2066,7 +2396,7 @@ class FileSystemDatabaseManager {
2066
2396
  if (!rule.routeId || typeof rule.routeId !== 'string') {
2067
2397
  return { valid: false, error: `规则[${index}](${rule.id}) 缺少有效的 routeId 字段` };
2068
2398
  }
2069
- const validContentTypes = ['default', 'background', 'thinking', 'long-context', 'image-understanding', 'model-mapping', 'high-iq'];
2399
+ const validContentTypes = ['default', 'background', 'thinking', 'long-context', 'image-understanding', 'model-mapping', 'high-iq', 'compact'];
2070
2400
  if (!rule.contentType || !validContentTypes.includes(rule.contentType)) {
2071
2401
  return { valid: false, error: `规则[${index}](${rule.id}) 的 contentType 无效` };
2072
2402
  }
@@ -2604,9 +2934,9 @@ class FileSystemDatabaseManager {
2604
2934
  * 检查日志是否属于指定 session
2605
2935
  */
2606
2936
  isLogBelongsToSession(log, sessionId) {
2607
- var _a, _b;
2608
- // 检查 headers 中的 session_id(Codex
2609
- if (((_a = log.headers) === null || _a === void 0 ? void 0 : _a['session_id']) === sessionId) {
2937
+ var _a, _b, _c;
2938
+ // 检查 headers 中的 session-id 或 session_id(Codex,兼容新旧版本)
2939
+ if (((_a = log.headers) === null || _a === void 0 ? void 0 : _a['session-id']) === sessionId || ((_b = log.headers) === null || _b === void 0 ? void 0 : _b['session_id']) === sessionId) {
2610
2940
  return true;
2611
2941
  }
2612
2942
  // 检查 body 中的 metadata.user_id(Claude Code)
@@ -2614,7 +2944,7 @@ class FileSystemDatabaseManager {
2614
2944
  try {
2615
2945
  // body 可能是对象(已解析)或字符串(未解析)
2616
2946
  const body = typeof log.body === 'string' ? JSON.parse(log.body) : log.body;
2617
- if ((_b = body.metadata) === null || _b === void 0 ? void 0 : _b.user_id) {
2947
+ if ((_c = body.metadata) === null || _c === void 0 ? void 0 : _c.user_id) {
2618
2948
  const userId = body.metadata.user_id;
2619
2949
  // 兼容新旧格式:新版本为 JSON 字符串,旧版本为纯字符串
2620
2950
  let extractedSessionId = null;
@@ -2624,7 +2954,7 @@ class FileSystemDatabaseManager {
2624
2954
  extractedSessionId = parsed.session_id;
2625
2955
  }
2626
2956
  }
2627
- catch (_c) {
2957
+ catch (_d) {
2628
2958
  // 不是 JSON,按旧版本纯字符串处理
2629
2959
  extractedSessionId = userId;
2630
2960
  }
@@ -2633,7 +2963,7 @@ class FileSystemDatabaseManager {
2633
2963
  }
2634
2964
  }
2635
2965
  }
2636
- catch (_d) {
2966
+ catch (_e) {
2637
2967
  // 忽略解析错误
2638
2968
  }
2639
2969
  }
@@ -2641,20 +2971,20 @@ class FileSystemDatabaseManager {
2641
2971
  }
2642
2972
  /**
2643
2973
  * 从日志条目中提取 sessionId(用于索引)
2644
- * Codex: headers['session_id']
2974
+ * Codex: headers['session-id'](新版)或 headers['session_id'](旧版)
2645
2975
  * Claude Code: body.metadata.user_id(兼容新旧格式)
2646
2976
  */
2647
2977
  extractSessionIdFromLog(log) {
2648
- var _a, _b;
2649
- // Codex: headers 中的 session_id
2650
- const headerSessionId = (_a = log.headers) === null || _a === void 0 ? void 0 : _a['session_id'];
2978
+ var _a, _b, _c;
2979
+ // Codex: headers 中的 session-id 或 session_id(兼容新旧版本)
2980
+ const headerSessionId = ((_a = log.headers) === null || _a === void 0 ? void 0 : _a['session-id']) || ((_b = log.headers) === null || _b === void 0 ? void 0 : _b['session_id']);
2651
2981
  if (typeof headerSessionId === 'string')
2652
2982
  return headerSessionId;
2653
2983
  // Claude Code: body 中的 metadata.user_id
2654
2984
  if (log.body) {
2655
2985
  try {
2656
2986
  const body = typeof log.body === 'string' ? JSON.parse(log.body) : log.body;
2657
- if ((_b = body.metadata) === null || _b === void 0 ? void 0 : _b.user_id) {
2987
+ if ((_c = body.metadata) === null || _c === void 0 ? void 0 : _c.user_id) {
2658
2988
  const userId = body.metadata.user_id;
2659
2989
  try {
2660
2990
  const parsed = JSON.parse(userId);
@@ -2662,12 +2992,12 @@ class FileSystemDatabaseManager {
2662
2992
  return parsed.session_id;
2663
2993
  }
2664
2994
  }
2665
- catch (_c) {
2995
+ catch (_d) {
2666
2996
  return userId;
2667
2997
  }
2668
2998
  }
2669
2999
  }
2670
- catch (_d) {
3000
+ catch (_e) {
2671
3001
  // 忽略解析错误
2672
3002
  }
2673
3003
  }