aicodeswitch 1.3.9 → 1.4.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/CLAUDE.md CHANGED
@@ -40,6 +40,10 @@ npm link # Link local package for CLI testing
40
40
  aicos start # Start the proxy server
41
41
  aicos stop # Stop the proxy server
42
42
  aicos restart # Restart the proxy server
43
+ aicos ui # Open web UI in browser (starts server if needed)
44
+ aicos update # Update to the latest version and restart
45
+ aicos restore # Restore original configuration files
46
+ aicos version # Show current version information
43
47
  ```
44
48
 
45
49
  ## Architecture
@@ -180,3 +184,9 @@ aicos restart # Restart the proxy server
180
184
  - **HTTP Client**: Axios
181
185
  - **Encryption**: CryptoJS (AES)
182
186
  - **CLI**: Yargs-like custom implementation
187
+
188
+ ## Development
189
+
190
+ * 使用yarn作为包管理器,请使用yarn安装依赖。
191
+ * 前端依赖库安装在devDependencies中,请使用yarn install --dev安装。
192
+ * 所有对话请使用中文。生成代码中的文案及相关注释根据代码原本的语言生成。
package/README.md CHANGED
@@ -30,6 +30,12 @@ aicos stop
30
30
  **进入管理界面**
31
31
 
32
32
  ```
33
+ # 自动启动服务和打开界面
34
+ aicos ui
35
+ ```
36
+
37
+ ```
38
+ # 手动在浏览器打开管理界面
33
39
  http://127.0.0.1:4567
34
40
  ```
35
41
 
package/bin/cli.js CHANGED
@@ -12,6 +12,7 @@ const commands = {
12
12
  update: () => require(path.join(__dirname, 'update')),
13
13
  restore: () => require(path.join(__dirname, 'restore')),
14
14
  version: () => require(path.join(__dirname, 'version')),
15
+ ui: () => require(path.join(__dirname, 'ui')),
15
16
  };
16
17
 
17
18
  if (!command || !commands[command]) {
@@ -22,6 +23,7 @@ Commands:
22
23
  start Start the AI Code Switch server
23
24
  stop Stop the AI Code Switch server
24
25
  restart Restart the AI Code Switch server
26
+ ui Open the web UI in browser (starts server if needed)
25
27
  update Update to the latest version and restart
26
28
  restore Restore original configuration files
27
29
  version Show current version information
@@ -30,6 +32,7 @@ Example:
30
32
  aicos start
31
33
  aicos stop
32
34
  aicos restart
35
+ aicos ui
33
36
  aicos update
34
37
  aicos restore
35
38
  aicos restore claude-code
package/bin/ui.js ADDED
@@ -0,0 +1,173 @@
1
+ const { spawn } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const chalk = require('chalk');
6
+ const ora = require('ora');
7
+
8
+ const PID_FILE = path.join(os.homedir(), '.aicodeswitch', 'server.pid');
9
+ const LOG_FILE = path.join(os.homedir(), '.aicodeswitch', 'server.log');
10
+
11
+ // 确保目录存在
12
+ const ensureDir = (filePath) => {
13
+ const dir = path.dirname(filePath);
14
+ if (!fs.existsSync(dir)) {
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ }
17
+ };
18
+
19
+ const isServerRunning = () => {
20
+ if (!fs.existsSync(PID_FILE)) {
21
+ return false;
22
+ }
23
+
24
+ try {
25
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
26
+ // 检查进程是否存在
27
+ process.kill(pid, 0);
28
+ return true;
29
+ } catch (err) {
30
+ // 进程不存在,删除过期的 PID 文件
31
+ fs.unlinkSync(PID_FILE);
32
+ return false;
33
+ }
34
+ };
35
+
36
+ const getServerInfo = () => {
37
+ // 尝试多个可能的配置文件位置
38
+ const possiblePaths = [
39
+ path.join(os.homedir(), '.aicodeswitch', '.env'),
40
+ path.join(os.homedir(), '.aicodeswitch', 'aicodeswitch.conf')
41
+ ];
42
+
43
+ let host = '127.0.0.1';
44
+ let port = 4567;
45
+
46
+ for (const dotenvPath of possiblePaths) {
47
+ if (fs.existsSync(dotenvPath)) {
48
+ const content = fs.readFileSync(dotenvPath, 'utf-8');
49
+ const hostMatch = content.match(/HOST=(.+)/);
50
+ const portMatch = content.match(/PORT=(.+)/);
51
+
52
+ if (hostMatch) host = hostMatch[1].trim();
53
+ if (portMatch) port = parseInt(portMatch[1].trim(), 10);
54
+ break;
55
+ }
56
+ }
57
+
58
+ return { host, port };
59
+ };
60
+
61
+ const startServer = async () => {
62
+ const spinner = ora({
63
+ text: chalk.cyan('Starting AI Code Switch server...'),
64
+ color: 'cyan'
65
+ }).start();
66
+
67
+ ensureDir(PID_FILE);
68
+ ensureDir(LOG_FILE);
69
+
70
+ // 找到 main.js 的路径
71
+ const serverPath = path.join(__dirname, '..', 'dist', 'server', 'main.js');
72
+
73
+ if (!fs.existsSync(serverPath)) {
74
+ spinner.fail(chalk.red('Server file not found!'));
75
+ console.log(chalk.yellow(`\nPlease run ${chalk.cyan('npm run build')} first.\n`));
76
+ process.exit(1);
77
+ }
78
+
79
+ // 启动服务器进程 - 完全分离
80
+ // 打开日志文件用于输出
81
+ const logFd = fs.openSync(LOG_FILE, 'a');
82
+
83
+ const serverProcess = spawn('node', [serverPath], {
84
+ detached: true,
85
+ stdio: ['ignore', logFd, logFd] // 使用文件描述符
86
+ });
87
+
88
+ // 关闭文件描述符(子进程会保持打开)
89
+ fs.closeSync(logFd);
90
+
91
+ // 保存 PID
92
+ fs.writeFileSync(PID_FILE, serverProcess.pid.toString());
93
+
94
+ // 分离进程,让父进程可以退出
95
+ serverProcess.unref();
96
+
97
+ // 等待服务器启动
98
+ await new Promise(resolve => setTimeout(resolve, 2000));
99
+
100
+ // 检查服务器是否成功启动
101
+ if (fs.existsSync(PID_FILE)) {
102
+ try {
103
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
104
+ process.kill(pid, 0);
105
+ spinner.succeed(chalk.green('Server started successfully!'));
106
+ return true;
107
+ } catch (err) {
108
+ spinner.fail(chalk.red('Failed to start server!'));
109
+ console.log(chalk.yellow(`\nCheck logs: ${chalk.cyan(LOG_FILE)}\n`));
110
+ return false;
111
+ }
112
+ } else {
113
+ spinner.fail(chalk.red('Failed to start server!'));
114
+ console.log(chalk.yellow(`\nCheck logs: ${chalk.cyan(LOG_FILE)}\n`));
115
+ return false;
116
+ }
117
+ };
118
+
119
+ const openBrowser = (url) => {
120
+ const platform = os.platform();
121
+ let command;
122
+
123
+ if (platform === 'darwin') {
124
+ command = 'open';
125
+ } else if (platform === 'win32') {
126
+ command = 'start';
127
+ } else {
128
+ // Linux and others
129
+ command = 'xdg-open';
130
+ }
131
+
132
+ const child = spawn(command, [url], {
133
+ detached: true,
134
+ stdio: 'ignore'
135
+ });
136
+
137
+ child.unref();
138
+ };
139
+
140
+ const openUI = async () => {
141
+ console.log('\n');
142
+
143
+ const running = isServerRunning();
144
+
145
+ if (!running) {
146
+ console.log(chalk.yellow('⚠ Server is not running, starting server first...\n'));
147
+ const started = await startServer();
148
+ if (!started) {
149
+ console.log(chalk.red('\n✗ Failed to start server, cannot open UI\n'));
150
+ process.exit(1);
151
+ }
152
+ } else {
153
+ console.log(chalk.green('✓ Server is already running\n'));
154
+ }
155
+
156
+ const { host, port } = getServerInfo();
157
+ const url = `http://${host}:${port}`;
158
+
159
+ console.log(chalk.cyan('🌐 Opening browser...'));
160
+ console.log(chalk.white(' URL: ') + chalk.cyan.bold(url) + '\n');
161
+
162
+ try {
163
+ openBrowser(url);
164
+ console.log(chalk.green('✓ Browser opened successfully!\n'));
165
+ } catch (err) {
166
+ console.log(chalk.yellow('⚠ Failed to open browser automatically'));
167
+ console.log(chalk.white(' Please open this URL manually: ') + chalk.cyan.bold(url) + '\n');
168
+ }
169
+
170
+ process.exit(0);
171
+ };
172
+
173
+ module.exports = openUI();
@@ -51,10 +51,17 @@ class DatabaseManager {
51
51
  writable: true,
52
52
  value: void 0
53
53
  });
54
+ Object.defineProperty(this, "blacklistDb", {
55
+ enumerable: true,
56
+ configurable: true,
57
+ writable: true,
58
+ value: void 0
59
+ });
54
60
  this.db = new better_sqlite3_1.default(path_1.default.join(dataPath, 'app.db'));
55
61
  this.logDb = new level_1.Level(path_1.default.join(dataPath, 'logs'), { valueEncoding: 'json' });
56
62
  this.accessLogDb = new level_1.Level(path_1.default.join(dataPath, 'access-logs'), { valueEncoding: 'json' });
57
63
  this.errorLogDb = new level_1.Level(path_1.default.join(dataPath, 'error-logs'), { valueEncoding: 'json' });
64
+ this.blacklistDb = new level_1.Level(path_1.default.join(dataPath, 'service-blacklist'), { valueEncoding: 'json' });
58
65
  }
59
66
  initialize() {
60
67
  return __awaiter(this, void 0, void 0, function* () {
@@ -125,6 +132,7 @@ class DatabaseManager {
125
132
  logRetentionDays: 7,
126
133
  maxLogSize: 1000,
127
134
  apiKey: '',
135
+ enableFailover: true, // 默认启用智能故障切换
128
136
  };
129
137
  this.db.prepare('INSERT INTO config (key, value) VALUES (?, ?)').run('app_config', JSON.stringify(defaultConfig));
130
138
  }
@@ -401,6 +409,93 @@ class DatabaseManager {
401
409
  yield this.errorLogDb.clear();
402
410
  });
403
411
  }
412
+ // Service blacklist operations
413
+ isServiceBlacklisted(serviceId, routeId, contentType) {
414
+ return __awaiter(this, void 0, void 0, function* () {
415
+ const key = `${routeId}:${contentType}:${serviceId}`;
416
+ try {
417
+ const value = yield this.blacklistDb.get(key);
418
+ const entry = JSON.parse(value);
419
+ // 检查是否过期
420
+ if (Date.now() > entry.expiresAt) {
421
+ // 已过期,删除记录
422
+ yield this.blacklistDb.del(key);
423
+ return false;
424
+ }
425
+ return true;
426
+ }
427
+ catch (error) {
428
+ if (error.code === 'LEVEL_NOT_FOUND') {
429
+ return false;
430
+ }
431
+ throw error;
432
+ }
433
+ });
434
+ }
435
+ addToBlacklist(serviceId, routeId, contentType, errorMessage, statusCode) {
436
+ return __awaiter(this, void 0, void 0, function* () {
437
+ const key = `${routeId}:${contentType}:${serviceId}`;
438
+ const now = Date.now();
439
+ try {
440
+ // 尝试读取现有记录
441
+ const existing = yield this.blacklistDb.get(key);
442
+ const entry = JSON.parse(existing);
443
+ // 更新现有记录
444
+ entry.blacklistedAt = now;
445
+ entry.expiresAt = now + 10 * 60 * 1000; // 10分钟
446
+ entry.errorCount++;
447
+ entry.lastError = errorMessage;
448
+ entry.lastStatusCode = statusCode;
449
+ yield this.blacklistDb.put(key, JSON.stringify(entry));
450
+ }
451
+ catch (error) {
452
+ if (error.code === 'LEVEL_NOT_FOUND') {
453
+ // 创建新记录
454
+ const entry = {
455
+ serviceId,
456
+ routeId,
457
+ contentType,
458
+ blacklistedAt: now,
459
+ expiresAt: now + 10 * 60 * 1000,
460
+ errorCount: 1,
461
+ lastError: errorMessage,
462
+ lastStatusCode: statusCode,
463
+ };
464
+ yield this.blacklistDb.put(key, JSON.stringify(entry));
465
+ }
466
+ else {
467
+ throw error;
468
+ }
469
+ }
470
+ });
471
+ }
472
+ cleanupExpiredBlacklist() {
473
+ return __awaiter(this, void 0, void 0, function* () {
474
+ var _a, e_4, _b, _c;
475
+ const now = Date.now();
476
+ let count = 0;
477
+ try {
478
+ for (var _d = true, _e = __asyncValues(this.blacklistDb.iterator()), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
479
+ _c = _f.value;
480
+ _d = false;
481
+ const [key, value] = _c;
482
+ const entry = JSON.parse(value);
483
+ if (now > entry.expiresAt) {
484
+ yield this.blacklistDb.del(key);
485
+ count++;
486
+ }
487
+ }
488
+ }
489
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
490
+ finally {
491
+ try {
492
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
493
+ }
494
+ finally { if (e_4) throw e_4.error; }
495
+ }
496
+ return count;
497
+ });
498
+ }
404
499
  // Config operations
405
500
  getConfig() {
406
501
  const row = this.db.prepare('SELECT value FROM config WHERE key = ?').get('app_config');
@@ -474,9 +569,235 @@ class DatabaseManager {
474
569
  }
475
570
  });
476
571
  }
572
+ // Statistics operations
573
+ getStatistics() {
574
+ return __awaiter(this, arguments, void 0, function* (days = 30) {
575
+ var _a, e_5, _b, _c, _d, e_6, _e, _f;
576
+ var _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
577
+ const now = Date.now();
578
+ const startTime = now - days * 24 * 60 * 60 * 1000;
579
+ // Get all logs within the time period
580
+ const allLogs = [];
581
+ try {
582
+ for (var _z = true, _0 = __asyncValues(this.logDb.iterator()), _1; _1 = yield _0.next(), _a = _1.done, !_a; _z = true) {
583
+ _c = _1.value;
584
+ _z = false;
585
+ const [, value] = _c;
586
+ const log = JSON.parse(value);
587
+ if (log.timestamp >= startTime) {
588
+ allLogs.push(log);
589
+ }
590
+ }
591
+ }
592
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
593
+ finally {
594
+ try {
595
+ if (!_z && !_a && (_b = _0.return)) yield _b.call(_0);
596
+ }
597
+ finally { if (e_5) throw e_5.error; }
598
+ }
599
+ // Get all error logs
600
+ const errorLogs = [];
601
+ const recentErrorLogs = [];
602
+ const recentTime = now - 24 * 60 * 60 * 1000; // 24 hours ago
603
+ try {
604
+ for (var _2 = true, _3 = __asyncValues(this.errorLogDb.iterator()), _4; _4 = yield _3.next(), _d = _4.done, !_d; _2 = true) {
605
+ _f = _4.value;
606
+ _2 = false;
607
+ const [, value] = _f;
608
+ const log = JSON.parse(value);
609
+ errorLogs.push(log);
610
+ if (log.timestamp >= recentTime) {
611
+ recentErrorLogs.push(log);
612
+ }
613
+ }
614
+ }
615
+ catch (e_6_1) { e_6 = { error: e_6_1 }; }
616
+ finally {
617
+ try {
618
+ if (!_2 && !_d && (_e = _3.return)) yield _e.call(_3);
619
+ }
620
+ finally { if (e_6) throw e_6.error; }
621
+ }
622
+ // Get vendors and services for mapping
623
+ const vendors = this.getVendors();
624
+ const vendorMap = new Map(vendors.map(v => [v.id, v.name]));
625
+ const services = this.getAPIServices();
626
+ const serviceMap = new Map(services.map(s => [s.id, { name: s.name, vendorId: s.vendorId }]));
627
+ // Calculate overview
628
+ const totalRequests = allLogs.length;
629
+ const successRequests = allLogs.filter(log => log.statusCode && log.statusCode >= 200 && log.statusCode < 400).length;
630
+ const totalInputTokens = allLogs.reduce((sum, log) => { var _a; return sum + (((_a = log.usage) === null || _a === void 0 ? void 0 : _a.inputTokens) || 0); }, 0);
631
+ const totalOutputTokens = allLogs.reduce((sum, log) => { var _a; return sum + (((_a = log.usage) === null || _a === void 0 ? void 0 : _a.outputTokens) || 0); }, 0);
632
+ const totalCacheReadTokens = allLogs.reduce((sum, log) => { var _a; return sum + (((_a = log.usage) === null || _a === void 0 ? void 0 : _a.cacheReadInputTokens) || 0); }, 0);
633
+ const totalTokens = allLogs.reduce((sum, log) => {
634
+ var _a, _b, _c;
635
+ if ((_a = log.usage) === null || _a === void 0 ? void 0 : _a.totalTokens)
636
+ return sum + log.usage.totalTokens;
637
+ return sum + (((_b = log.usage) === null || _b === void 0 ? void 0 : _b.inputTokens) || 0) + (((_c = log.usage) === null || _c === void 0 ? void 0 : _c.outputTokens) || 0);
638
+ }, 0);
639
+ const avgResponseTime = allLogs.length > 0
640
+ ? allLogs.reduce((sum, log) => sum + (log.responseTime || 0), 0) / allLogs.length
641
+ : 0;
642
+ const successRate = totalRequests > 0 ? (successRequests / totalRequests) * 100 : 0;
643
+ // Calculate coding time (estimate based on tokens and requests)
644
+ // Assume average reading speed: 250 tokens/minute, coding speed: 100 tokens/minute
645
+ const totalCodingTime = Math.round(totalInputTokens / 250 + totalOutputTokens / 100);
646
+ // Group by target type
647
+ const byTargetTypeMap = new Map();
648
+ for (const log of allLogs) {
649
+ const key = log.targetType || 'unknown';
650
+ if (!byTargetTypeMap.has(key)) {
651
+ byTargetTypeMap.set(key, { requests: 0, tokens: 0, responseTime: 0 });
652
+ }
653
+ const stats = byTargetTypeMap.get(key);
654
+ stats.requests++;
655
+ stats.tokens += ((_g = log.usage) === null || _g === void 0 ? void 0 : _g.totalTokens) || (((_h = log.usage) === null || _h === void 0 ? void 0 : _h.inputTokens) || 0) + (((_j = log.usage) === null || _j === void 0 ? void 0 : _j.outputTokens) || 0);
656
+ stats.responseTime += log.responseTime || 0;
657
+ }
658
+ const byTargetType = Array.from(byTargetTypeMap.entries()).map(([targetType, stats]) => ({
659
+ targetType: targetType,
660
+ totalRequests: stats.requests,
661
+ totalTokens: stats.tokens,
662
+ avgResponseTime: stats.requests > 0 ? Math.round(stats.responseTime / stats.requests) : 0,
663
+ }));
664
+ // Group by vendor
665
+ const byVendorMap = new Map();
666
+ for (const log of allLogs) {
667
+ const key = log.vendorId || 'unknown';
668
+ if (!byVendorMap.has(key)) {
669
+ byVendorMap.set(key, { requests: 0, tokens: 0, responseTime: 0 });
670
+ }
671
+ const stats = byVendorMap.get(key);
672
+ stats.requests++;
673
+ stats.tokens += ((_k = log.usage) === null || _k === void 0 ? void 0 : _k.totalTokens) || (((_l = log.usage) === null || _l === void 0 ? void 0 : _l.inputTokens) || 0) + (((_m = log.usage) === null || _m === void 0 ? void 0 : _m.outputTokens) || 0);
674
+ stats.responseTime += log.responseTime || 0;
675
+ }
676
+ const byVendor = Array.from(byVendorMap.entries()).map(([vendorId, stats]) => ({
677
+ vendorId,
678
+ vendorName: vendorMap.get(vendorId) || 'Unknown',
679
+ totalRequests: stats.requests,
680
+ totalTokens: stats.tokens,
681
+ avgResponseTime: stats.requests > 0 ? Math.round(stats.responseTime / stats.requests) : 0,
682
+ }));
683
+ // Group by service
684
+ const byServiceMap = new Map();
685
+ for (const log of allLogs) {
686
+ const key = log.targetServiceId || 'unknown';
687
+ if (!byServiceMap.has(key)) {
688
+ byServiceMap.set(key, { requests: 0, tokens: 0, responseTime: 0 });
689
+ }
690
+ const stats = byServiceMap.get(key);
691
+ stats.requests++;
692
+ stats.tokens += ((_o = log.usage) === null || _o === void 0 ? void 0 : _o.totalTokens) || (((_p = log.usage) === null || _p === void 0 ? void 0 : _p.inputTokens) || 0) + (((_q = log.usage) === null || _q === void 0 ? void 0 : _q.outputTokens) || 0);
693
+ stats.responseTime += log.responseTime || 0;
694
+ }
695
+ const byService = Array.from(byServiceMap.entries()).map(([serviceId, stats]) => {
696
+ const serviceInfo = serviceMap.get(serviceId);
697
+ return {
698
+ serviceId,
699
+ serviceName: (serviceInfo === null || serviceInfo === void 0 ? void 0 : serviceInfo.name) || 'Unknown',
700
+ vendorName: serviceInfo ? vendorMap.get(serviceInfo.vendorId) || 'Unknown' : 'Unknown',
701
+ totalRequests: stats.requests,
702
+ totalTokens: stats.tokens,
703
+ avgResponseTime: stats.requests > 0 ? Math.round(stats.responseTime / stats.requests) : 0,
704
+ };
705
+ });
706
+ // Group by model
707
+ const byModelMap = new Map();
708
+ for (const log of allLogs) {
709
+ const key = log.targetModel || 'unknown';
710
+ if (!byModelMap.has(key)) {
711
+ byModelMap.set(key, { requests: 0, tokens: 0, responseTime: 0 });
712
+ }
713
+ const stats = byModelMap.get(key);
714
+ stats.requests++;
715
+ stats.tokens += ((_r = log.usage) === null || _r === void 0 ? void 0 : _r.totalTokens) || (((_s = log.usage) === null || _s === void 0 ? void 0 : _s.inputTokens) || 0) + (((_t = log.usage) === null || _t === void 0 ? void 0 : _t.outputTokens) || 0);
716
+ stats.responseTime += log.responseTime || 0;
717
+ }
718
+ const byModel = Array.from(byModelMap.entries()).map(([modelName, stats]) => ({
719
+ modelName,
720
+ totalRequests: stats.requests,
721
+ totalTokens: stats.tokens,
722
+ avgResponseTime: stats.requests > 0 ? Math.round(stats.responseTime / stats.requests) : 0,
723
+ }));
724
+ // Timeline data (by day)
725
+ const timelineMap = new Map();
726
+ for (const log of allLogs) {
727
+ const date = new Date(log.timestamp).toISOString().split('T')[0];
728
+ if (!timelineMap.has(date)) {
729
+ timelineMap.set(date, { requests: 0, tokens: 0, inputTokens: 0, outputTokens: 0 });
730
+ }
731
+ const stats = timelineMap.get(date);
732
+ stats.requests++;
733
+ stats.inputTokens += ((_u = log.usage) === null || _u === void 0 ? void 0 : _u.inputTokens) || 0;
734
+ stats.outputTokens += ((_v = log.usage) === null || _v === void 0 ? void 0 : _v.outputTokens) || 0;
735
+ stats.tokens += ((_w = log.usage) === null || _w === void 0 ? void 0 : _w.totalTokens) || (((_x = log.usage) === null || _x === void 0 ? void 0 : _x.inputTokens) || 0) + (((_y = log.usage) === null || _y === void 0 ? void 0 : _y.outputTokens) || 0);
736
+ }
737
+ const timeline = Array.from(timelineMap.entries())
738
+ .map(([date, stats]) => ({
739
+ date,
740
+ totalRequests: stats.requests,
741
+ totalTokens: stats.tokens,
742
+ totalInputTokens: stats.inputTokens,
743
+ totalOutputTokens: stats.outputTokens,
744
+ }))
745
+ .sort((a, b) => a.date.localeCompare(b.date));
746
+ // Content type distribution (infer from request patterns)
747
+ const contentTypeMap = new Map();
748
+ for (const log of allLogs) {
749
+ // Infer content type from request characteristics
750
+ let contentType = 'default';
751
+ if (log.body && (log.body.includes('image') || log.body.includes('base64'))) {
752
+ contentType = 'image-understanding';
753
+ }
754
+ else if (log.requestModel && log.requestModel.toLowerCase().includes('think')) {
755
+ contentType = 'thinking';
756
+ }
757
+ else if (log.usage && log.usage.inputTokens > 12000) {
758
+ contentType = 'long-context';
759
+ }
760
+ contentTypeMap.set(contentType, (contentTypeMap.get(contentType) || 0) + 1);
761
+ }
762
+ const contentTypeDistribution = Array.from(contentTypeMap.entries()).map(([contentType, count]) => ({
763
+ contentType,
764
+ count,
765
+ percentage: totalRequests > 0 ? Math.round((count / totalRequests) * 100) : 0,
766
+ }));
767
+ return {
768
+ overview: {
769
+ totalRequests,
770
+ totalTokens,
771
+ totalInputTokens,
772
+ totalOutputTokens,
773
+ totalCacheReadTokens,
774
+ totalVendors: vendors.length,
775
+ totalServices: services.length,
776
+ totalRoutes: this.getRoutes().length,
777
+ totalRules: this.getRules().length,
778
+ avgResponseTime: Math.round(avgResponseTime),
779
+ successRate: Math.round(successRate * 10) / 10,
780
+ totalCodingTime,
781
+ },
782
+ byTargetType,
783
+ byVendor,
784
+ byService,
785
+ byModel,
786
+ timeline,
787
+ contentTypeDistribution,
788
+ errors: {
789
+ totalErrors: errorLogs.length,
790
+ recentErrors: recentErrorLogs.length,
791
+ },
792
+ };
793
+ });
794
+ }
477
795
  close() {
478
796
  this.db.close();
479
797
  this.logDb.close();
798
+ this.accessLogDb.close();
799
+ this.errorLogDb.close();
800
+ this.blacklistDb.close();
480
801
  }
481
802
  }
482
803
  exports.DatabaseManager = DatabaseManager;
@@ -21,6 +21,7 @@ const database_1 = require("./database");
21
21
  const proxy_server_1 = require("./proxy-server");
22
22
  const os_1 = __importDefault(require("os"));
23
23
  const auth_1 = require("./auth");
24
+ const version_check_1 = require("./version-check");
24
25
  const dotenvPath = path_1.default.resolve(os_1.default.homedir(), '.aicodeswitch/aicodeswitch.conf');
25
26
  if (fs_1.default.existsSync(dotenvPath)) {
26
27
  dotenv_1.default.config({ path: dotenvPath });
@@ -44,6 +45,12 @@ const writeClaudeConfig = (dbManager) => __awaiter(void 0, void 0, void 0, funct
44
45
  const claudeDir = path_1.default.join(homeDir, '.claude');
45
46
  const claudeSettingsPath = path_1.default.join(claudeDir, 'settings.json');
46
47
  const claudeSettingsBakPath = path_1.default.join(claudeDir, 'settings.json.bak');
48
+ const claudeJsonBakPath = path_1.default.join(homeDir, '.claude.json.bak');
49
+ // Check if any backup file already exists
50
+ if (fs_1.default.existsSync(claudeSettingsBakPath) || fs_1.default.existsSync(claudeJsonBakPath)) {
51
+ console.error('Claude backup files already exist, refusing to overwrite');
52
+ return false;
53
+ }
47
54
  if (fs_1.default.existsSync(claudeSettingsPath)) {
48
55
  fs_1.default.renameSync(claudeSettingsPath, claudeSettingsBakPath);
49
56
  }
@@ -60,7 +67,6 @@ const writeClaudeConfig = (dbManager) => __awaiter(void 0, void 0, void 0, funct
60
67
  fs_1.default.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2));
61
68
  // Claude Code .claude.json
62
69
  const claudeJsonPath = path_1.default.join(homeDir, '.claude.json');
63
- const claudeJsonBakPath = path_1.default.join(homeDir, '.claude.json.bak');
64
70
  if (fs_1.default.existsSync(claudeJsonPath)) {
65
71
  fs_1.default.renameSync(claudeJsonPath, claudeJsonBakPath);
66
72
  }
@@ -86,6 +92,12 @@ const writeCodexConfig = (dbManager) => __awaiter(void 0, void 0, void 0, functi
86
92
  const codexDir = path_1.default.join(homeDir, '.codex');
87
93
  const codexConfigPath = path_1.default.join(codexDir, 'config.toml');
88
94
  const codexConfigBakPath = path_1.default.join(codexDir, 'config.toml.bak');
95
+ const codexAuthBakPath = path_1.default.join(codexDir, 'auth.json.bak');
96
+ // Check if any backup file already exists
97
+ if (fs_1.default.existsSync(codexConfigBakPath) || fs_1.default.existsSync(codexAuthBakPath)) {
98
+ console.error('Codex backup files already exist, refusing to overwrite');
99
+ return false;
100
+ }
89
101
  if (fs_1.default.existsSync(codexConfigPath)) {
90
102
  fs_1.default.renameSync(codexConfigPath, codexConfigBakPath);
91
103
  }
@@ -107,7 +119,6 @@ requires_openai_auth = true
107
119
  fs_1.default.writeFileSync(codexConfigPath, codexConfig);
108
120
  // Codex auth.json
109
121
  const codexAuthPath = path_1.default.join(codexDir, 'auth.json');
110
- const codexAuthBakPath = path_1.default.join(codexDir, 'auth.json.bak');
111
122
  if (fs_1.default.existsSync(codexAuthPath)) {
112
123
  fs_1.default.renameSync(codexAuthPath, codexAuthBakPath);
113
124
  }
@@ -351,6 +362,15 @@ const registerRoutes = (dbManager, proxyServer) => {
351
362
  }
352
363
  res.json(result);
353
364
  })));
365
+ app.get('/api/version/check', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
366
+ const versionInfo = yield (0, version_check_1.checkVersionUpdate)();
367
+ res.json(versionInfo);
368
+ })));
369
+ app.get('/api/statistics', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
370
+ const days = typeof req.query.days === 'string' ? parseInt(req.query.days, 10) : 30;
371
+ const stats = yield dbManager.getStatistics(days);
372
+ res.json(stats);
373
+ })));
354
374
  app.use(express_1.default.static(path_1.default.resolve(__dirname, '../ui')));
355
375
  };
356
376
  const start = () => __awaiter(void 0, void 0, void 0, function* () {