aicodeswitch 3.0.2 → 3.0.4
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 +7 -0
- package/dist/server/fs-database.js +349 -65
- package/dist/server/main.js +7 -1
- package/dist/server/proxy-server.js +248 -6
- package/dist/server/transformers/gemini.js +625 -0
- package/dist/server/transformers/streaming.js +563 -7
- package/dist/types/index.js +1 -0
- package/dist/ui/assets/index-BFZeqM0H.css +1 -0
- package/dist/ui/assets/index-DyW-TIXE.js +481 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -2
- package/schema/claude.schema.md +945 -0
- package/schema/gemini.schema.md +1408 -0
- package/schema/openai.schema.md +2162 -0
- package/dist/ui/assets/index-B8caYm4n.css +0 -1
- package/dist/ui/assets/index-BHXiQgEv.js +0 -466
package/README.md
CHANGED
|
@@ -105,6 +105,7 @@ Codex的配置覆盖逻辑一模一样。
|
|
|
105
105
|
* Claude Chat
|
|
106
106
|
* Claude Code
|
|
107
107
|
* DeepSeek Chat
|
|
108
|
+
* Gemini
|
|
108
109
|
|
|
109
110
|
**有什么用?**
|
|
110
111
|
|
|
@@ -141,6 +142,12 @@ aicodeswitch内部,会根据“源类型”来转换数据。例如,你的
|
|
|
141
142
|
|
|
142
143
|
当同一请求类型配置多个规则时,系统会按排序优先使用第一个,如果某个服务报错(4xx/5xx)或请求超时,将自动切换到下一个可用规则,确保你可以正常使用coding工具。
|
|
143
144
|
|
|
145
|
+
### 数据转流
|
|
146
|
+
|
|
147
|
+
支持将openai的流式响应数据转换成Claude Code的流式数据,并支持实时转换,从而可以让你使用支持openAI的API服务来在Claude Code中使用。
|
|
148
|
+
|
|
149
|
+
*不过,需要注意,如果你的API服务商不支持tools,或者所支持的max_tokens太小,是无法在Claude Code中使用的。*
|
|
150
|
+
|
|
144
151
|
## Skills管理
|
|
145
152
|
|
|
146
153
|
你可以在 aicodeswitch 中集中统一管理 skills,把skills分发给claude code和codex,随时启用和停用skills。
|
|
@@ -137,6 +137,18 @@ class FileSystemDatabaseManager {
|
|
|
137
137
|
writable: true,
|
|
138
138
|
value: this.createEmptyStatistics()
|
|
139
139
|
});
|
|
140
|
+
Object.defineProperty(this, "contentTypeDistributionInitialized", {
|
|
141
|
+
enumerable: true,
|
|
142
|
+
configurable: true,
|
|
143
|
+
writable: true,
|
|
144
|
+
value: false
|
|
145
|
+
});
|
|
146
|
+
Object.defineProperty(this, "contentTypeDistributionInitializing", {
|
|
147
|
+
enumerable: true,
|
|
148
|
+
configurable: true,
|
|
149
|
+
writable: true,
|
|
150
|
+
value: false
|
|
151
|
+
});
|
|
140
152
|
// 缓存机制
|
|
141
153
|
Object.defineProperty(this, "logsCountCache", {
|
|
142
154
|
enumerable: true,
|
|
@@ -169,6 +181,16 @@ class FileSystemDatabaseManager {
|
|
|
169
181
|
writable: true,
|
|
170
182
|
value: 30
|
|
171
183
|
});
|
|
184
|
+
// Export/Import operations
|
|
185
|
+
/**
|
|
186
|
+
* 当前支持的导出数据版本
|
|
187
|
+
*/
|
|
188
|
+
Object.defineProperty(this, "CURRENT_EXPORT_VERSION", {
|
|
189
|
+
enumerable: true,
|
|
190
|
+
configurable: true,
|
|
191
|
+
writable: true,
|
|
192
|
+
value: '3.0.0'
|
|
193
|
+
});
|
|
172
194
|
this.dataPath = dataPath;
|
|
173
195
|
}
|
|
174
196
|
initialize() {
|
|
@@ -252,12 +274,7 @@ class FileSystemDatabaseManager {
|
|
|
252
274
|
}
|
|
253
275
|
// 保存新的 vendors.json
|
|
254
276
|
yield this.saveVendors();
|
|
255
|
-
// 备份旧文件
|
|
256
|
-
const timestamp = Date.now();
|
|
257
|
-
const backupFile = path_1.default.join(this.dataPath, `services.json.backup.${timestamp}`);
|
|
258
|
-
yield promises_1.default.rename(oldServicesFile, backupFile);
|
|
259
277
|
console.log(`[Database] 迁移完成:${migratedCount} 个服务已迁移`);
|
|
260
|
-
console.log(`[Database] 旧的 services.json 已备份到 ${backupFile}`);
|
|
261
278
|
}
|
|
262
279
|
catch (err) {
|
|
263
280
|
if (err.code === 'ENOENT') {
|
|
@@ -376,18 +393,6 @@ class FileSystemDatabaseManager {
|
|
|
376
393
|
if (routesFileFormat !== 'combined' || merged || (routesFileFormat === 'combined' && !hasRulesInRoutesFile)) {
|
|
377
394
|
yield this.saveRoutesData();
|
|
378
395
|
}
|
|
379
|
-
// 备份旧的 rules.json 文件
|
|
380
|
-
if (oldRulesExists) {
|
|
381
|
-
try {
|
|
382
|
-
const timestamp = Date.now();
|
|
383
|
-
const backupFile = path_1.default.join(this.dataPath, `rules.json.backup.${timestamp}`);
|
|
384
|
-
yield promises_1.default.rename(oldRulesFile, backupFile);
|
|
385
|
-
console.log(`[Database] 旧的 rules.json 已备份到 ${backupFile}`);
|
|
386
|
-
}
|
|
387
|
-
catch (error) {
|
|
388
|
-
console.error('[Database] 备份 rules.json 失败:', error);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
396
|
});
|
|
392
397
|
}
|
|
393
398
|
loadConfig() {
|
|
@@ -522,10 +527,6 @@ class FileSystemDatabaseManager {
|
|
|
522
527
|
// 保存索引
|
|
523
528
|
yield this.saveLogsIndex();
|
|
524
529
|
console.log(`[Database] Successfully migrated ${migratedCount} log entries to ${this.logShardsIndex.length} shard(s)`);
|
|
525
|
-
// 备份旧文件
|
|
526
|
-
const backupFile = path_1.default.join(this.dataPath, 'logs.json.backup');
|
|
527
|
-
yield promises_1.default.rename(oldLogsFile, backupFile);
|
|
528
|
-
console.log(`[Database] Old logs.json backed up to ${backupFile}`);
|
|
529
530
|
}
|
|
530
531
|
catch (err) {
|
|
531
532
|
if (err.code === 'ENOENT') {
|
|
@@ -668,9 +669,11 @@ class FileSystemDatabaseManager {
|
|
|
668
669
|
try {
|
|
669
670
|
const data = yield promises_1.default.readFile(this.statisticsFile, 'utf-8');
|
|
670
671
|
this.statistics = JSON.parse(data);
|
|
672
|
+
this.contentTypeDistributionInitialized = this.statistics.contentTypeDistribution.length > 0;
|
|
671
673
|
}
|
|
672
674
|
catch (_a) {
|
|
673
675
|
this.statistics = this.createEmptyStatistics();
|
|
676
|
+
this.contentTypeDistributionInitialized = false;
|
|
674
677
|
// 创建空文件
|
|
675
678
|
yield this.saveStatistics();
|
|
676
679
|
}
|
|
@@ -1165,7 +1168,8 @@ class FileSystemDatabaseManager {
|
|
|
1165
1168
|
addLog(log) {
|
|
1166
1169
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1167
1170
|
const id = crypto_1.default.randomUUID();
|
|
1168
|
-
const
|
|
1171
|
+
const contentType = this.resolveLogContentType(log);
|
|
1172
|
+
const logWithId = Object.assign(Object.assign({}, log), { contentType, id });
|
|
1169
1173
|
// 获取目标分片文件名
|
|
1170
1174
|
const filename = yield this.getLogShardFilename(logWithId.timestamp);
|
|
1171
1175
|
// 加载现有分片数据
|
|
@@ -1368,23 +1372,153 @@ class FileSystemDatabaseManager {
|
|
|
1368
1372
|
return true;
|
|
1369
1373
|
});
|
|
1370
1374
|
}
|
|
1371
|
-
|
|
1375
|
+
/**
|
|
1376
|
+
* 验证供应商数据格式
|
|
1377
|
+
*/
|
|
1378
|
+
validateVendor(vendor, index) {
|
|
1379
|
+
if (!vendor || typeof vendor !== 'object') {
|
|
1380
|
+
return { valid: false, error: `供应商[${index}] 不是有效的对象` };
|
|
1381
|
+
}
|
|
1382
|
+
if (!vendor.id || typeof vendor.id !== 'string') {
|
|
1383
|
+
return { valid: false, error: `供应商[${index}] 缺少有效的 id 字段` };
|
|
1384
|
+
}
|
|
1385
|
+
if (!vendor.name || typeof vendor.name !== 'string') {
|
|
1386
|
+
return { valid: false, error: `供应商[${index}](${vendor.id}) 缺少有效的 name 字段` };
|
|
1387
|
+
}
|
|
1388
|
+
if (!Array.isArray(vendor.services)) {
|
|
1389
|
+
return { valid: false, error: `供应商[${index}](${vendor.id}) 的 services 不是数组` };
|
|
1390
|
+
}
|
|
1391
|
+
for (let i = 0; i < vendor.services.length; i++) {
|
|
1392
|
+
const service = vendor.services[i];
|
|
1393
|
+
if (!service || typeof service !== 'object') {
|
|
1394
|
+
return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] 不是有效的对象` };
|
|
1395
|
+
}
|
|
1396
|
+
if (!service.id || typeof service.id !== 'string') {
|
|
1397
|
+
return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] 缺少有效的 id 字段` };
|
|
1398
|
+
}
|
|
1399
|
+
if (!service.name || typeof service.name !== 'string') {
|
|
1400
|
+
return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] 缺少有效的 name 字段` };
|
|
1401
|
+
}
|
|
1402
|
+
if (!service.apiUrl || typeof service.apiUrl !== 'string') {
|
|
1403
|
+
return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] 缺少有效的 apiUrl 字段` };
|
|
1404
|
+
}
|
|
1405
|
+
if (!service.apiKey || typeof service.apiKey !== 'string') {
|
|
1406
|
+
return { valid: false, error: `供应商[${index}](${vendor.id}) 的服务[${i}] 缺少有效的 apiKey 字段` };
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return { valid: true };
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* 验证路由数据格式
|
|
1413
|
+
*/
|
|
1414
|
+
validateRoute(route, index) {
|
|
1415
|
+
if (!route || typeof route !== 'object') {
|
|
1416
|
+
return { valid: false, error: `路由[${index}] 不是有效的对象` };
|
|
1417
|
+
}
|
|
1418
|
+
if (!route.id || typeof route.id !== 'string') {
|
|
1419
|
+
return { valid: false, error: `路由[${index}] 缺少有效的 id 字段` };
|
|
1420
|
+
}
|
|
1421
|
+
if (!route.name || typeof route.name !== 'string') {
|
|
1422
|
+
return { valid: false, error: `路由[${index}](${route.id}) 缺少有效的 name 字段` };
|
|
1423
|
+
}
|
|
1424
|
+
if (!route.targetType || !['claude-code', 'codex'].includes(route.targetType)) {
|
|
1425
|
+
return { valid: false, error: `路由[${index}](${route.id}) 的 targetType 必须是 'claude-code' 或 'codex'` };
|
|
1426
|
+
}
|
|
1427
|
+
if (typeof route.isActive !== 'boolean') {
|
|
1428
|
+
return { valid: false, error: `路由[${index}](${route.id}) 的 isActive 必须是布尔值` };
|
|
1429
|
+
}
|
|
1430
|
+
return { valid: true };
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* 验证规则数据格式
|
|
1434
|
+
*/
|
|
1435
|
+
validateRule(rule, index) {
|
|
1436
|
+
if (!rule || typeof rule !== 'object') {
|
|
1437
|
+
return { valid: false, error: `规则[${index}] 不是有效的对象` };
|
|
1438
|
+
}
|
|
1439
|
+
if (!rule.id || typeof rule.id !== 'string') {
|
|
1440
|
+
return { valid: false, error: `规则[${index}] 缺少有效的 id 字段` };
|
|
1441
|
+
}
|
|
1442
|
+
if (!rule.routeId || typeof rule.routeId !== 'string') {
|
|
1443
|
+
return { valid: false, error: `规则[${index}](${rule.id}) 缺少有效的 routeId 字段` };
|
|
1444
|
+
}
|
|
1445
|
+
if (!rule.targetServiceId || typeof rule.targetServiceId !== 'string') {
|
|
1446
|
+
return { valid: false, error: `规则[${index}](${rule.id}) 缺少有效的 targetServiceId 字段` };
|
|
1447
|
+
}
|
|
1448
|
+
const validContentTypes = ['default', 'background', 'thinking', 'long-context', 'image-understanding', 'model-mapping'];
|
|
1449
|
+
if (!rule.contentType || !validContentTypes.includes(rule.contentType)) {
|
|
1450
|
+
return { valid: false, error: `规则[${index}](${rule.id}) 的 contentType 无效` };
|
|
1451
|
+
}
|
|
1452
|
+
return { valid: true };
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* 验证配置数据格式
|
|
1456
|
+
*/
|
|
1457
|
+
validateConfig(config) {
|
|
1458
|
+
if (!config || typeof config !== 'object') {
|
|
1459
|
+
return { valid: false, error: 'config 不是有效的对象' };
|
|
1460
|
+
}
|
|
1461
|
+
return { valid: true };
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* 验证导出数据格式(严格校验)
|
|
1465
|
+
*/
|
|
1466
|
+
validateExportData(data) {
|
|
1467
|
+
if (!data || typeof data !== 'object') {
|
|
1468
|
+
return { valid: false, error: '数据不是有效的对象' };
|
|
1469
|
+
}
|
|
1470
|
+
// 检查必需字段是否存在
|
|
1471
|
+
if (!data.version || typeof data.version !== 'string') {
|
|
1472
|
+
return { valid: false, error: '缺少有效的 version 字段' };
|
|
1473
|
+
}
|
|
1474
|
+
// 检查版本是否匹配当前版本
|
|
1475
|
+
if (data.version !== this.CURRENT_EXPORT_VERSION) {
|
|
1476
|
+
return { valid: false, error: `数据版本 ${data.version} 与当前支持的版本 ${this.CURRENT_EXPORT_VERSION} 不匹配。请使用相同版本的系统导出数据。` };
|
|
1477
|
+
}
|
|
1478
|
+
if (!data.exportDate || typeof data.exportDate !== 'number') {
|
|
1479
|
+
return { valid: false, error: '缺少有效的 exportDate 字段' };
|
|
1480
|
+
}
|
|
1481
|
+
// 检查 vendors
|
|
1482
|
+
if (!Array.isArray(data.vendors)) {
|
|
1483
|
+
return { valid: false, error: 'vendors 不是数组' };
|
|
1484
|
+
}
|
|
1485
|
+
for (let i = 0; i < data.vendors.length; i++) {
|
|
1486
|
+
const result = this.validateVendor(data.vendors[i], i);
|
|
1487
|
+
if (!result.valid)
|
|
1488
|
+
return result;
|
|
1489
|
+
}
|
|
1490
|
+
// 检查 routes
|
|
1491
|
+
if (!Array.isArray(data.routes)) {
|
|
1492
|
+
return { valid: false, error: 'routes 不是数组' };
|
|
1493
|
+
}
|
|
1494
|
+
for (let i = 0; i < data.routes.length; i++) {
|
|
1495
|
+
const result = this.validateRoute(data.routes[i], i);
|
|
1496
|
+
if (!result.valid)
|
|
1497
|
+
return result;
|
|
1498
|
+
}
|
|
1499
|
+
// 检查 rules
|
|
1500
|
+
if (!Array.isArray(data.rules)) {
|
|
1501
|
+
return { valid: false, error: 'rules 不是数组' };
|
|
1502
|
+
}
|
|
1503
|
+
for (let i = 0; i < data.rules.length; i++) {
|
|
1504
|
+
const result = this.validateRule(data.rules[i], i);
|
|
1505
|
+
if (!result.valid)
|
|
1506
|
+
return result;
|
|
1507
|
+
}
|
|
1508
|
+
// 检查 config
|
|
1509
|
+
const configResult = this.validateConfig(data.config);
|
|
1510
|
+
if (!configResult.valid)
|
|
1511
|
+
return configResult;
|
|
1512
|
+
return { valid: true };
|
|
1513
|
+
}
|
|
1372
1514
|
exportData(password) {
|
|
1373
1515
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1374
|
-
//
|
|
1375
|
-
const allServices = [];
|
|
1376
|
-
for (const vendor of this.vendors) {
|
|
1377
|
-
if (vendor.services) {
|
|
1378
|
-
// 为每个服务添加 vendorId(兼容旧格式)
|
|
1379
|
-
const servicesWithVendorId = vendor.services.map(s => (Object.assign(Object.assign({}, s), { vendorId: vendor.id })));
|
|
1380
|
-
allServices.push(...servicesWithVendorId);
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1516
|
+
// 只导出当前格式,不再兼容旧格式
|
|
1383
1517
|
const exportData = {
|
|
1384
|
-
version:
|
|
1518
|
+
version: this.CURRENT_EXPORT_VERSION,
|
|
1385
1519
|
exportDate: Date.now(),
|
|
1386
1520
|
vendors: this.vendors,
|
|
1387
|
-
apiServices:
|
|
1521
|
+
apiServices: [], // 保留字段以兼容类型定义,但内容为空
|
|
1388
1522
|
routes: this.routes,
|
|
1389
1523
|
rules: this.rules,
|
|
1390
1524
|
config: this.config,
|
|
@@ -1394,49 +1528,184 @@ class FileSystemDatabaseManager {
|
|
|
1394
1528
|
return encrypted;
|
|
1395
1529
|
});
|
|
1396
1530
|
}
|
|
1397
|
-
|
|
1531
|
+
/**
|
|
1532
|
+
* 预览导入数据
|
|
1533
|
+
*/
|
|
1534
|
+
previewImportData(encryptedData, password) {
|
|
1398
1535
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1399
1536
|
try {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1537
|
+
// 解密
|
|
1538
|
+
let jsonData;
|
|
1539
|
+
try {
|
|
1540
|
+
const decrypted = crypto_js_1.default.AES.decrypt(encryptedData, password);
|
|
1541
|
+
jsonData = decrypted.toString(crypto_js_1.default.enc.Utf8);
|
|
1542
|
+
if (!jsonData) {
|
|
1543
|
+
return { success: false, message: '解密失败:密码错误或数据损坏' };
|
|
1544
|
+
}
|
|
1408
1545
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1546
|
+
catch (error) {
|
|
1547
|
+
return { success: false, message: '解密失败:密码错误或数据格式错误' };
|
|
1548
|
+
}
|
|
1549
|
+
// 解析 JSON
|
|
1550
|
+
let importData;
|
|
1551
|
+
try {
|
|
1552
|
+
importData = JSON.parse(jsonData);
|
|
1553
|
+
}
|
|
1554
|
+
catch (error) {
|
|
1555
|
+
return { success: false, message: '数据解析失败:不是有效的 JSON 格式' };
|
|
1556
|
+
}
|
|
1557
|
+
// 验证数据格式
|
|
1558
|
+
const validation = this.validateExportData(importData);
|
|
1559
|
+
if (!validation.valid) {
|
|
1560
|
+
return { success: false, message: `数据验证失败:${validation.error}` };
|
|
1561
|
+
}
|
|
1562
|
+
// 计算服务数量
|
|
1563
|
+
const servicesCount = importData.vendors.reduce((sum, v) => { var _a; return sum + (((_a = v.services) === null || _a === void 0 ? void 0 : _a.length) || 0); }, 0);
|
|
1564
|
+
return {
|
|
1565
|
+
success: true,
|
|
1566
|
+
data: {
|
|
1567
|
+
vendors: importData.vendors.length,
|
|
1568
|
+
services: servicesCount,
|
|
1569
|
+
routes: importData.routes.length,
|
|
1570
|
+
rules: importData.rules.length,
|
|
1571
|
+
exportDate: importData.exportDate,
|
|
1572
|
+
version: importData.version,
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
catch (error) {
|
|
1577
|
+
console.error('Preview import error:', error);
|
|
1578
|
+
return { success: false, message: `预览失败:${error instanceof Error ? error.message : '未知错误'}` };
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
importData(encryptedData, password) {
|
|
1583
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1584
|
+
try {
|
|
1585
|
+
// 解密
|
|
1586
|
+
let jsonData;
|
|
1587
|
+
try {
|
|
1588
|
+
const decrypted = crypto_js_1.default.AES.decrypt(encryptedData, password);
|
|
1589
|
+
jsonData = decrypted.toString(crypto_js_1.default.enc.Utf8);
|
|
1590
|
+
if (!jsonData) {
|
|
1591
|
+
return { success: false, message: '导入失败', details: '解密失败:密码错误或数据损坏' };
|
|
1422
1592
|
}
|
|
1423
|
-
// 合并到 vendors
|
|
1424
|
-
this.vendors = vendors.map((vendor) => (Object.assign(Object.assign({}, vendor), { services: servicesByVendor.get(vendor.id) || [] })));
|
|
1425
1593
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1594
|
+
catch (error) {
|
|
1595
|
+
return { success: false, message: '导入失败', details: '解密失败:密码错误或数据格式错误' };
|
|
1596
|
+
}
|
|
1597
|
+
// 解析 JSON
|
|
1598
|
+
let importData;
|
|
1599
|
+
try {
|
|
1600
|
+
importData = JSON.parse(jsonData);
|
|
1601
|
+
}
|
|
1602
|
+
catch (error) {
|
|
1603
|
+
return { success: false, message: '导入失败', details: '数据解析失败:不是有效的 JSON 格式' };
|
|
1604
|
+
}
|
|
1605
|
+
// 验证数据格式
|
|
1606
|
+
const validation = this.validateExportData(importData);
|
|
1607
|
+
if (!validation.valid) {
|
|
1608
|
+
return { success: false, message: '导入失败', details: `数据验证失败:${validation.error}` };
|
|
1609
|
+
}
|
|
1610
|
+
// 导入数据(更新 updatedAt)
|
|
1611
|
+
const now = Date.now();
|
|
1612
|
+
this.vendors = importData.vendors.map((v) => (Object.assign(Object.assign({}, v), { updatedAt: now })));
|
|
1613
|
+
this.routes = importData.routes.map((r) => (Object.assign(Object.assign({}, r), { updatedAt: now })));
|
|
1614
|
+
this.rules = importData.rules.map((r) => (Object.assign(Object.assign({}, r), { updatedAt: now })));
|
|
1615
|
+
this.config = Object.assign(Object.assign({}, importData.config), { updatedAt: now });
|
|
1616
|
+
// 保存数据
|
|
1429
1617
|
yield Promise.all([
|
|
1430
1618
|
this.saveVendors(),
|
|
1431
|
-
// 删除: this.saveServices(),
|
|
1432
1619
|
this.saveRoutes(),
|
|
1433
1620
|
this.saveConfig(),
|
|
1434
1621
|
]);
|
|
1435
|
-
return
|
|
1622
|
+
const servicesCount = this.vendors.reduce((sum, v) => { var _a; return sum + (((_a = v.services) === null || _a === void 0 ? void 0 : _a.length) || 0); }, 0);
|
|
1623
|
+
return {
|
|
1624
|
+
success: true,
|
|
1625
|
+
message: '导入成功',
|
|
1626
|
+
details: `已导入 ${this.vendors.length} 个供应商、${servicesCount} 个服务、${this.routes.length} 个路由、${this.rules.length} 个规则`
|
|
1627
|
+
};
|
|
1436
1628
|
}
|
|
1437
1629
|
catch (error) {
|
|
1438
1630
|
console.error('Import error:', error);
|
|
1439
|
-
return
|
|
1631
|
+
return {
|
|
1632
|
+
success: false,
|
|
1633
|
+
message: '导入失败',
|
|
1634
|
+
details: error instanceof Error ? error.message : '未知错误'
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
inferContentTypeFromLog(log) {
|
|
1640
|
+
var _a, _b;
|
|
1641
|
+
const requestModel = ((_a = log.requestModel) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || '';
|
|
1642
|
+
let bodyText = '';
|
|
1643
|
+
if (typeof log.body === 'string') {
|
|
1644
|
+
bodyText = log.body;
|
|
1645
|
+
}
|
|
1646
|
+
else if (log.body !== undefined) {
|
|
1647
|
+
try {
|
|
1648
|
+
bodyText = JSON.stringify(log.body);
|
|
1649
|
+
}
|
|
1650
|
+
catch (_c) {
|
|
1651
|
+
bodyText = '';
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const lowerBody = bodyText.toLowerCase();
|
|
1655
|
+
if (lowerBody.includes('image') || lowerBody.includes('base64')) {
|
|
1656
|
+
return 'image-understanding';
|
|
1657
|
+
}
|
|
1658
|
+
if (requestModel.includes('think')) {
|
|
1659
|
+
return 'thinking';
|
|
1660
|
+
}
|
|
1661
|
+
if ((((_b = log.usage) === null || _b === void 0 ? void 0 : _b.inputTokens) || 0) > 12000) {
|
|
1662
|
+
return 'long-context';
|
|
1663
|
+
}
|
|
1664
|
+
return 'default';
|
|
1665
|
+
}
|
|
1666
|
+
resolveLogContentType(log) {
|
|
1667
|
+
if (log.contentType) {
|
|
1668
|
+
return log.contentType;
|
|
1669
|
+
}
|
|
1670
|
+
if (log.ruleId) {
|
|
1671
|
+
const rule = this.getRule(log.ruleId);
|
|
1672
|
+
if (rule === null || rule === void 0 ? void 0 : rule.contentType) {
|
|
1673
|
+
return rule.contentType;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return this.inferContentTypeFromLog(log);
|
|
1677
|
+
}
|
|
1678
|
+
ensureContentTypeDistribution() {
|
|
1679
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1680
|
+
if (this.contentTypeDistributionInitialized || this.contentTypeDistributionInitializing) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
this.contentTypeDistributionInitializing = true;
|
|
1684
|
+
try {
|
|
1685
|
+
if (this.logShardsIndex.length === 0) {
|
|
1686
|
+
this.contentTypeDistributionInitialized = true;
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
const counts = new Map();
|
|
1690
|
+
let totalRequests = 0;
|
|
1691
|
+
for (const shard of this.logShardsIndex) {
|
|
1692
|
+
const shardLogs = yield this.loadLogShard(shard.filename);
|
|
1693
|
+
for (const log of shardLogs) {
|
|
1694
|
+
totalRequests++;
|
|
1695
|
+
const contentType = this.resolveLogContentType(log);
|
|
1696
|
+
counts.set(contentType, (counts.get(contentType) || 0) + 1);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
this.statistics.contentTypeDistribution = Array.from(counts.entries()).map(([contentType, count]) => ({
|
|
1700
|
+
contentType,
|
|
1701
|
+
count,
|
|
1702
|
+
percentage: totalRequests > 0 ? Math.round((count / totalRequests) * 100) : 0,
|
|
1703
|
+
}));
|
|
1704
|
+
this.contentTypeDistributionInitialized = true;
|
|
1705
|
+
yield this.saveStatistics();
|
|
1706
|
+
}
|
|
1707
|
+
finally {
|
|
1708
|
+
this.contentTypeDistributionInitializing = false;
|
|
1440
1709
|
}
|
|
1441
1710
|
});
|
|
1442
1711
|
}
|
|
@@ -1539,6 +1808,19 @@ class FileSystemDatabaseManager {
|
|
|
1539
1808
|
modelStats.avgResponseTime =
|
|
1540
1809
|
(modelStats.avgResponseTime * (modelStats.totalRequests - 1) + responseTime) / modelStats.totalRequests;
|
|
1541
1810
|
}
|
|
1811
|
+
// 更新 contentTypeDistribution
|
|
1812
|
+
const resolvedContentType = this.resolveLogContentType(log);
|
|
1813
|
+
let contentTypeStats = this.statistics.contentTypeDistribution.find(s => s.contentType === resolvedContentType);
|
|
1814
|
+
if (!contentTypeStats) {
|
|
1815
|
+
contentTypeStats = { contentType: resolvedContentType, count: 0, percentage: 0 };
|
|
1816
|
+
this.statistics.contentTypeDistribution.push(contentTypeStats);
|
|
1817
|
+
}
|
|
1818
|
+
contentTypeStats.count++;
|
|
1819
|
+
const totalRequests = this.statistics.overview.totalRequests;
|
|
1820
|
+
for (const entry of this.statistics.contentTypeDistribution) {
|
|
1821
|
+
entry.percentage = totalRequests > 0 ? Math.round((entry.count / totalRequests) * 100) : 0;
|
|
1822
|
+
}
|
|
1823
|
+
this.contentTypeDistributionInitialized = true;
|
|
1542
1824
|
// 更新 timeline
|
|
1543
1825
|
const date = new Date(log.timestamp).toISOString().split('T')[0];
|
|
1544
1826
|
let timelineStats = this.statistics.timeline.find(t => t.date === date);
|
|
@@ -1566,6 +1848,7 @@ class FileSystemDatabaseManager {
|
|
|
1566
1848
|
*/
|
|
1567
1849
|
getStatistics() {
|
|
1568
1850
|
return __awaiter(this, arguments, void 0, function* (days = 30) {
|
|
1851
|
+
yield this.ensureContentTypeDistribution();
|
|
1569
1852
|
const now = Date.now();
|
|
1570
1853
|
const startTime = now - days * 24 * 60 * 60 * 1000;
|
|
1571
1854
|
// 过滤 timeline 数据
|
|
@@ -1662,7 +1945,8 @@ class FileSystemDatabaseManager {
|
|
|
1662
1945
|
// 检查 body 中的 metadata.user_id(Claude Code)
|
|
1663
1946
|
if (log.body) {
|
|
1664
1947
|
try {
|
|
1665
|
-
|
|
1948
|
+
// body 可能是对象(已解析)或字符串(未解析)
|
|
1949
|
+
const body = typeof log.body === 'string' ? JSON.parse(log.body) : log.body;
|
|
1666
1950
|
if (((_b = body.metadata) === null || _b === void 0 ? void 0 : _b.user_id) === sessionId) {
|
|
1667
1951
|
return true;
|
|
1668
1952
|
}
|
package/dist/server/main.js
CHANGED
|
@@ -1274,10 +1274,16 @@ ${instruction}
|
|
|
1274
1274
|
const data = yield dbManager.exportData(password);
|
|
1275
1275
|
res.json({ data });
|
|
1276
1276
|
})));
|
|
1277
|
+
// 导入数据预览
|
|
1278
|
+
app.post('/api/import/preview', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1279
|
+
const { encryptedData, password } = req.body;
|
|
1280
|
+
const result = yield dbManager.previewImportData(encryptedData, password);
|
|
1281
|
+
res.json(result);
|
|
1282
|
+
})));
|
|
1277
1283
|
app.post('/api/import', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1278
1284
|
const { encryptedData, password } = req.body;
|
|
1279
1285
|
const result = yield dbManager.importData(encryptedData, password);
|
|
1280
|
-
if (result) {
|
|
1286
|
+
if (result.success) {
|
|
1281
1287
|
yield proxyServer.reloadRoutes();
|
|
1282
1288
|
}
|
|
1283
1289
|
res.json(result);
|