aicodeswitch 5.2.12 → 6.0.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 -2
- package/UPGRADE.md +5 -4
- package/bin/utils/get-server.js +3 -3
- package/dist/server/access-keys/index.js +2 -2
- package/dist/server/access-keys/key-logger.js +63 -309
- package/dist/server/agent-map/activity-extractor.js +383 -0
- package/dist/server/agent-map/agent-map-service.js +1011 -0
- package/dist/server/agent-map/index.js +10 -0
- package/dist/server/agent-map/routes.js +88 -0
- package/dist/server/agent-map/session-meta.js +300 -0
- package/dist/server/conversions/index.js +10 -2
- package/dist/server/conversions/thinking/providers.js +12 -7
- package/dist/server/fs-database.js +169 -926
- package/dist/server/log-store/index.js +21 -0
- package/dist/server/log-store/log-store.js +1367 -0
- package/dist/server/log-store/types.js +2 -0
- package/dist/server/main.js +141 -46
- package/dist/server/notifier.js +100 -0
- package/dist/server/performance-tracker.js +78 -8
- package/dist/server/proxy-server.js +173 -56
- package/dist/server/transformers/chunk-collector.js +112 -25
- package/dist/ui/assets/index-CQVhBlBB.css +1 -0
- package/dist/ui/assets/index-QZBX5HHX.js +1186 -0
- package/dist/ui/assets/three-CFpmPosW.js +3802 -0
- package/dist/ui/index.html +3 -2
- package/package.json +4 -2
- package/scripts/dev.js +283 -0
- package/dist/server/tools-service.js +0 -203
- package/dist/server/websocket-service.js +0 -148
- package/dist/ui/assets/index-BFVjD9Y2.js +0 -799
- package/dist/ui/assets/index-Dm34-4zP.css +0 -1
package/dist/server/main.js
CHANGED
|
@@ -25,12 +25,13 @@ const index_1 = require("./access-keys/index");
|
|
|
25
25
|
const manager_1 = require("./access-keys/manager");
|
|
26
26
|
const policy_manager_1 = require("./access-keys/policy-manager");
|
|
27
27
|
const performance_tracker_1 = require("./performance-tracker");
|
|
28
|
+
const log_store_1 = require("./log-store");
|
|
29
|
+
const agent_map_1 = require("./agent-map");
|
|
30
|
+
const notifier_1 = require("./notifier");
|
|
28
31
|
const os_1 = __importDefault(require("os"));
|
|
29
32
|
const auth_1 = require("./auth");
|
|
30
33
|
const version_check_1 = require("./version-check");
|
|
31
34
|
const utils_1 = require("./utils");
|
|
32
|
-
const tools_service_1 = require("./tools-service");
|
|
33
|
-
const websocket_service_1 = require("./websocket-service");
|
|
34
35
|
const rules_status_service_1 = require("./rules-status-service");
|
|
35
36
|
const type_migration_1 = require("./type-migration");
|
|
36
37
|
const config_metadata_1 = require("./config-metadata");
|
|
@@ -47,8 +48,15 @@ const upgradeHashFilePath = path_1.default.join(appDir, 'upgrade-hash');
|
|
|
47
48
|
if (fs_1.default.existsSync(dotenvPath)) {
|
|
48
49
|
dotenv_1.default.config({ path: dotenvPath });
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
+
// 服务监听地址由 AUTH 模式强制决定(忽略 process.env.HOST):
|
|
52
|
+
// - AUTH 开启:监听 0.0.0.0,允许远端 AccessKey 客户端连接
|
|
53
|
+
// - AUTH 关闭:监听 127.0.0.1,仅本机访问(默认最安全)
|
|
54
|
+
const host = (0, auth_1.isAuthEnabled)() ? '0.0.0.0' : '127.0.0.1';
|
|
51
55
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
|
|
56
|
+
// 写入本地编程工具配置(Codex config.toml / Claude settings.json)+ UI/CLI 展示用的地址恒为回环地址。
|
|
57
|
+
// 即便 AUTH 开启监听 0.0.0.0,本机工具与 dashboard 仍走 127.0.0.1,
|
|
58
|
+
// 避免 0.0.0.0(监听语义)被当成连接目标,导致 Windows 客户端 stream disconnected。
|
|
59
|
+
const clientHost = '127.0.0.1';
|
|
52
60
|
let globalProxyConfig = null;
|
|
53
61
|
function updateProxyConfig(config) {
|
|
54
62
|
if (config.proxyEnabled && config.proxyUrl) {
|
|
@@ -283,7 +291,7 @@ const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermiss
|
|
|
283
291
|
// 构建代理配置
|
|
284
292
|
const claudeSettingsEnv = {
|
|
285
293
|
ANTHROPIC_AUTH_TOKEN: "api_key",
|
|
286
|
-
ANTHROPIC_BASE_URL: `http://${
|
|
294
|
+
ANTHROPIC_BASE_URL: `http://${clientHost}:${port}/claude-code`,
|
|
287
295
|
API_TIMEOUT_MS: "3000000",
|
|
288
296
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
|
289
297
|
CLAUDE_CODE_MAX_RETRIES: 3
|
|
@@ -444,7 +452,7 @@ const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbMana
|
|
|
444
452
|
model_providers: {
|
|
445
453
|
aicodeswitch: {
|
|
446
454
|
name: "aicodeswitch",
|
|
447
|
-
base_url: `http://${
|
|
455
|
+
base_url: `http://${clientHost}:${port}/codex`,
|
|
448
456
|
wire_api: "responses",
|
|
449
457
|
stream_max_retries: 3,
|
|
450
458
|
stream_retry_backoff: "fixed"
|
|
@@ -1341,6 +1349,8 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1341
1349
|
clearInterval(heartbeat);
|
|
1342
1350
|
});
|
|
1343
1351
|
})));
|
|
1352
|
+
// Agent Map(任务可视化节点地图)路由:SSE 实时流 + REST 快照/事件
|
|
1353
|
+
(0, agent_map_1.registerAgentMapRoutes)(app, agent_map_1.agentMapService);
|
|
1344
1354
|
// 清除规则的错误状态(广播 idle 状态给所有客户端)
|
|
1345
1355
|
app.post('/api/rules/:id/clear-status', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1346
1356
|
const { id } = req.params;
|
|
@@ -1388,8 +1398,28 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1388
1398
|
const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
|
|
1389
1399
|
const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
|
|
1390
1400
|
const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1401
|
+
const str = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
1402
|
+
const keyword = str(req.query.keyword) || str(req.query.query);
|
|
1403
|
+
const filters = {
|
|
1404
|
+
targetType: str(req.query.targetType) || undefined,
|
|
1405
|
+
vendorId: str(req.query.vendorId) || undefined,
|
|
1406
|
+
targetServiceId: str(req.query.serviceId) || str(req.query.targetServiceId) || undefined,
|
|
1407
|
+
targetModel: str(req.query.model) || str(req.query.targetModel) || undefined,
|
|
1408
|
+
routeId: str(req.query.routeId) || undefined,
|
|
1409
|
+
};
|
|
1410
|
+
const hasAnyFilter = keyword || filters.targetType || filters.vendorId || filters.targetServiceId || filters.targetModel || filters.routeId;
|
|
1411
|
+
if (hasAnyFilter) {
|
|
1412
|
+
const result = yield dbManager.queryLogs({ filters, keyword, limit, offset });
|
|
1413
|
+
res.json({ logs: result.data, total: result.total });
|
|
1414
|
+
}
|
|
1415
|
+
else {
|
|
1416
|
+
// 无筛选:仍返回 total,避免前端额外请求 count
|
|
1417
|
+
const [logs, total] = yield Promise.all([
|
|
1418
|
+
dbManager.getLogs(limit, offset),
|
|
1419
|
+
dbManager.getLogsCount(),
|
|
1420
|
+
]);
|
|
1421
|
+
res.json({ logs, total });
|
|
1422
|
+
}
|
|
1393
1423
|
})));
|
|
1394
1424
|
app.delete('/api/logs', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1395
1425
|
yield dbManager.clearLogs();
|
|
@@ -1400,8 +1430,27 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1400
1430
|
const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
|
|
1401
1431
|
const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
|
|
1402
1432
|
const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1433
|
+
const str = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
1434
|
+
const filters = {
|
|
1435
|
+
targetType: str(req.query.targetType) || undefined,
|
|
1436
|
+
vendorId: str(req.query.vendorId) || undefined,
|
|
1437
|
+
serviceId: str(req.query.serviceId) || str(req.query.targetServiceId) || undefined,
|
|
1438
|
+
model: str(req.query.model) || str(req.query.targetModel) || undefined,
|
|
1439
|
+
routeId: str(req.query.routeId) || undefined,
|
|
1440
|
+
};
|
|
1441
|
+
const keyword = str(req.query.keyword) || str(req.query.query);
|
|
1442
|
+
const hasAnyFilter = keyword || filters.targetType || filters.vendorId || filters.serviceId || filters.model || filters.routeId;
|
|
1443
|
+
if (hasAnyFilter) {
|
|
1444
|
+
const result = yield dbManager.queryErrorLogs({ filters, keyword, limit, offset });
|
|
1445
|
+
res.json({ logs: result.data, total: result.total });
|
|
1446
|
+
}
|
|
1447
|
+
else {
|
|
1448
|
+
const [logs, total] = yield Promise.all([
|
|
1449
|
+
dbManager.getErrorLogs(limit, offset),
|
|
1450
|
+
dbManager.getErrorLogsCount(),
|
|
1451
|
+
]);
|
|
1452
|
+
res.json({ logs, total });
|
|
1453
|
+
}
|
|
1405
1454
|
})));
|
|
1406
1455
|
app.delete('/api/error-logs', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1407
1456
|
yield dbManager.clearErrorLogs();
|
|
@@ -2189,8 +2238,21 @@ ${instruction}
|
|
|
2189
2238
|
const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
|
|
2190
2239
|
const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
|
|
2191
2240
|
const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
|
|
2192
|
-
const
|
|
2193
|
-
|
|
2241
|
+
const str = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
2242
|
+
const opts = {
|
|
2243
|
+
targetType: str(req.query.targetType) || undefined,
|
|
2244
|
+
keyword: str(req.query.keyword) || str(req.query.query) || undefined,
|
|
2245
|
+
vendorId: str(req.query.vendorId) || undefined,
|
|
2246
|
+
serviceId: str(req.query.serviceId) || undefined,
|
|
2247
|
+
model: str(req.query.model) || undefined,
|
|
2248
|
+
routeId: str(req.query.routeId) || undefined,
|
|
2249
|
+
};
|
|
2250
|
+
const hasFilter = opts.targetType || opts.keyword || opts.vendorId || opts.serviceId || opts.model || opts.routeId;
|
|
2251
|
+
const [sessions, total] = yield Promise.all([
|
|
2252
|
+
dbManager.getSessions(hasFilter ? opts : undefined, limit, offset),
|
|
2253
|
+
dbManager.getSessionsCount(hasFilter ? opts : undefined),
|
|
2254
|
+
]);
|
|
2255
|
+
res.json({ sessions, total });
|
|
2194
2256
|
})));
|
|
2195
2257
|
app.get('/api/sessions/count', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2196
2258
|
const count = yield dbManager.getSessionsCount();
|
|
@@ -2450,19 +2512,6 @@ ${instruction}
|
|
|
2450
2512
|
res.json({ success: false });
|
|
2451
2513
|
}
|
|
2452
2514
|
})));
|
|
2453
|
-
// 工具安装检测相关路由
|
|
2454
|
-
app.get('/api/tools/status', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2455
|
-
console.log('[API] GET /api/tools/status - 获取工具安装状态');
|
|
2456
|
-
try {
|
|
2457
|
-
const status = yield (0, tools_service_1.getToolsInstallationStatus)();
|
|
2458
|
-
console.log('[API] 工具安装状态:', status);
|
|
2459
|
-
res.json(status);
|
|
2460
|
-
}
|
|
2461
|
-
catch (error) {
|
|
2462
|
-
console.error('[API] 获取工具状态失败:', error);
|
|
2463
|
-
res.status(500).json({ error: '获取工具状态失败' });
|
|
2464
|
-
}
|
|
2465
|
-
})));
|
|
2466
2515
|
// MCP 工具管理相关路由
|
|
2467
2516
|
app.get('/api/mcps', (_req, res) => {
|
|
2468
2517
|
res.json(dbManager.getMCPs());
|
|
@@ -3369,6 +3418,20 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3369
3418
|
// 自动检测数据库类型并执行迁移(如果需要)
|
|
3370
3419
|
console.time('[Server] step "database-init"');
|
|
3371
3420
|
const dbManager = yield database_factory_1.DatabaseFactory.createAuto(dataDir, legacyDataDir);
|
|
3421
|
+
// 创建并初始化共享 LogStore(追加写 NDJSON),注入 dbManager
|
|
3422
|
+
const logStore = (0, log_store_1.createLogStore)(dataDir);
|
|
3423
|
+
yield logStore.init();
|
|
3424
|
+
// 在服务对外提供流量前完成旧 JSON → NDJSON 迁移(含自愈:标记缺失时清空重迁,避免重复)。
|
|
3425
|
+
// 必须在 listen 之前,防止迁移的「清空重迁」与实时写入竞争。
|
|
3426
|
+
try {
|
|
3427
|
+
yield logStore.migrateLegacy(dataDir);
|
|
3428
|
+
}
|
|
3429
|
+
catch (err) {
|
|
3430
|
+
console.error('[Server] LogStore legacy migration failed:', err);
|
|
3431
|
+
}
|
|
3432
|
+
dbManager.setLogStore(logStore);
|
|
3433
|
+
// Agent Map 服务接入 dbManager(种子化已有 Session + 启动状态清扫定时器)
|
|
3434
|
+
agent_map_1.agentMapService.attach(dbManager);
|
|
3372
3435
|
console.timeEnd('[Server] step "database-init"');
|
|
3373
3436
|
// 服务启动时自动同步配置文件(适用于 CLI 和 dev:server)
|
|
3374
3437
|
console.time('[Server] step "sync-configs"');
|
|
@@ -3386,7 +3449,7 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3386
3449
|
catch ( /* ignore */_a) { /* ignore */ }
|
|
3387
3450
|
const proxyServer = new proxy_server_1.ProxyServer(dbManager, app);
|
|
3388
3451
|
// Initialize AccessKey module
|
|
3389
|
-
const accessKeyModule = new index_1.AccessKeyModule(dataDir);
|
|
3452
|
+
const accessKeyModule = new index_1.AccessKeyModule(dataDir, logStore);
|
|
3390
3453
|
try {
|
|
3391
3454
|
yield accessKeyModule.initialize();
|
|
3392
3455
|
proxyServer.setAccessKeyModule(accessKeyModule);
|
|
@@ -3394,6 +3457,15 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3394
3457
|
catch (error) {
|
|
3395
3458
|
console.error('[Server] AccessKey module initialization failed:', error);
|
|
3396
3459
|
}
|
|
3460
|
+
// 日志保留期定时清理(主库 global + 所有 AccessKey key:*),每 6h 一次
|
|
3461
|
+
const logRetentionTimer = setInterval(() => {
|
|
3462
|
+
Promise.all([
|
|
3463
|
+
logStore.retain('global', 30).catch(() => { }),
|
|
3464
|
+
accessKeyModule.keyLogger.cleanupOldLogs().catch(() => { }),
|
|
3465
|
+
]).catch(() => { });
|
|
3466
|
+
}, 6 * 60 * 60 * 1000);
|
|
3467
|
+
if (typeof logRetentionTimer.unref === 'function')
|
|
3468
|
+
logRetentionTimer.unref();
|
|
3397
3469
|
// Initialize Service Performance Tracker (全局统计,与 AUTH 无关)
|
|
3398
3470
|
const performanceTracker = new performance_tracker_1.ServicePerformanceTracker(dataDir);
|
|
3399
3471
|
try {
|
|
@@ -3430,15 +3502,34 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3430
3502
|
res.setHeader('Content-Type', 'application/json');
|
|
3431
3503
|
res.status(500).json({ error: err.message || 'Internal server error' });
|
|
3432
3504
|
});
|
|
3433
|
-
|
|
3505
|
+
// 端口检测:若被占用,可能是上一个进程仍在退出过程中(Ctrl+C 后 shutdown 尚未完全释放端口),
|
|
3506
|
+
// 此处轮询等待其释放后再继续启动,避免与正在退出的旧进程发生端口冲突。
|
|
3507
|
+
// 超时仍未释放才判定为真正的占用并报错退出。
|
|
3508
|
+
const PORT_POLL_INTERVAL = 300;
|
|
3509
|
+
const PORT_WAIT_TIMEOUT = 10000;
|
|
3510
|
+
let isPortUsable = yield (0, utils_1.checkPortUsable)(port);
|
|
3434
3511
|
if (!isPortUsable) {
|
|
3435
|
-
console.
|
|
3436
|
-
|
|
3512
|
+
console.warn(`端口 ${port} 当前被占用,可能是上一个服务进程仍在退出中,等待其释放...`);
|
|
3513
|
+
const portDeadline = Date.now() + PORT_WAIT_TIMEOUT;
|
|
3514
|
+
while (!isPortUsable && Date.now() < portDeadline) {
|
|
3515
|
+
yield new Promise(resolve => setTimeout(resolve, PORT_POLL_INTERVAL));
|
|
3516
|
+
isPortUsable = yield (0, utils_1.checkPortUsable)(port);
|
|
3517
|
+
}
|
|
3518
|
+
if (!isPortUsable) {
|
|
3519
|
+
console.error(`端口 ${port} 在 ${PORT_WAIT_TIMEOUT / 1000}s 后仍被占用,无法启动服务。请执行 aicos stop 后重启。`);
|
|
3520
|
+
process.exit(1);
|
|
3521
|
+
}
|
|
3522
|
+
console.log(`端口 ${port} 已释放,继续启动...`);
|
|
3437
3523
|
}
|
|
3438
3524
|
console.time('[Server] step "listen"');
|
|
3439
3525
|
const server = app.listen(port, host, () => {
|
|
3440
3526
|
listenReady = true;
|
|
3441
|
-
|
|
3527
|
+
const listenInfo = host === '0.0.0.0'
|
|
3528
|
+
? ` (listening on all interfaces, port ${port})`
|
|
3529
|
+
: '';
|
|
3530
|
+
console.log(`Admin server running on http://${clientHost}:${port}${listenInfo}`);
|
|
3531
|
+
// 点击 OS 通知时打开任务地图页(仅 terminal-notifier 路径生效;osascript 无法控制点击)
|
|
3532
|
+
(0, notifier_1.setNotifierAppUrl)(`http://${clientHost}:${port}/#/agent-map`);
|
|
3442
3533
|
console.timeEnd('[Server] step "listen"');
|
|
3443
3534
|
// 启动后异步执行延迟维护任务(分片校验/修复、日志清理、会话索引构建)
|
|
3444
3535
|
// 不阻塞服务启动,后台静默执行
|
|
@@ -3457,26 +3548,12 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3457
3548
|
}
|
|
3458
3549
|
setImmediate(() => process.exit(1));
|
|
3459
3550
|
});
|
|
3460
|
-
// 创建 WebSocket 服务器用于工具安装
|
|
3461
|
-
const toolInstallWss = (0, websocket_service_1.createToolInstallationWSServer)();
|
|
3462
3551
|
// 设置黑名单检查函数,用于在规则状态同步时检查黑名单是否已过期
|
|
3463
3552
|
rules_status_service_1.rulesStatusBroadcaster.setBlacklistChecker((serviceId, routeId, contentType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3464
3553
|
// 检查服务��否在黑名单中
|
|
3465
3554
|
const isBlacklisted = yield dbManager.isServiceBlacklisted(serviceId, routeId, contentType);
|
|
3466
3555
|
return isBlacklisted;
|
|
3467
3556
|
}));
|
|
3468
|
-
// 将 WebSocket 服务器附加到 HTTP 服务器(仅用于工具安装)
|
|
3469
|
-
server.on('upgrade', (request, socket, head) => {
|
|
3470
|
-
if (request.url === '/api/tools/install') {
|
|
3471
|
-
toolInstallWss.handleUpgrade(request, socket, head, (ws) => {
|
|
3472
|
-
toolInstallWss.emit('connection', ws, request);
|
|
3473
|
-
});
|
|
3474
|
-
}
|
|
3475
|
-
else {
|
|
3476
|
-
socket.destroy();
|
|
3477
|
-
}
|
|
3478
|
-
});
|
|
3479
|
-
console.log(`WebSocket server for tool installation attached to ws://${host}:${port}/api/tools/install`);
|
|
3480
3557
|
let isShuttingDown = false;
|
|
3481
3558
|
let shutdownPromise = null;
|
|
3482
3559
|
const shutdown = (signal) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -3486,6 +3563,18 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3486
3563
|
isShuttingDown = true;
|
|
3487
3564
|
shutdownPromise = (() => __awaiter(void 0, void 0, void 0, function* () {
|
|
3488
3565
|
console.log(`[Server] Received ${signal}, shutting down...`);
|
|
3566
|
+
// 立即停止监听以释放端口(同步关闭监听句柄,端口马上可用),
|
|
3567
|
+
// 避免在漫长的清理流程期间端口仍被占用,导致此时重启产生 EADDRINUSE 冲突。
|
|
3568
|
+
const serverClosedPromise = new Promise((resolve) => {
|
|
3569
|
+
server.close(() => resolve());
|
|
3570
|
+
});
|
|
3571
|
+
// 强制断开所有现存连接(含浏览器长连的 SSE:agent-map stream、rules-status
|
|
3572
|
+
// 广播等)。否则这些连接永不自行关闭,server.close 的回调要等满下面 5s 超时
|
|
3573
|
+
// 才触发,表现为 Ctrl+C 后「卡很久才打印 Server stopped.」。closeAllConnections
|
|
3574
|
+
// 立即销毁所有 socket,让 close 回调几乎瞬时完成。
|
|
3575
|
+
if (typeof server.closeAllConnections === 'function') {
|
|
3576
|
+
server.closeAllConnections();
|
|
3577
|
+
}
|
|
3489
3578
|
// 服务终止前恢复配置文件(适用于 aicos stop 与 Ctrl+C)
|
|
3490
3579
|
try {
|
|
3491
3580
|
const claudeRestored = yield restoreClaudeConfig();
|
|
@@ -3517,12 +3606,18 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3517
3606
|
console.error('[Shutdown ...] Performance tracker flush failed:', error);
|
|
3518
3607
|
}
|
|
3519
3608
|
dbManager.close();
|
|
3609
|
+
// 落盘 LogStore 所有 namespace 的索引
|
|
3610
|
+
try {
|
|
3611
|
+
yield logStore.close();
|
|
3612
|
+
}
|
|
3613
|
+
catch (error) {
|
|
3614
|
+
console.error('[Shutdown ...] LogStore close failed:', error);
|
|
3615
|
+
}
|
|
3520
3616
|
// 清理规则状态广播器(关闭 SSE 连接)
|
|
3521
3617
|
rules_status_service_1.rulesStatusBroadcaster.destroy();
|
|
3618
|
+
// 等待监听句柄与现有连接关闭完成(最多 5s),确保端口彻底释放后再退出。
|
|
3522
3619
|
yield Promise.race([
|
|
3523
|
-
|
|
3524
|
-
server.close(() => resolve());
|
|
3525
|
-
}),
|
|
3620
|
+
serverClosedPromise,
|
|
3526
3621
|
new Promise((resolve) => {
|
|
3527
3622
|
setTimeout(resolve, 5000);
|
|
3528
3623
|
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setNotifierAppUrl = setNotifierAppUrl;
|
|
4
|
+
exports.notify = notify;
|
|
5
|
+
/**
|
|
6
|
+
* 跨平台 OS 原生通知(零 npm 依赖,shell-out)。
|
|
7
|
+
*
|
|
8
|
+
* macOS:直接用 `osascript display notification`。它以 Terminal/Script Editor 的身份发起,
|
|
9
|
+
* 这些身份在本机已有通知权限,因此**始终能可靠弹出**。代价:通知图标位是系统默认占位
|
|
10
|
+
* (无自定义 Logo)——尝试用「带 Logo 的 AppleScript applet」方案在较新 macOS 上会被系统
|
|
11
|
+
* 静默拦截(ad-hoc applet 无通知权限),故回退到这条最稳的路径。
|
|
12
|
+
*
|
|
13
|
+
* - linux:notify-send(libnotify;未安装则静默)
|
|
14
|
+
* - win32:PowerShell + WinForms NotifyIcon balloon(best-effort)
|
|
15
|
+
*
|
|
16
|
+
* 任何环节失败均静默,绝不影响代理主流程。
|
|
17
|
+
*/
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
/** 转义 AppleScript 字符串里的反斜杠与双引号 */
|
|
20
|
+
function escApple(s) {
|
|
21
|
+
return String(s !== null && s !== void 0 ? s : '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
22
|
+
}
|
|
23
|
+
// terminal-notifier 存在性(带缓存)。它是 macOS 上唯一能「点击通知可控」的可靠手段:
|
|
24
|
+
// osascript 的 display notification 点击必激活脚本宿主(Script Editor/文本阅读器),无法关闭;
|
|
25
|
+
// terminal-notifier 点击可经 -open 打开我们指定的 URL(默认无 URL 则点击=仅消失、无动作)。
|
|
26
|
+
let hasTN;
|
|
27
|
+
function hasTerminalNotifier() {
|
|
28
|
+
if (hasTN !== undefined)
|
|
29
|
+
return hasTN;
|
|
30
|
+
try {
|
|
31
|
+
(0, child_process_1.execFileSync)('which', ['terminal-notifier'], { stdio: 'ignore', windowsHide: true });
|
|
32
|
+
hasTN = true;
|
|
33
|
+
}
|
|
34
|
+
catch (_a) {
|
|
35
|
+
hasTN = false;
|
|
36
|
+
}
|
|
37
|
+
return hasTN;
|
|
38
|
+
}
|
|
39
|
+
// 点击通知要打开的 URL(由 main.ts 在服务启动后设置,指向 AICodeSwitch 任务地图页)。
|
|
40
|
+
// 仅 terminal-notifier 路径生效;osascript 无法控制点击行为。
|
|
41
|
+
let appOpenUrl = null;
|
|
42
|
+
function setNotifierAppUrl(url) { appOpenUrl = url; }
|
|
43
|
+
function notifyDarwin(opts) {
|
|
44
|
+
// 优先 terminal-notifier:可控制点击行为。设了 appOpenUrl 则点击打开该 URL(我们的页面),
|
|
45
|
+
// 否则点击仅消失、无动作。
|
|
46
|
+
if (hasTerminalNotifier()) {
|
|
47
|
+
const args = ['-title', opts.title, '-message', opts.body, '-ignoreDn'];
|
|
48
|
+
if (opts.subtitle)
|
|
49
|
+
args.push('-subtitle', opts.subtitle);
|
|
50
|
+
if (appOpenUrl)
|
|
51
|
+
args.push('-open', appOpenUrl);
|
|
52
|
+
(0, child_process_1.execFile)('terminal-notifier', args, { windowsHide: true }, () => { });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// 回退:osascript(稳定弹通知,但点击会打开脚本宿主,无法避免)
|
|
56
|
+
let script = `display notification "${escApple(opts.body)}" with title "${escApple(opts.title)}"`;
|
|
57
|
+
if (opts.subtitle)
|
|
58
|
+
script += ` subtitle "${escApple(opts.subtitle)}"`;
|
|
59
|
+
(0, child_process_1.execFile)('osascript', ['-e', script], { windowsHide: true }, () => { });
|
|
60
|
+
}
|
|
61
|
+
function notifyLinux(opts) {
|
|
62
|
+
(0, child_process_1.execFile)('notify-send', ['-a', 'AICodeSwitch', opts.title, opts.body], { windowsHide: true }, () => { });
|
|
63
|
+
}
|
|
64
|
+
function notifyWindows(opts) {
|
|
65
|
+
var _a, _b;
|
|
66
|
+
// WinForms NotifyIcon balloon(Win10/11 上显示为 toast)
|
|
67
|
+
const title = String((_a = opts.title) !== null && _a !== void 0 ? _a : '').replace(/'/g, "''");
|
|
68
|
+
const body = String((_b = opts.body) !== null && _b !== void 0 ? _b : '').replace(/'/g, "''");
|
|
69
|
+
const script = [
|
|
70
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
71
|
+
'$n = New-Object System.Windows.Forms.NotifyIcon',
|
|
72
|
+
'$n.Icon = [System.Drawing.SystemIcons]::Information',
|
|
73
|
+
'$n.Visible = $true',
|
|
74
|
+
`$n.ShowBalloonTip(5000, '${title}', '${body}', [System.Windows.Forms.ToolTipIcon]::Info)`,
|
|
75
|
+
'Start-Sleep -Seconds 6',
|
|
76
|
+
'$n.Dispose()',
|
|
77
|
+
].join('; ');
|
|
78
|
+
const child = (0, child_process_1.spawn)('powershell', ['-NoProfile', '-Command', script], {
|
|
79
|
+
stdio: 'ignore',
|
|
80
|
+
windowsHide: true,
|
|
81
|
+
detached: true,
|
|
82
|
+
});
|
|
83
|
+
child.on('error', () => { });
|
|
84
|
+
child.unref();
|
|
85
|
+
}
|
|
86
|
+
/** 发送一条 OS 原生通知。失败静默。title 请自带「AICodeSwitch」标识来源。 */
|
|
87
|
+
function notify(opts) {
|
|
88
|
+
try {
|
|
89
|
+
if (process.platform === 'darwin')
|
|
90
|
+
return notifyDarwin(opts);
|
|
91
|
+
if (process.platform === 'linux')
|
|
92
|
+
return notifyLinux(opts);
|
|
93
|
+
if (process.platform === 'win32')
|
|
94
|
+
return notifyWindows(opts);
|
|
95
|
+
// 其它平台:no-op
|
|
96
|
+
}
|
|
97
|
+
catch (_a) {
|
|
98
|
+
/* ignore */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -68,8 +68,12 @@ class ServicePerformanceTracker {
|
|
|
68
68
|
try {
|
|
69
69
|
const raw = yield promises_1.default.readFile(this.filePath, 'utf-8');
|
|
70
70
|
const parsed = JSON.parse(raw);
|
|
71
|
-
if (parsed && parsed.vendors)
|
|
71
|
+
if (parsed && parsed.vendors) {
|
|
72
72
|
this.file = parsed;
|
|
73
|
+
// 旧数据文件可能缺新增的 token 字段(sumInputTokens/sumTotalTokens),
|
|
74
|
+
// 直接累加会得到 undefined+N=NaN,污染派生值。这里补齐为 0。
|
|
75
|
+
this.migrateBuckets(this.file);
|
|
76
|
+
}
|
|
73
77
|
}
|
|
74
78
|
catch (_a) {
|
|
75
79
|
// 首次启动或文件损坏,使用空桶
|
|
@@ -77,6 +81,42 @@ class ServicePerformanceTracker {
|
|
|
77
81
|
}
|
|
78
82
|
});
|
|
79
83
|
}
|
|
84
|
+
/** 加载后补齐旧数据缺失的桶字段,避免 undefined 累加成 NaN */
|
|
85
|
+
migrateBuckets(file) {
|
|
86
|
+
for (const v of Object.values(file.vendors)) {
|
|
87
|
+
if (v.vendorRollup)
|
|
88
|
+
this.normalizeAggregate(v.vendorRollup);
|
|
89
|
+
for (const s of Object.values(v.services || {})) {
|
|
90
|
+
if (s.serviceRollup)
|
|
91
|
+
this.normalizeAggregate(s.serviceRollup);
|
|
92
|
+
for (const agg of Object.values(s.models || {})) {
|
|
93
|
+
this.normalizeAggregate(agg);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
normalizeAggregate(agg) {
|
|
99
|
+
this.normalizeBucket(agg.precise);
|
|
100
|
+
this.normalizeBucket(agg.estimated);
|
|
101
|
+
for (const b of Object.values(agg.hourly || {}))
|
|
102
|
+
this.normalizeBucket(b);
|
|
103
|
+
}
|
|
104
|
+
normalizeBucket(b) {
|
|
105
|
+
if (!b)
|
|
106
|
+
return;
|
|
107
|
+
if (typeof b.count !== 'number')
|
|
108
|
+
b.count = 0;
|
|
109
|
+
if (typeof b.sumTtftMs !== 'number')
|
|
110
|
+
b.sumTtftMs = 0;
|
|
111
|
+
if (typeof b.sumTps !== 'number')
|
|
112
|
+
b.sumTps = 0;
|
|
113
|
+
if (typeof b.totalOutputTokens !== 'number')
|
|
114
|
+
b.totalOutputTokens = 0;
|
|
115
|
+
if (typeof b.sumInputTokens !== 'number')
|
|
116
|
+
b.sumInputTokens = 0;
|
|
117
|
+
if (typeof b.sumTotalTokens !== 'number')
|
|
118
|
+
b.sumTotalTokens = 0;
|
|
119
|
+
}
|
|
80
120
|
/**
|
|
81
121
|
* 记录一次请求的性能数据点(三级同步聚合)。
|
|
82
122
|
* 纯内存操作,不阻塞调用方。
|
|
@@ -257,7 +297,7 @@ class ServicePerformanceTracker {
|
|
|
257
297
|
};
|
|
258
298
|
}
|
|
259
299
|
emptyBucket() {
|
|
260
|
-
return { count: 0, sumTtftMs: 0, sumTps: 0, totalOutputTokens: 0 };
|
|
300
|
+
return { count: 0, sumTtftMs: 0, sumTps: 0, totalOutputTokens: 0, sumInputTokens: 0, sumTotalTokens: 0 };
|
|
261
301
|
}
|
|
262
302
|
accumulate(agg, m, hour, withExtremes) {
|
|
263
303
|
var _a;
|
|
@@ -268,6 +308,7 @@ class ServicePerformanceTracker {
|
|
|
268
308
|
const bucket = m.timingAccuracy === 'precise' ? agg.precise : agg.estimated;
|
|
269
309
|
const hasTtft = m.timingAccuracy === 'precise' && typeof m.ttftMs === 'number';
|
|
270
310
|
const hasTps = typeof m.tokensPerSecond === 'number';
|
|
311
|
+
// token 量与计时精度无关,precise/estimated 桶都累加
|
|
271
312
|
bucket.count += 1;
|
|
272
313
|
if (hasTtft)
|
|
273
314
|
bucket.sumTtftMs += m.ttftMs;
|
|
@@ -275,18 +316,37 @@ class ServicePerformanceTracker {
|
|
|
275
316
|
bucket.sumTps += m.tokensPerSecond;
|
|
276
317
|
if (m.outputTokens)
|
|
277
318
|
bucket.totalOutputTokens += m.outputTokens;
|
|
278
|
-
|
|
319
|
+
if (m.inputTokens)
|
|
320
|
+
bucket.sumInputTokens += m.inputTokens;
|
|
321
|
+
if (m.totalTokens)
|
|
322
|
+
bucket.sumTotalTokens += m.totalTokens;
|
|
323
|
+
// 小时走势桶:
|
|
324
|
+
// - count/sumTtftMs/sumTps 仅精确样本计入(避免估算样本污染 TTFT/TPM 均值)
|
|
325
|
+
// - token 三项与精度无关,所有样本都计入走势(含非流式)
|
|
326
|
+
const hb = (_a = agg.hourly[hour]) !== null && _a !== void 0 ? _a : (agg.hourly[hour] = this.emptyBucket());
|
|
327
|
+
let touchedHourly = false;
|
|
279
328
|
if (m.timingAccuracy === 'precise') {
|
|
280
|
-
const hb = (_a = agg.hourly[hour]) !== null && _a !== void 0 ? _a : (agg.hourly[hour] = this.emptyBucket());
|
|
281
329
|
hb.count += 1;
|
|
282
330
|
if (hasTtft)
|
|
283
331
|
hb.sumTtftMs += m.ttftMs;
|
|
284
332
|
if (hasTps)
|
|
285
333
|
hb.sumTps += m.tokensPerSecond;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
334
|
+
touchedHourly = true;
|
|
335
|
+
}
|
|
336
|
+
if (m.outputTokens) {
|
|
337
|
+
hb.totalOutputTokens += m.outputTokens;
|
|
338
|
+
touchedHourly = true;
|
|
339
|
+
}
|
|
340
|
+
if (m.inputTokens) {
|
|
341
|
+
hb.sumInputTokens += m.inputTokens;
|
|
342
|
+
touchedHourly = true;
|
|
289
343
|
}
|
|
344
|
+
if (m.totalTokens) {
|
|
345
|
+
hb.sumTotalTokens += m.totalTokens;
|
|
346
|
+
touchedHourly = true;
|
|
347
|
+
}
|
|
348
|
+
if (touchedHourly)
|
|
349
|
+
this.trimHourly(agg.hourly);
|
|
290
350
|
// 极值(仅模型级、仅精确样本)
|
|
291
351
|
if (withExtremes && m.timingAccuracy === 'precise') {
|
|
292
352
|
if (hasTtft) {
|
|
@@ -315,9 +375,14 @@ class ServicePerformanceTracker {
|
|
|
315
375
|
// ---------------- 内部:派生 ----------------
|
|
316
376
|
derive(agg) {
|
|
317
377
|
const p = agg.precise;
|
|
378
|
+
const e = agg.estimated;
|
|
318
379
|
const count = p.count;
|
|
319
380
|
const avgTtftMs = count > 0 ? p.sumTtftMs / count : 0;
|
|
320
381
|
const avgTps = count > 0 ? p.sumTps / count : 0;
|
|
382
|
+
// token 量跨 precise+estimated 求和(含非流式样本)
|
|
383
|
+
const totalOutputTokens = p.totalOutputTokens + e.totalOutputTokens;
|
|
384
|
+
const totalInputTokens = p.sumInputTokens + e.sumInputTokens;
|
|
385
|
+
const totalTokens = p.sumTotalTokens + e.sumTotalTokens;
|
|
321
386
|
return {
|
|
322
387
|
count,
|
|
323
388
|
avgTtftMs,
|
|
@@ -327,7 +392,9 @@ class ServicePerformanceTracker {
|
|
|
327
392
|
minTps: agg.minTps,
|
|
328
393
|
maxTps: agg.maxTps,
|
|
329
394
|
errorCount: agg.errorCount,
|
|
330
|
-
totalOutputTokens
|
|
395
|
+
totalOutputTokens,
|
|
396
|
+
totalInputTokens,
|
|
397
|
+
totalTokens,
|
|
331
398
|
successRate: count + agg.errorCount > 0 ? count / (count + agg.errorCount) : 0,
|
|
332
399
|
};
|
|
333
400
|
}
|
|
@@ -355,6 +422,9 @@ class ServicePerformanceTracker {
|
|
|
355
422
|
count: b.count,
|
|
356
423
|
avgTtftMs: b.count > 0 ? b.sumTtftMs / b.count : 0,
|
|
357
424
|
avgTpm: b.count > 0 ? (b.sumTps / b.count) * 60 : 0,
|
|
425
|
+
inputTokens: b.sumInputTokens,
|
|
426
|
+
outputTokens: b.totalOutputTokens,
|
|
427
|
+
totalTokens: b.sumTotalTokens,
|
|
358
428
|
}));
|
|
359
429
|
}
|
|
360
430
|
locateService(serviceId) {
|