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.
- package/README.md +6 -5
- package/UPGRADE.md +5 -6
- package/dist/server/coding-plan.js +94 -0
- package/dist/server/config-managed-fields.js +1 -0
- package/dist/server/conversions/compact.js +613 -0
- package/dist/server/conversions/detector.js +70 -0
- package/dist/server/conversions/index.js +290 -0
- package/dist/server/conversions/pairs/claude-completions/request.js +167 -0
- package/dist/server/conversions/pairs/claude-completions/response.js +56 -0
- package/dist/server/conversions/pairs/claude-completions/streaming.js +259 -0
- package/dist/server/conversions/pairs/claude-gemini/request.js +130 -0
- package/dist/server/conversions/pairs/claude-gemini/response.js +65 -0
- package/dist/server/conversions/pairs/claude-gemini/streaming.js +199 -0
- package/dist/server/conversions/pairs/claude-responses/request.js +190 -0
- package/dist/server/conversions/pairs/claude-responses/response.js +89 -0
- package/dist/server/conversions/pairs/claude-responses/streaming.js +266 -0
- package/dist/server/conversions/pairs/completions-claude/request.js +111 -0
- package/dist/server/conversions/pairs/completions-claude/response.js +67 -0
- package/dist/server/conversions/pairs/completions-claude/streaming.js +165 -0
- package/dist/server/conversions/pairs/completions-gemini/request.js +169 -0
- package/dist/server/conversions/pairs/completions-gemini/response.js +70 -0
- package/dist/server/conversions/pairs/completions-gemini/streaming.js +132 -0
- package/dist/server/conversions/pairs/completions-responses/request.js +149 -0
- package/dist/server/conversions/pairs/completions-responses/response.js +74 -0
- package/dist/server/conversions/pairs/completions-responses/streaming.js +189 -0
- package/dist/server/conversions/pairs/gemini-claude/request.js +118 -0
- package/dist/server/conversions/pairs/gemini-claude/response.js +45 -0
- package/dist/server/conversions/pairs/gemini-claude/streaming.js +146 -0
- package/dist/server/conversions/pairs/gemini-completions/request.js +151 -0
- package/dist/server/conversions/pairs/gemini-completions/response.js +54 -0
- package/dist/server/conversions/pairs/gemini-completions/streaming.js +108 -0
- package/dist/server/conversions/pairs/gemini-responses/request.js +18 -0
- package/dist/server/conversions/pairs/gemini-responses/response.js +18 -0
- package/dist/server/conversions/pairs/gemini-responses/streaming.js +43 -0
- package/dist/server/conversions/pairs/responses-claude/request.js +180 -0
- package/dist/server/conversions/pairs/responses-claude/response.js +70 -0
- package/dist/server/conversions/pairs/responses-claude/streaming.js +345 -0
- package/dist/server/conversions/pairs/responses-completions/request.js +207 -0
- package/dist/server/conversions/pairs/responses-completions/response.js +96 -0
- package/dist/server/conversions/pairs/responses-completions/streaming.js +344 -0
- package/dist/server/conversions/pairs/responses-gemini/request.js +18 -0
- package/dist/server/conversions/pairs/responses-gemini/response.js +18 -0
- package/dist/server/conversions/pairs/responses-gemini/streaming.js +43 -0
- package/dist/server/conversions/pairs/responses-responses/request.js +115 -0
- package/dist/server/conversions/pipeline.js +296 -0
- package/dist/server/conversions/stream-converter-adapter.js +49 -0
- package/dist/server/conversions/thinking/effort.js +61 -0
- package/dist/server/conversions/thinking/mapper.js +59 -0
- package/dist/server/conversions/thinking/providers.js +80 -0
- package/dist/server/conversions/types.js +5 -0
- package/dist/server/conversions/url-normalizer.js +58 -0
- package/dist/server/conversions/utils/format-mappers.js +57 -0
- package/dist/server/conversions/utils/id.js +33 -0
- package/dist/server/conversions/utils/stop-reasons.js +95 -0
- package/dist/server/conversions/utils/streaming-helpers.js +59 -0
- package/dist/server/conversions/utils/tool-schema.js +169 -0
- package/dist/server/conversions/utils/usage.js +82 -0
- package/dist/server/fs-database.js +465 -135
- package/dist/server/main.js +93 -33
- package/dist/server/original-config-reader.js +1 -1
- package/dist/server/proxy-server.js +887 -633
- package/dist/server/transformers/chunk-collector.js +5 -1
- package/dist/server/transformers/streaming.js +6 -3235
- package/dist/server/type-migration.js +2 -3
- package/dist/server/utils.js +5 -0
- package/dist/ui/assets/{index-C7G0whng.css → index-BHR12ImE.css} +1 -1
- package/dist/ui/assets/index-Rwiqttz-.js +517 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/server/transformers/transformers.js +0 -1767
- package/dist/ui/assets/index-Dl-B9pXM.js +0 -514
- package/schema/claude.schema.md +0 -946
- package/schema/deepseek-chat.schema.md +0 -799
- package/schema/gemini.schema.md +0 -1408
- package/schema/openai-chat-completions.schema.md +0 -1088
- package/schema/openai-responses.schema.md +0 -226196
- 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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
633
|
-
|
|
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
|
-
|
|
867
|
-
|
|
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
|
-
|
|
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
|
-
|
|
983
|
-
enableLogging:
|
|
984
|
-
logRetentionDays:
|
|
985
|
-
maxLogSize:
|
|
986
|
-
apiKey:
|
|
987
|
-
enableFailover:
|
|
988
|
-
failoverRecoverySeconds:
|
|
989
|
-
ruleGlobalTimeout:
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
proxyEnabled:
|
|
998
|
-
proxyUrl:
|
|
999
|
-
proxyUsername:
|
|
1000
|
-
proxyPassword:
|
|
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('[
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
//
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
if (
|
|
1061
|
-
|
|
1062
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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 ===
|
|
1616
|
+
const route = this.routes.find(r => r.id === routeId);
|
|
1316
1617
|
if (!route)
|
|
1317
1618
|
return false;
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
1624
|
+
deactivateToolRoute(tool) {
|
|
1329
1625
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1330
|
-
|
|
1331
|
-
|
|
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
|
-
|
|
1631
|
+
deactivateAllToolRoutes() {
|
|
1339
1632
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1340
1633
|
let count = 0;
|
|
1341
|
-
for (const
|
|
1342
|
-
if (
|
|
1343
|
-
|
|
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.
|
|
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
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
shardIndex
|
|
1557
|
-
|
|
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
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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:
|
|
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 +=
|
|
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
|
-
|
|
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
|
-
|
|
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 ((
|
|
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 (
|
|
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 (
|
|
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 ((
|
|
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 (
|
|
2995
|
+
catch (_d) {
|
|
2666
2996
|
return userId;
|
|
2667
2997
|
}
|
|
2668
2998
|
}
|
|
2669
2999
|
}
|
|
2670
|
-
catch (
|
|
3000
|
+
catch (_e) {
|
|
2671
3001
|
// 忽略解析错误
|
|
2672
3002
|
}
|
|
2673
3003
|
}
|