aicodeswitch 3.9.4 → 4.0.1

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.
@@ -29,6 +29,19 @@ const promises_1 = __importDefault(require("fs/promises"));
29
29
  const crypto_1 = __importDefault(require("crypto"));
30
30
  const crypto_js_1 = __importDefault(require("crypto-js"));
31
31
  const type_migration_1 = require("./type-migration");
32
+ const VALID_CODEX_REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
33
+ const DEFAULT_CODEX_REASONING_EFFORT = 'high';
34
+ const DEFAULT_FAILOVER_RECOVERY_SECONDS = 30;
35
+ const isCodexReasoningEffort = (value) => {
36
+ return typeof value === 'string' && VALID_CODEX_REASONING_EFFORTS.includes(value);
37
+ };
38
+ const normalizeFailoverRecoverySeconds = (value) => {
39
+ const parsed = typeof value === 'number' ? value : Number(value);
40
+ if (!Number.isFinite(parsed) || parsed <= 0) {
41
+ return DEFAULT_FAILOVER_RECOVERY_SECONDS;
42
+ }
43
+ return Math.floor(parsed);
44
+ };
32
45
  /**
33
46
  * 基于文件系统的数据库管理器
34
47
  * 使用 JSON 文件存储数据,无需编译依赖
@@ -43,6 +56,7 @@ class FileSystemDatabaseManager {
43
56
  get sessionsFile() { return path_1.default.join(this.dataPath, 'sessions.json'); }
44
57
  get logsDir() { return path_1.default.join(this.dataPath, 'logs'); }
45
58
  get logsIndexFile() { return path_1.default.join(this.dataPath, 'logs-index.json'); }
59
+ get sessionLogIndexFile() { return path_1.default.join(this.dataPath, 'session-log-index.json'); }
46
60
  get errorLogsFile() { return path_1.default.join(this.dataPath, 'error-logs.json'); }
47
61
  get blacklistFile() { return path_1.default.join(this.dataPath, 'blacklist.json'); }
48
62
  get statisticsFile() { return path_1.default.join(this.dataPath, 'statistics.json'); }
@@ -120,6 +134,42 @@ class FileSystemDatabaseManager {
120
134
  writable: true,
121
135
  value: []
122
136
  });
137
+ Object.defineProperty(this, "sessionLogIndex", {
138
+ enumerable: true,
139
+ configurable: true,
140
+ writable: true,
141
+ value: new Map()
142
+ });
143
+ Object.defineProperty(this, "sessionLogIndexDirty", {
144
+ enumerable: true,
145
+ configurable: true,
146
+ writable: true,
147
+ value: false
148
+ });
149
+ Object.defineProperty(this, "sessionLogIndexDirtyCount", {
150
+ enumerable: true,
151
+ configurable: true,
152
+ writable: true,
153
+ value: 0
154
+ });
155
+ Object.defineProperty(this, "sessionLogIndexFlushTimer", {
156
+ enumerable: true,
157
+ configurable: true,
158
+ writable: true,
159
+ value: null
160
+ });
161
+ Object.defineProperty(this, "SESSION_LOG_INDEX_FLUSH_DELAY", {
162
+ enumerable: true,
163
+ configurable: true,
164
+ writable: true,
165
+ value: 3000
166
+ });
167
+ Object.defineProperty(this, "SESSION_LOG_INDEX_FLUSH_THRESHOLD", {
168
+ enumerable: true,
169
+ configurable: true,
170
+ writable: true,
171
+ value: 50
172
+ });
123
173
  Object.defineProperty(this, "errorLogs", {
124
174
  enumerable: true,
125
175
  configurable: true,
@@ -209,8 +259,8 @@ class FileSystemDatabaseManager {
209
259
  yield this.loadAllData();
210
260
  // 执行数据源类型迁移(在加载数据之后)
211
261
  yield this.migrateSourceTypes();
212
- // OpenAI base URL 迁移:将末尾 /v1 自动移除
213
- yield this.migrateOpenAIBaseUrls();
262
+ // 路由级工具配置迁移到全局配置(兼容旧版本)
263
+ yield this.migrateRouteToolSettingsToGlobalConfig();
214
264
  // 确保默认配置
215
265
  yield this.ensureDefaultConfig();
216
266
  });
@@ -229,19 +279,43 @@ class FileSystemDatabaseManager {
229
279
  this.loadStatistics(),
230
280
  this.loadMCPs(),
231
281
  ]);
282
+ // 会话日志索引依赖 logShardsIndex,必须在 loadLogsIndex 之后
283
+ yield this.loadSessionLogIndex();
232
284
  });
233
285
  }
234
286
  loadVendors() {
235
287
  return __awaiter(this, void 0, void 0, function* () {
288
+ let needSave = false;
236
289
  try {
237
290
  const data = yield promises_1.default.readFile(this.vendorsFile, 'utf-8');
238
- this.vendors = JSON.parse(data);
291
+ const parsed = JSON.parse(data);
292
+ this.vendors = Array.isArray(parsed) ? parsed.map((vendor) => {
293
+ const normalizedServices = Array.isArray(vendor.services)
294
+ ? vendor.services.map((service) => {
295
+ const normalizedService = Object.assign(Object.assign({}, service), { apiKey: typeof service.apiKey === 'string' ? service.apiKey : '', inheritVendorApiKey: service.inheritVendorApiKey === true });
296
+ if (normalizedService.apiKey !== service.apiKey ||
297
+ normalizedService.inheritVendorApiKey !== service.inheritVendorApiKey) {
298
+ needSave = true;
299
+ }
300
+ return normalizedService;
301
+ })
302
+ : [];
303
+ const normalizedVendor = Object.assign(Object.assign({}, vendor), { apiKey: typeof vendor.apiKey === 'string' ? vendor.apiKey : '', services: normalizedServices });
304
+ if (normalizedVendor.apiKey !== vendor.apiKey ||
305
+ !Array.isArray(vendor.services)) {
306
+ needSave = true;
307
+ }
308
+ return normalizedVendor;
309
+ }) : [];
239
310
  }
240
311
  catch (_a) {
241
312
  this.vendors = [];
242
313
  }
243
314
  // 兼容性检查:如果存在旧的 services.json,自动迁移
244
315
  yield this.migrateServicesIfNeeded();
316
+ if (needSave) {
317
+ yield this.saveVendors();
318
+ }
245
319
  });
246
320
  }
247
321
  /**
@@ -359,41 +433,6 @@ class FileSystemDatabaseManager {
359
433
  console.log(`[TypeMigration] Migration completed. Migrated ${migratedCount} services.`);
360
434
  });
361
435
  }
362
- /**
363
- * 迁移 OpenAI base URL(在初始化时执行)
364
- * 仅处理 sourceType=openai 且 apiUrl 末尾为 /v1 的服务
365
- */
366
- migrateOpenAIBaseUrls() {
367
- return __awaiter(this, void 0, void 0, function* () {
368
- console.log('[OpenAIBaseUrlMigration] Checking for OpenAI base URL migration...');
369
- let migratedCount = 0;
370
- for (const vendor of this.vendors) {
371
- if (!vendor.services)
372
- continue;
373
- for (const service of vendor.services) {
374
- if (service.sourceType !== 'openai' || typeof service.apiUrl !== 'string') {
375
- continue;
376
- }
377
- const trimmedUrl = service.apiUrl.trim();
378
- if (!/\/v1\/?$/i.test(trimmedUrl)) {
379
- continue;
380
- }
381
- const migratedUrl = trimmedUrl.replace(/\/v1\/?$/i, '');
382
- if (migratedUrl && migratedUrl !== service.apiUrl) {
383
- console.log(`[OpenAIBaseUrlMigration] Migrated service "${service.name}": ${service.apiUrl} -> ${migratedUrl}`);
384
- service.apiUrl = migratedUrl;
385
- migratedCount++;
386
- }
387
- }
388
- }
389
- if (migratedCount === 0) {
390
- console.log('[OpenAIBaseUrlMigration] No migration needed');
391
- return;
392
- }
393
- yield this.saveVendors();
394
- console.log(`[OpenAIBaseUrlMigration] Migration completed. Migrated ${migratedCount} services.`);
395
- });
396
- }
397
436
  /**
398
437
  * 迁移导入数据中的类型
399
438
  * 用于导入功能,自动将旧类型转换为新类型
@@ -402,11 +441,7 @@ class FileSystemDatabaseManager {
402
441
  return vendors.map(vendor => {
403
442
  var _a;
404
443
  return (Object.assign(Object.assign({}, vendor), { services: (_a = vendor.services) === null || _a === void 0 ? void 0 : _a.map(service => {
405
- const normalizedSourceType = service.sourceType ? (0, type_migration_1.normalizeSourceType)(service.sourceType) : undefined;
406
- const normalizedApiUrl = normalizedSourceType === 'openai' && typeof service.apiUrl === 'string'
407
- ? service.apiUrl.trim().replace(/\/v1\/?$/i, '')
408
- : service.apiUrl;
409
- return Object.assign(Object.assign({}, service), { sourceType: normalizedSourceType, apiUrl: normalizedApiUrl });
444
+ return Object.assign(Object.assign({}, service), { sourceType: service.sourceType ? (0, type_migration_1.normalizeSourceType)(service.sourceType) : undefined });
410
445
  }) }));
411
446
  });
412
447
  }
@@ -575,6 +610,96 @@ class FileSystemDatabaseManager {
575
610
  yield promises_1.default.writeFile(this.logsIndexFile, JSON.stringify(this.logShardsIndex, null, 2));
576
611
  });
577
612
  }
613
+ /**
614
+ * 加载会话日志索引,若不存在则从现有日志全量构建
615
+ */
616
+ loadSessionLogIndex() {
617
+ return __awaiter(this, void 0, void 0, function* () {
618
+ try {
619
+ const data = yield promises_1.default.readFile(this.sessionLogIndexFile, 'utf-8');
620
+ const parsed = JSON.parse(data);
621
+ this.sessionLogIndex = new Map(Object.entries(parsed));
622
+ console.log(`[Database] Session log index loaded: ${this.sessionLogIndex.size} sessions`);
623
+ }
624
+ catch (_a) {
625
+ // 索引文件不存在,从现有日志全量构建
626
+ console.log('[Database] Session log index not found, building from existing logs...');
627
+ yield this.buildSessionLogIndex();
628
+ }
629
+ });
630
+ }
631
+ /**
632
+ * 从所有现有日志分片全量构建会话日志索引(首次启动或迁移用)
633
+ */
634
+ buildSessionLogIndex() {
635
+ return __awaiter(this, void 0, void 0, function* () {
636
+ this.sessionLogIndex.clear();
637
+ for (const shard of this.logShardsIndex) {
638
+ const shardLogs = yield this.loadLogShard(shard.filename);
639
+ for (let i = 0; i < shardLogs.length; i++) {
640
+ const sessionId = this.extractSessionIdFromLog(shardLogs[i]);
641
+ if (sessionId) {
642
+ let refs = this.sessionLogIndex.get(sessionId);
643
+ if (!refs) {
644
+ refs = [];
645
+ this.sessionLogIndex.set(sessionId, refs);
646
+ }
647
+ refs.push({ filename: shard.filename, index: i, timestamp: shardLogs[i].timestamp });
648
+ }
649
+ }
650
+ }
651
+ if (this.sessionLogIndex.size > 0) {
652
+ yield this.saveSessionLogIndexNow();
653
+ console.log(`[Database] Session log index built: ${this.sessionLogIndex.size} sessions indexed`);
654
+ }
655
+ });
656
+ }
657
+ /**
658
+ * 将会话日志索引写入磁盘
659
+ */
660
+ saveSessionLogIndexNow() {
661
+ return __awaiter(this, void 0, void 0, function* () {
662
+ const obj = {};
663
+ for (const [key, refs] of this.sessionLogIndex) {
664
+ obj[key] = refs;
665
+ }
666
+ yield promises_1.default.writeFile(this.sessionLogIndexFile, JSON.stringify(obj));
667
+ });
668
+ }
669
+ /**
670
+ * 标记索引脏数据,触发防抖写盘
671
+ */
672
+ scheduleSessionLogIndexFlush() {
673
+ this.sessionLogIndexDirty = true;
674
+ this.sessionLogIndexDirtyCount++;
675
+ // 达到阈值立即刷盘
676
+ if (this.sessionLogIndexDirtyCount >= this.SESSION_LOG_INDEX_FLUSH_THRESHOLD) {
677
+ this.flushSessionLogIndex();
678
+ return;
679
+ }
680
+ // 防抖定时器
681
+ if (!this.sessionLogIndexFlushTimer) {
682
+ this.sessionLogIndexFlushTimer = setTimeout(() => {
683
+ this.flushSessionLogIndex();
684
+ }, this.SESSION_LOG_INDEX_FLUSH_DELAY);
685
+ }
686
+ }
687
+ /**
688
+ * 立即将索引刷盘(关闭时调用)
689
+ */
690
+ flushSessionLogIndex() {
691
+ if (this.sessionLogIndexFlushTimer) {
692
+ clearTimeout(this.sessionLogIndexFlushTimer);
693
+ this.sessionLogIndexFlushTimer = null;
694
+ }
695
+ if (this.sessionLogIndexDirty) {
696
+ this.sessionLogIndexDirty = false;
697
+ this.sessionLogIndexDirtyCount = 0;
698
+ this.saveSessionLogIndexNow().catch(err => {
699
+ console.error('[Database] Failed to flush session log index:', err);
700
+ });
701
+ }
702
+ }
578
703
  /**
579
704
  * 迁移旧的 logs.json 文件到新的分片格式
580
705
  */
@@ -687,6 +812,18 @@ class FileSystemDatabaseManager {
687
812
  this.logShardsIndex = this.logShardsIndex.filter(s => !toDelete.includes(s.filename));
688
813
  if (toDelete.length > 0) {
689
814
  yield this.saveLogsIndex();
815
+ // 同步清理会话日志索引中被删除分片的条目
816
+ const deleteSet = new Set(toDelete);
817
+ for (const [sid, refs] of this.sessionLogIndex) {
818
+ const remaining = refs.filter(r => !deleteSet.has(r.filename));
819
+ if (remaining.length === 0) {
820
+ this.sessionLogIndex.delete(sid);
821
+ }
822
+ else if (remaining.length < refs.length) {
823
+ this.sessionLogIndex.set(sid, remaining);
824
+ }
825
+ }
826
+ yield this.saveSessionLogIndexNow();
690
827
  }
691
828
  });
692
829
  }
@@ -827,22 +964,89 @@ class FileSystemDatabaseManager {
827
964
  }
828
965
  ensureDefaultConfig() {
829
966
  return __awaiter(this, void 0, void 0, function* () {
830
- if (!this.config) {
831
- this.config = {
832
- enableLogging: true,
833
- logRetentionDays: 30,
834
- maxLogSize: 100000,
835
- apiKey: '',
836
- enableFailover: true,
837
- proxyEnabled: false,
838
- proxyUrl: '',
839
- proxyUsername: '',
840
- proxyPassword: '',
841
- };
967
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
968
+ const current = this.config;
969
+ this.config = {
970
+ enableLogging: (_a = current === null || current === void 0 ? void 0 : current.enableLogging) !== null && _a !== void 0 ? _a : true,
971
+ logRetentionDays: (_b = current === null || current === void 0 ? void 0 : current.logRetentionDays) !== null && _b !== void 0 ? _b : 30,
972
+ maxLogSize: (_c = current === null || current === void 0 ? void 0 : current.maxLogSize) !== null && _c !== void 0 ? _c : 100000,
973
+ apiKey: (_d = current === null || current === void 0 ? void 0 : current.apiKey) !== null && _d !== void 0 ? _d : '',
974
+ enableFailover: (_e = current === null || current === void 0 ? void 0 : current.enableFailover) !== null && _e !== void 0 ? _e : true,
975
+ failoverRecoverySeconds: normalizeFailoverRecoverySeconds(current === null || current === void 0 ? void 0 : current.failoverRecoverySeconds),
976
+ enableAgentTeams: (_f = current === null || current === void 0 ? void 0 : current.enableAgentTeams) !== null && _f !== void 0 ? _f : false,
977
+ enableBypassPermissionsSupport: (_g = current === null || current === void 0 ? void 0 : current.enableBypassPermissionsSupport) !== null && _g !== void 0 ? _g : false,
978
+ codexModelReasoningEffort: isCodexReasoningEffort(current === null || current === void 0 ? void 0 : current.codexModelReasoningEffort)
979
+ ? current.codexModelReasoningEffort
980
+ : DEFAULT_CODEX_REASONING_EFFORT,
981
+ proxyEnabled: (_h = current === null || current === void 0 ? void 0 : current.proxyEnabled) !== null && _h !== void 0 ? _h : false,
982
+ proxyUrl: (_j = current === null || current === void 0 ? void 0 : current.proxyUrl) !== null && _j !== void 0 ? _j : '',
983
+ proxyUsername: (_k = current === null || current === void 0 ? void 0 : current.proxyUsername) !== null && _k !== void 0 ? _k : '',
984
+ proxyPassword: (_l = current === null || current === void 0 ? void 0 : current.proxyPassword) !== null && _l !== void 0 ? _l : '',
985
+ };
986
+ // 仅在首次创建或存在字段补齐时落盘
987
+ if (!current || JSON.stringify(current) !== JSON.stringify(this.config)) {
842
988
  yield this.saveConfig();
843
989
  }
844
990
  });
845
991
  }
992
+ migrateRouteToolSettingsToGlobalConfig() {
993
+ return __awaiter(this, void 0, void 0, function* () {
994
+ const hasGlobalToolConfig = !!this.config &&
995
+ (Object.prototype.hasOwnProperty.call(this.config, 'enableAgentTeams') ||
996
+ Object.prototype.hasOwnProperty.call(this.config, 'enableBypassPermissionsSupport') ||
997
+ Object.prototype.hasOwnProperty.call(this.config, 'codexModelReasoningEffort'));
998
+ const getPreferredRoute = (targetType) => {
999
+ const activeRoute = this.routes.find(route => route.targetType === targetType && route.isActive);
1000
+ if (activeRoute) {
1001
+ return activeRoute;
1002
+ }
1003
+ return this.routes.find(route => route.targetType === targetType);
1004
+ };
1005
+ let configUpdated = false;
1006
+ if (!hasGlobalToolConfig) {
1007
+ const preferredClaudeRoute = getPreferredRoute('claude-code');
1008
+ const preferredCodexRoute = getPreferredRoute('codex');
1009
+ const nextConfig = Object.assign({}, (this.config || {}));
1010
+ if (typeof (preferredClaudeRoute === null || preferredClaudeRoute === void 0 ? void 0 : preferredClaudeRoute.enableAgentTeams) === 'boolean') {
1011
+ nextConfig.enableAgentTeams = preferredClaudeRoute.enableAgentTeams;
1012
+ configUpdated = true;
1013
+ }
1014
+ if (typeof (preferredClaudeRoute === null || preferredClaudeRoute === void 0 ? void 0 : preferredClaudeRoute.enableBypassPermissionsSupport) === 'boolean') {
1015
+ nextConfig.enableBypassPermissionsSupport = preferredClaudeRoute.enableBypassPermissionsSupport;
1016
+ configUpdated = true;
1017
+ }
1018
+ if (isCodexReasoningEffort(preferredCodexRoute === null || preferredCodexRoute === void 0 ? void 0 : preferredCodexRoute.codexModelReasoningEffort)) {
1019
+ nextConfig.codexModelReasoningEffort = preferredCodexRoute.codexModelReasoningEffort;
1020
+ configUpdated = true;
1021
+ }
1022
+ if (configUpdated) {
1023
+ this.config = nextConfig;
1024
+ yield this.saveConfig();
1025
+ console.log('[ConfigMigration] Migrated route-level tool settings to global app config');
1026
+ }
1027
+ }
1028
+ // 清理路由中的旧字段,避免后续重复歧义
1029
+ let routesUpdated = false;
1030
+ this.routes = this.routes.map((route) => {
1031
+ const hasLegacyFields = Object.prototype.hasOwnProperty.call(route, 'enableAgentTeams') ||
1032
+ Object.prototype.hasOwnProperty.call(route, 'enableBypassPermissionsSupport') ||
1033
+ Object.prototype.hasOwnProperty.call(route, 'codexModelReasoningEffort');
1034
+ if (!hasLegacyFields) {
1035
+ return route;
1036
+ }
1037
+ routesUpdated = true;
1038
+ const cleanedRoute = Object.assign({}, route);
1039
+ delete cleanedRoute.enableAgentTeams;
1040
+ delete cleanedRoute.enableBypassPermissionsSupport;
1041
+ delete cleanedRoute.codexModelReasoningEffort;
1042
+ return cleanedRoute;
1043
+ });
1044
+ if (routesUpdated) {
1045
+ yield this.saveRoutes();
1046
+ console.log('[ConfigMigration] Removed deprecated route-level tool settings');
1047
+ }
1048
+ });
1049
+ }
846
1050
  // Vendor operations
847
1051
  getVendors() {
848
1052
  return [...this.vendors].sort((a, b) => {
@@ -861,7 +1065,7 @@ class FileSystemDatabaseManager {
861
1065
  console.log('[数据库] 创建供应商,输入数据:', JSON.stringify(vendor, null, 2));
862
1066
  const id = crypto_1.default.randomUUID();
863
1067
  const now = Date.now();
864
- const newVendor = Object.assign(Object.assign({}, vendor), { id, services: vendor.services || [], createdAt: now, updatedAt: now });
1068
+ const newVendor = Object.assign(Object.assign({}, vendor), { apiKey: typeof vendor.apiKey === 'string' ? vendor.apiKey : '', id, services: vendor.services || [], createdAt: now, updatedAt: now });
865
1069
  console.log('[数据库] 创建供应商,返回数据:', JSON.stringify(newVendor, null, 2));
866
1070
  this.vendors.push(newVendor);
867
1071
  yield this.saveVendors();
@@ -874,7 +1078,11 @@ class FileSystemDatabaseManager {
874
1078
  if (index === -1)
875
1079
  return false;
876
1080
  const now = Date.now();
877
- this.vendors[index] = Object.assign(Object.assign(Object.assign({}, this.vendors[index]), vendor), { id, services: vendor.services !== undefined ? vendor.services : this.vendors[index].services, updatedAt: now });
1081
+ this.vendors[index] = Object.assign(Object.assign(Object.assign({}, this.vendors[index]), vendor), { id, apiKey: typeof vendor.apiKey === 'string'
1082
+ ? vendor.apiKey
1083
+ : (this.vendors[index].apiKey || ''),
1084
+ // 供应商服务应通过 create/update/deleteAPIService 单独维护,避免编辑供应商时误覆盖
1085
+ services: this.vendors[index].services, updatedAt: now });
878
1086
  yield this.saveVendors();
879
1087
  return true;
880
1088
  });
@@ -884,12 +1092,15 @@ class FileSystemDatabaseManager {
884
1092
  const index = this.vendors.findIndex(v => v.id === id);
885
1093
  if (index === -1)
886
1094
  return false;
887
- // 检查是否有服务被规则使用
1095
+ // 级联删除:删除该供应商下服务关联的所有规则
888
1096
  const vendor = this.vendors[index];
889
1097
  const serviceIds = (vendor.services || []).map(s => s.id);
890
- const rulesUsingServices = this.rules.filter(r => serviceIds.includes(r.targetServiceId));
891
- if (rulesUsingServices.length > 0) {
892
- throw new Error(`无法删除供应商:有 ${rulesUsingServices.length} 个路由规则正在使用该供应商的服务`);
1098
+ if (serviceIds.length > 0) {
1099
+ const beforeCount = this.rules.length;
1100
+ this.rules = this.rules.filter(r => !serviceIds.includes(r.targetServiceId));
1101
+ if (this.rules.length !== beforeCount) {
1102
+ yield this.saveRules();
1103
+ }
893
1104
  }
894
1105
  this.vendors.splice(index, 1);
895
1106
  yield this.saveVendors();
@@ -954,7 +1165,7 @@ class FileSystemDatabaseManager {
954
1165
  const _a = service, { vendorId: _ } = _a, serviceData = __rest(_a, ["vendorId"]);
955
1166
  const id = crypto_1.default.randomUUID();
956
1167
  const now = Date.now();
957
- const newService = Object.assign(Object.assign({}, serviceData), { id, createdAt: now, updatedAt: now });
1168
+ const newService = Object.assign(Object.assign({}, serviceData), { apiKey: typeof serviceData.apiKey === 'string' ? serviceData.apiKey : '', inheritVendorApiKey: serviceData.inheritVendorApiKey === true, id, createdAt: now, updatedAt: now });
958
1169
  console.log('[数据库] 创建服务,最终数据:', JSON.stringify(newService, null, 2));
959
1170
  if (!vendor.services) {
960
1171
  vendor.services = [];
@@ -977,7 +1188,9 @@ class FileSystemDatabaseManager {
977
1188
  if (index === -1)
978
1189
  return false;
979
1190
  const now = Date.now();
980
- vendor.services[index] = Object.assign(Object.assign(Object.assign({}, vendor.services[index]), service), { id, updatedAt: now });
1191
+ 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
1192
+ ? service.inheritVendorApiKey === true
1193
+ : vendor.services[index].inheritVendorApiKey === true, updatedAt: now });
981
1194
  // 更新供应商的 updatedAt 时间
982
1195
  vendor.updatedAt = now;
983
1196
  yield this.saveVendors();
@@ -995,10 +1208,11 @@ class FileSystemDatabaseManager {
995
1208
  const index = vendor.services.findIndex(s => s.id === id);
996
1209
  if (index === -1)
997
1210
  return false;
998
- // 检查是否有规则正在使用此服务
999
- const rulesUsingService = this.rules.filter(r => r.targetServiceId === id);
1000
- if (rulesUsingService.length > 0) {
1001
- throw new Error(`无法删除服务:有 ${rulesUsingService.length} 个路由规则正在使用此服务`);
1211
+ // 级联删除:删除使用该服务的所有规则
1212
+ const beforeCount = this.rules.length;
1213
+ this.rules = this.rules.filter(r => r.targetServiceId !== id);
1214
+ if (this.rules.length !== beforeCount) {
1215
+ yield this.saveRules();
1002
1216
  }
1003
1217
  vendor.services.splice(index, 1);
1004
1218
  // 更新供应商的 updatedAt 时间
@@ -1340,6 +1554,17 @@ class FileSystemDatabaseManager {
1340
1554
  yield this.updateStatistics(logWithId);
1341
1555
  // 清除计数缓存
1342
1556
  this.logsCountCache = null;
1557
+ // 更新会话日志索引
1558
+ const sessionId = this.extractSessionIdFromLog(logWithId);
1559
+ if (sessionId) {
1560
+ let refs = this.sessionLogIndex.get(sessionId);
1561
+ if (!refs) {
1562
+ refs = [];
1563
+ this.sessionLogIndex.set(sessionId, refs);
1564
+ }
1565
+ refs.push({ filename, index: shardLogs.length - 1, timestamp: logWithId.timestamp });
1566
+ this.scheduleSessionLogIndexFlush();
1567
+ }
1343
1568
  });
1344
1569
  }
1345
1570
  getLogs() {
@@ -1602,7 +1827,56 @@ class FileSystemDatabaseManager {
1602
1827
  }
1603
1828
  return false;
1604
1829
  }
1830
+ /**
1831
+ * 获取状态码为 499 的请求日志
1832
+ * @param limit 返回数量限制
1833
+ * @param offset 偏移量
1834
+ * @returns 匹配的请求日志列表
1835
+ */
1836
+ getClientClosedLogs() {
1837
+ return __awaiter(this, arguments, void 0, function* (limit = 100, offset = 0) {
1838
+ const allMatches = [];
1839
+ const sortedShards = [...this.logShardsIndex].sort((a, b) => b.endTime - a.endTime);
1840
+ // 递序遍历所有分片, collect 499 logs
1841
+ for (const shard of sortedShards) {
1842
+ const shardLogs = yield this.loadLogShard(shard.filename);
1843
+ for (const log of shardLogs) {
1844
+ if (log.statusCode === 499) {
1845
+ allMatches.push(log);
1846
+ }
1847
+ }
1848
+ }
1849
+ // 按时间倒序排列并分页
1850
+ return allMatches
1851
+ .sort((a, b) => b.timestamp - a.timestamp)
1852
+ .slice(offset, offset + limit);
1853
+ });
1854
+ }
1855
+ /**
1856
+ * 获取状态码为 499 的请求日志数量
1857
+ * @returns 匹配的请求数量
1858
+ */
1859
+ getClientClosedLogsCount() {
1860
+ return __awaiter(this, void 0, void 0, function* () {
1861
+ let count = 0;
1862
+ for (const shard of this.logShardsIndex) {
1863
+ const shardLogs = yield this.loadLogShard(shard.filename);
1864
+ for (const log of shardLogs) {
1865
+ if (log.statusCode === 499) {
1866
+ count++;
1867
+ }
1868
+ }
1869
+ }
1870
+ return count;
1871
+ });
1872
+ }
1873
+ // Service blacklist operations
1605
1874
  // Service blacklist operations
1875
+ getFailoverRecoveryMs() {
1876
+ var _a;
1877
+ const seconds = normalizeFailoverRecoverySeconds((_a = this.config) === null || _a === void 0 ? void 0 : _a.failoverRecoverySeconds);
1878
+ return seconds * 1000;
1879
+ }
1606
1880
  isServiceBlacklisted(serviceId, routeId, contentType) {
1607
1881
  return __awaiter(this, void 0, void 0, function* () {
1608
1882
  const key = `${routeId}:${contentType}:${serviceId}`;
@@ -1621,10 +1895,11 @@ class FileSystemDatabaseManager {
1621
1895
  return __awaiter(this, void 0, void 0, function* () {
1622
1896
  const key = `${routeId}:${contentType}:${serviceId}`;
1623
1897
  const now = Date.now();
1898
+ const recoveryMs = this.getFailoverRecoveryMs();
1624
1899
  const existing = this.blacklist.get(key);
1625
1900
  if (existing) {
1626
1901
  existing.blacklistedAt = now;
1627
- existing.expiresAt = now + 2 * 60 * 1000; // 2分钟黑名单(从10分钟缩短)
1902
+ existing.expiresAt = now + recoveryMs;
1628
1903
  existing.errorCount++;
1629
1904
  existing.lastError = errorMessage;
1630
1905
  existing.lastStatusCode = statusCode;
@@ -1636,7 +1911,7 @@ class FileSystemDatabaseManager {
1636
1911
  routeId,
1637
1912
  contentType,
1638
1913
  blacklistedAt: now,
1639
- expiresAt: now + 2 * 60 * 1000, // 2分钟黑名单(从10分钟缩短)
1914
+ expiresAt: now + recoveryMs,
1640
1915
  errorCount: 1,
1641
1916
  lastError: errorMessage,
1642
1917
  lastStatusCode: statusCode,
@@ -1675,7 +1950,12 @@ class FileSystemDatabaseManager {
1675
1950
  }
1676
1951
  updateConfig(config) {
1677
1952
  return __awaiter(this, void 0, void 0, function* () {
1678
- this.config = config;
1953
+ const merged = Object.assign(Object.assign({}, (this.config || {})), config);
1954
+ if (!isCodexReasoningEffort(merged.codexModelReasoningEffort)) {
1955
+ merged.codexModelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT;
1956
+ }
1957
+ merged.failoverRecoverySeconds = normalizeFailoverRecoverySeconds(merged.failoverRecoverySeconds);
1958
+ this.config = merged;
1679
1959
  yield this.saveConfig();
1680
1960
  return true;
1681
1961
  });
@@ -1693,6 +1973,9 @@ class FileSystemDatabaseManager {
1693
1973
  if (!vendor.name || typeof vendor.name !== 'string') {
1694
1974
  return { valid: false, error: `供应商[${index}](${vendor.id}) 缺少有效的 name 字段` };
1695
1975
  }
1976
+ if (vendor.apiKey !== undefined && typeof vendor.apiKey !== 'string') {
1977
+ return { valid: false, error: `供应商[${index}](${vendor.id}) 的 apiKey 必须是字符串` };
1978
+ }
1696
1979
  if (!Array.isArray(vendor.services)) {
1697
1980
  return { valid: false, error: `供应商[${index}](${vendor.id}) 的 services 不是数组` };
1698
1981
  }
@@ -1711,7 +1994,12 @@ class FileSystemDatabaseManager {
1711
1994
  return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] 缺少有效的 apiUrl 字段` };
1712
1995
  }
1713
1996
  if (!service.apiKey || typeof service.apiKey !== 'string') {
1714
- return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] 缺少有效的 apiKey 字段` };
1997
+ if (service.inheritVendorApiKey !== true) {
1998
+ return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] 缺少有效的 apiKey 字段` };
1999
+ }
2000
+ }
2001
+ if (service.inheritVendorApiKey !== undefined && typeof service.inheritVendorApiKey !== 'boolean') {
2002
+ return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] inheritVendorApiKey 必须是布尔值` };
1715
2003
  }
1716
2004
  }
1717
2005
  return { valid: true };
@@ -1898,6 +2186,7 @@ class FileSystemDatabaseManager {
1898
2186
  }
1899
2187
  importData(encryptedData, password) {
1900
2188
  return __awaiter(this, void 0, void 0, function* () {
2189
+ var _a;
1901
2190
  try {
1902
2191
  // 解密
1903
2192
  let jsonData;
@@ -1933,7 +2222,7 @@ class FileSystemDatabaseManager {
1933
2222
  this.vendors = importData.vendors.map((v) => (Object.assign(Object.assign({}, v), { updatedAt: now })));
1934
2223
  this.routes = importData.routes.map((r) => (Object.assign(Object.assign({}, r), { updatedAt: now })));
1935
2224
  this.rules = importData.rules.map((r) => (Object.assign(Object.assign({}, r), { updatedAt: now })));
1936
- this.config = Object.assign(Object.assign({}, importData.config), { updatedAt: now });
2225
+ this.config = Object.assign(Object.assign({}, importData.config), { failoverRecoverySeconds: normalizeFailoverRecoverySeconds((_a = importData.config) === null || _a === void 0 ? void 0 : _a.failoverRecoverySeconds), updatedAt: now });
1937
2226
  // 保存数据
1938
2227
  yield Promise.all([
1939
2228
  this.saveVendors(),
@@ -2244,8 +2533,37 @@ class FileSystemDatabaseManager {
2244
2533
  }
2245
2534
  getLogsBySessionId(sessionId_1) {
2246
2535
  return __awaiter(this, arguments, void 0, function* (sessionId, limit = 100) {
2536
+ const refs = this.sessionLogIndex.get(sessionId);
2537
+ // 有索引:仅加载相关分片,按 index 直接取值
2538
+ if (refs && refs.length > 0) {
2539
+ // 按 filename 分组,避免重复加载同一分片
2540
+ const shardMap = new Map();
2541
+ for (const ref of refs) {
2542
+ let indices = shardMap.get(ref.filename);
2543
+ if (!indices) {
2544
+ indices = [];
2545
+ shardMap.set(ref.filename, indices);
2546
+ }
2547
+ indices.push(ref.index);
2548
+ }
2549
+ const logs = [];
2550
+ for (const [filename, indices] of shardMap) {
2551
+ try {
2552
+ const shardLogs = yield this.loadLogShard(filename);
2553
+ for (const idx of indices) {
2554
+ if (idx >= 0 && idx < shardLogs.length) {
2555
+ logs.push(shardLogs[idx]);
2556
+ }
2557
+ }
2558
+ }
2559
+ catch (_a) {
2560
+ // 分片文件可能已被清理,跳过
2561
+ }
2562
+ }
2563
+ return logs.sort((a, b) => b.timestamp - a.timestamp).slice(0, limit);
2564
+ }
2565
+ // 无索引(兼容旧数据):回退到全扫描
2247
2566
  const allLogs = [];
2248
- // 遍历所有分片
2249
2567
  for (const shard of this.logShardsIndex) {
2250
2568
  const shardLogs = yield this.loadLogShard(shard.filename);
2251
2569
  const filtered = shardLogs.filter(log => this.isLogBelongsToSession(log, sessionId));
@@ -2268,22 +2586,73 @@ class FileSystemDatabaseManager {
2268
2586
  try {
2269
2587
  // body 可能是对象(已解析)或字符串(未解析)
2270
2588
  const body = typeof log.body === 'string' ? JSON.parse(log.body) : log.body;
2271
- if (((_b = body.metadata) === null || _b === void 0 ? void 0 : _b.user_id) === sessionId) {
2272
- return true;
2589
+ if ((_b = body.metadata) === null || _b === void 0 ? void 0 : _b.user_id) {
2590
+ const userId = body.metadata.user_id;
2591
+ // 兼容新旧格式:新版本为 JSON 字符串,旧版本为纯字符串
2592
+ let extractedSessionId = null;
2593
+ try {
2594
+ const parsed = JSON.parse(userId);
2595
+ if (parsed && typeof parsed === 'object' && parsed.session_id) {
2596
+ extractedSessionId = parsed.session_id;
2597
+ }
2598
+ }
2599
+ catch (_c) {
2600
+ // 不是 JSON,按旧版本纯字符串处理
2601
+ extractedSessionId = userId;
2602
+ }
2603
+ if (extractedSessionId === sessionId) {
2604
+ return true;
2605
+ }
2273
2606
  }
2274
2607
  }
2275
- catch (_c) {
2608
+ catch (_d) {
2276
2609
  // 忽略解析错误
2277
2610
  }
2278
2611
  }
2279
2612
  return false;
2280
2613
  }
2614
+ /**
2615
+ * 从日志条目中提取 sessionId(用于索引)
2616
+ * Codex: headers['session_id']
2617
+ * Claude Code: body.metadata.user_id(兼容新旧格式)
2618
+ */
2619
+ extractSessionIdFromLog(log) {
2620
+ var _a, _b;
2621
+ // Codex: headers 中的 session_id
2622
+ const headerSessionId = (_a = log.headers) === null || _a === void 0 ? void 0 : _a['session_id'];
2623
+ if (typeof headerSessionId === 'string')
2624
+ return headerSessionId;
2625
+ // Claude Code: body 中的 metadata.user_id
2626
+ if (log.body) {
2627
+ try {
2628
+ const body = typeof log.body === 'string' ? JSON.parse(log.body) : log.body;
2629
+ if ((_b = body.metadata) === null || _b === void 0 ? void 0 : _b.user_id) {
2630
+ const userId = body.metadata.user_id;
2631
+ try {
2632
+ const parsed = JSON.parse(userId);
2633
+ if (parsed && typeof parsed === 'object' && parsed.session_id) {
2634
+ return parsed.session_id;
2635
+ }
2636
+ }
2637
+ catch (_c) {
2638
+ return userId;
2639
+ }
2640
+ }
2641
+ }
2642
+ catch (_d) {
2643
+ // 忽略解析错误
2644
+ }
2645
+ }
2646
+ return null;
2647
+ }
2281
2648
  deleteSession(sessionId) {
2282
2649
  return __awaiter(this, void 0, void 0, function* () {
2283
2650
  const index = this.sessions.findIndex(s => s.id === sessionId);
2284
2651
  if (index === -1)
2285
2652
  return false;
2286
2653
  this.sessions.splice(index, 1);
2654
+ this.sessionLogIndex.delete(sessionId);
2655
+ this.scheduleSessionLogIndexFlush();
2287
2656
  yield this.saveSessions();
2288
2657
  return true;
2289
2658
  });
@@ -2291,6 +2660,8 @@ class FileSystemDatabaseManager {
2291
2660
  clearSessions() {
2292
2661
  return __awaiter(this, void 0, void 0, function* () {
2293
2662
  this.sessions = [];
2663
+ this.sessionLogIndex.clear();
2664
+ this.scheduleSessionLogIndexFlush();
2294
2665
  yield this.saveSessions();
2295
2666
  });
2296
2667
  }
@@ -2405,8 +2776,8 @@ class FileSystemDatabaseManager {
2405
2776
  }
2406
2777
  // Close method for compatibility (no-op for filesystem database)
2407
2778
  close() {
2408
- // 文件系统数据库不需要关闭连接
2409
- // 所有数据已经持久化到文件
2779
+ // 刷盘会话日志索引
2780
+ this.flushSessionLogIndex();
2410
2781
  }
2411
2782
  }
2412
2783
  exports.FileSystemDatabaseManager = FileSystemDatabaseManager;