aicodeswitch 1.3.8 → 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 +10 -0
- package/README.md +6 -0
- package/bin/cli.js +3 -0
- package/bin/restore.js +1 -1
- package/bin/ui.js +173 -0
- package/dist/server/database.js +321 -0
- package/dist/server/main.js +22 -2
- package/dist/server/proxy-server.js +263 -67
- package/dist/server/transformers/chunk-collector.js +150 -1
- package/dist/server/version-check.js +114 -0
- package/dist/ui/assets/{index-Cj2o6J8f.css → index-dcQX0zYo.css} +1 -1
- package/dist/ui/assets/index-s2lL7OMJ.js +358 -0
- package/dist/ui/index.html +2 -2
- package/package.json +3 -1
- package/dist/ui/assets/index-DcJFhVnN.js +0 -285
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
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/restore.js
CHANGED
|
@@ -220,7 +220,7 @@ const restore = async () => {
|
|
|
220
220
|
|
|
221
221
|
console.log('');
|
|
222
222
|
console.log(chalk.cyan('💡 Tips:\n'));
|
|
223
|
-
console.log(chalk.white(' •
|
|
223
|
+
console.log(chalk.white(' • Restart server: ') + chalk.cyan('aicos restart'));
|
|
224
224
|
console.log(chalk.white(' • Start server: ') + chalk.cyan('aicos start'));
|
|
225
225
|
console.log('\n');
|
|
226
226
|
};
|
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();
|
package/dist/server/database.js
CHANGED
|
@@ -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;
|
package/dist/server/main.js
CHANGED
|
@@ -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* () {
|