aicodeswitch 5.2.11 → 5.2.13
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 +707 -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/fs-database.js +166 -929
- 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 +213 -18
- package/dist/server/notifier.js +100 -0
- package/dist/server/performance-tracker.js +377 -0
- package/dist/server/proxy-server.js +142 -39
- package/dist/server/rules-status-service.js +21 -6
- package/dist/server/transformers/stream-timing-transform.js +63 -0
- package/dist/ui/assets/index-4liE4bV8.css +1 -0
- package/dist/ui/assets/index-CJMAVkIN.js +813 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -2
- package/scripts/dev.js +237 -0
- package/dist/ui/assets/index-DR6cZIa7.js +0 -799
- package/dist/ui/assets/index-MjMlew6J.css +0 -1
package/dist/server/main.js
CHANGED
|
@@ -24,6 +24,10 @@ const proxy_server_1 = require("./proxy-server");
|
|
|
24
24
|
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
|
+
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");
|
|
27
31
|
const os_1 = __importDefault(require("os"));
|
|
28
32
|
const auth_1 = require("./auth");
|
|
29
33
|
const version_check_1 = require("./version-check");
|
|
@@ -46,8 +50,15 @@ const upgradeHashFilePath = path_1.default.join(appDir, 'upgrade-hash');
|
|
|
46
50
|
if (fs_1.default.existsSync(dotenvPath)) {
|
|
47
51
|
dotenv_1.default.config({ path: dotenvPath });
|
|
48
52
|
}
|
|
49
|
-
|
|
53
|
+
// 服务监听地址由 AUTH 模式强制决定(忽略 process.env.HOST):
|
|
54
|
+
// - AUTH 开启:监听 0.0.0.0,允许远端 AccessKey 客户端连接
|
|
55
|
+
// - AUTH 关闭:监听 127.0.0.1,仅本机访问(默认最安全)
|
|
56
|
+
const host = (0, auth_1.isAuthEnabled)() ? '0.0.0.0' : '127.0.0.1';
|
|
50
57
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
|
|
58
|
+
// 写入本地编程工具配置(Codex config.toml / Claude settings.json)+ UI/CLI 展示用的地址恒为回环地址。
|
|
59
|
+
// 即便 AUTH 开启监听 0.0.0.0,本机工具与 dashboard 仍走 127.0.0.1,
|
|
60
|
+
// 避免 0.0.0.0(监听语义)被当成连接目标,导致 Windows 客户端 stream disconnected。
|
|
61
|
+
const clientHost = '127.0.0.1';
|
|
51
62
|
let globalProxyConfig = null;
|
|
52
63
|
function updateProxyConfig(config) {
|
|
53
64
|
if (config.proxyEnabled && config.proxyUrl) {
|
|
@@ -282,7 +293,7 @@ const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermiss
|
|
|
282
293
|
// 构建代理配置
|
|
283
294
|
const claudeSettingsEnv = {
|
|
284
295
|
ANTHROPIC_AUTH_TOKEN: "api_key",
|
|
285
|
-
ANTHROPIC_BASE_URL: `http://${
|
|
296
|
+
ANTHROPIC_BASE_URL: `http://${clientHost}:${port}/claude-code`,
|
|
286
297
|
API_TIMEOUT_MS: "3000000",
|
|
287
298
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
|
288
299
|
CLAUDE_CODE_MAX_RETRIES: 3
|
|
@@ -443,7 +454,7 @@ const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbMana
|
|
|
443
454
|
model_providers: {
|
|
444
455
|
aicodeswitch: {
|
|
445
456
|
name: "aicodeswitch",
|
|
446
|
-
base_url: `http://${
|
|
457
|
+
base_url: `http://${clientHost}:${port}/codex`,
|
|
447
458
|
wire_api: "responses",
|
|
448
459
|
stream_max_retries: 3,
|
|
449
460
|
stream_retry_backoff: "fixed"
|
|
@@ -1340,6 +1351,8 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1340
1351
|
clearInterval(heartbeat);
|
|
1341
1352
|
});
|
|
1342
1353
|
})));
|
|
1354
|
+
// Agent Map(任务可视化节点地图)路由:SSE 实时流 + REST 快照/事件
|
|
1355
|
+
(0, agent_map_1.registerAgentMapRoutes)(app, agent_map_1.agentMapService);
|
|
1343
1356
|
// 清除规则的错误状态(广播 idle 状态给所有客户端)
|
|
1344
1357
|
app.post('/api/rules/:id/clear-status', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1345
1358
|
const { id } = req.params;
|
|
@@ -1387,8 +1400,28 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1387
1400
|
const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
|
|
1388
1401
|
const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
|
|
1389
1402
|
const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1403
|
+
const str = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
1404
|
+
const keyword = str(req.query.keyword) || str(req.query.query);
|
|
1405
|
+
const filters = {
|
|
1406
|
+
targetType: str(req.query.targetType) || undefined,
|
|
1407
|
+
vendorId: str(req.query.vendorId) || undefined,
|
|
1408
|
+
targetServiceId: str(req.query.serviceId) || str(req.query.targetServiceId) || undefined,
|
|
1409
|
+
targetModel: str(req.query.model) || str(req.query.targetModel) || undefined,
|
|
1410
|
+
routeId: str(req.query.routeId) || undefined,
|
|
1411
|
+
};
|
|
1412
|
+
const hasAnyFilter = keyword || filters.targetType || filters.vendorId || filters.targetServiceId || filters.targetModel || filters.routeId;
|
|
1413
|
+
if (hasAnyFilter) {
|
|
1414
|
+
const result = yield dbManager.queryLogs({ filters, keyword, limit, offset });
|
|
1415
|
+
res.json({ logs: result.data, total: result.total });
|
|
1416
|
+
}
|
|
1417
|
+
else {
|
|
1418
|
+
// 无筛选:仍返回 total,避免前端额外请求 count
|
|
1419
|
+
const [logs, total] = yield Promise.all([
|
|
1420
|
+
dbManager.getLogs(limit, offset),
|
|
1421
|
+
dbManager.getLogsCount(),
|
|
1422
|
+
]);
|
|
1423
|
+
res.json({ logs, total });
|
|
1424
|
+
}
|
|
1392
1425
|
})));
|
|
1393
1426
|
app.delete('/api/logs', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1394
1427
|
yield dbManager.clearLogs();
|
|
@@ -1399,8 +1432,27 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1399
1432
|
const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
|
|
1400
1433
|
const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
|
|
1401
1434
|
const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1435
|
+
const str = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
1436
|
+
const filters = {
|
|
1437
|
+
targetType: str(req.query.targetType) || undefined,
|
|
1438
|
+
vendorId: str(req.query.vendorId) || undefined,
|
|
1439
|
+
serviceId: str(req.query.serviceId) || str(req.query.targetServiceId) || undefined,
|
|
1440
|
+
model: str(req.query.model) || str(req.query.targetModel) || undefined,
|
|
1441
|
+
routeId: str(req.query.routeId) || undefined,
|
|
1442
|
+
};
|
|
1443
|
+
const keyword = str(req.query.keyword) || str(req.query.query);
|
|
1444
|
+
const hasAnyFilter = keyword || filters.targetType || filters.vendorId || filters.serviceId || filters.model || filters.routeId;
|
|
1445
|
+
if (hasAnyFilter) {
|
|
1446
|
+
const result = yield dbManager.queryErrorLogs({ filters, keyword, limit, offset });
|
|
1447
|
+
res.json({ logs: result.data, total: result.total });
|
|
1448
|
+
}
|
|
1449
|
+
else {
|
|
1450
|
+
const [logs, total] = yield Promise.all([
|
|
1451
|
+
dbManager.getErrorLogs(limit, offset),
|
|
1452
|
+
dbManager.getErrorLogsCount(),
|
|
1453
|
+
]);
|
|
1454
|
+
res.json({ logs, total });
|
|
1455
|
+
}
|
|
1404
1456
|
})));
|
|
1405
1457
|
app.delete('/api/error-logs', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1406
1458
|
yield dbManager.clearErrorLogs();
|
|
@@ -2188,8 +2240,21 @@ ${instruction}
|
|
|
2188
2240
|
const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
|
|
2189
2241
|
const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
|
|
2190
2242
|
const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
|
|
2191
|
-
const
|
|
2192
|
-
|
|
2243
|
+
const str = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
2244
|
+
const opts = {
|
|
2245
|
+
targetType: str(req.query.targetType) || undefined,
|
|
2246
|
+
keyword: str(req.query.keyword) || str(req.query.query) || undefined,
|
|
2247
|
+
vendorId: str(req.query.vendorId) || undefined,
|
|
2248
|
+
serviceId: str(req.query.serviceId) || undefined,
|
|
2249
|
+
model: str(req.query.model) || undefined,
|
|
2250
|
+
routeId: str(req.query.routeId) || undefined,
|
|
2251
|
+
};
|
|
2252
|
+
const hasFilter = opts.targetType || opts.keyword || opts.vendorId || opts.serviceId || opts.model || opts.routeId;
|
|
2253
|
+
const [sessions, total] = yield Promise.all([
|
|
2254
|
+
dbManager.getSessions(hasFilter ? opts : undefined, limit, offset),
|
|
2255
|
+
dbManager.getSessionsCount(hasFilter ? opts : undefined),
|
|
2256
|
+
]);
|
|
2257
|
+
res.json({ sessions, total });
|
|
2193
2258
|
})));
|
|
2194
2259
|
app.get('/api/sessions/count', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2195
2260
|
const count = yield dbManager.getSessionsCount();
|
|
@@ -3088,6 +3153,64 @@ ${instruction}
|
|
|
3088
3153
|
}
|
|
3089
3154
|
res.json(alerts);
|
|
3090
3155
|
})));
|
|
3156
|
+
// ============ 服务性能统计(全局,与 AUTH 无关) ============
|
|
3157
|
+
const requirePerfTracker = (res) => {
|
|
3158
|
+
const tracker = proxyServer.getPerformanceTracker();
|
|
3159
|
+
if (!tracker) {
|
|
3160
|
+
res.status(503).json({ error: 'Performance tracker not initialized' });
|
|
3161
|
+
}
|
|
3162
|
+
return tracker;
|
|
3163
|
+
};
|
|
3164
|
+
// 全部 API 服务平铺一览(含所属供应商)
|
|
3165
|
+
app.get('/api/performance/services-overview', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3166
|
+
const tracker = requirePerfTracker(res);
|
|
3167
|
+
if (!tracker)
|
|
3168
|
+
return;
|
|
3169
|
+
res.json(tracker.getServicesOverview());
|
|
3170
|
+
})));
|
|
3171
|
+
// 全部供应商一览
|
|
3172
|
+
app.get('/api/performance/vendors', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3173
|
+
const tracker = requirePerfTracker(res);
|
|
3174
|
+
if (!tracker)
|
|
3175
|
+
return;
|
|
3176
|
+
res.json(tracker.getVendorsOverview());
|
|
3177
|
+
})));
|
|
3178
|
+
// 某供应商详情(rollup + 其下服务)
|
|
3179
|
+
app.get('/api/performance/vendors/:vendorId', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3180
|
+
const tracker = requirePerfTracker(res);
|
|
3181
|
+
if (!tracker)
|
|
3182
|
+
return;
|
|
3183
|
+
const detail = tracker.getVendorDetail(req.params.vendorId);
|
|
3184
|
+
if (!detail) {
|
|
3185
|
+
res.status(404).json({ error: 'Vendor not found' });
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
res.json(detail);
|
|
3189
|
+
})));
|
|
3190
|
+
// 某服务详情(rollup + 其下模型)
|
|
3191
|
+
app.get('/api/performance/services/:serviceId', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3192
|
+
const tracker = requirePerfTracker(res);
|
|
3193
|
+
if (!tracker)
|
|
3194
|
+
return;
|
|
3195
|
+
const detail = tracker.getServiceDetail(req.params.serviceId);
|
|
3196
|
+
if (!detail) {
|
|
3197
|
+
res.status(404).json({ error: 'Service not found' });
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
res.json(detail);
|
|
3201
|
+
})));
|
|
3202
|
+
// 单模型详情(派生 + 小时走势 + 极值)
|
|
3203
|
+
app.get('/api/performance/services/:serviceId/models/:model', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3204
|
+
const tracker = requirePerfTracker(res);
|
|
3205
|
+
if (!tracker)
|
|
3206
|
+
return;
|
|
3207
|
+
const detail = tracker.getModelDetail(req.params.serviceId, decodeURIComponent(req.params.model));
|
|
3208
|
+
if (!detail) {
|
|
3209
|
+
res.status(404).json({ error: 'Model not found' });
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
res.json(detail);
|
|
3213
|
+
})));
|
|
3091
3214
|
// 写入MCP配置到Claude Code或Codex的全局配置文件
|
|
3092
3215
|
const writeMCPConfig = (targetType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3093
3216
|
try {
|
|
@@ -3310,6 +3433,20 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3310
3433
|
// 自动检测数据库类型并执行迁移(如果需要)
|
|
3311
3434
|
console.time('[Server] step "database-init"');
|
|
3312
3435
|
const dbManager = yield database_factory_1.DatabaseFactory.createAuto(dataDir, legacyDataDir);
|
|
3436
|
+
// 创建并初始化共享 LogStore(追加写 NDJSON),注入 dbManager
|
|
3437
|
+
const logStore = (0, log_store_1.createLogStore)(dataDir);
|
|
3438
|
+
yield logStore.init();
|
|
3439
|
+
// 在服务对外提供流量前完成旧 JSON → NDJSON 迁移(含自愈:标记缺失时清空重迁,避免重复)。
|
|
3440
|
+
// 必须在 listen 之前,防止迁移的「清空重迁」与实时写入竞争。
|
|
3441
|
+
try {
|
|
3442
|
+
yield logStore.migrateLegacy(dataDir);
|
|
3443
|
+
}
|
|
3444
|
+
catch (err) {
|
|
3445
|
+
console.error('[Server] LogStore legacy migration failed:', err);
|
|
3446
|
+
}
|
|
3447
|
+
dbManager.setLogStore(logStore);
|
|
3448
|
+
// Agent Map 服务接入 dbManager(种子化已有 Session + 启动状态清扫定时器)
|
|
3449
|
+
agent_map_1.agentMapService.attach(dbManager);
|
|
3313
3450
|
console.timeEnd('[Server] step "database-init"');
|
|
3314
3451
|
// 服务启动时自动同步配置文件(适用于 CLI 和 dev:server)
|
|
3315
3452
|
console.time('[Server] step "sync-configs"');
|
|
@@ -3327,7 +3464,7 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3327
3464
|
catch ( /* ignore */_a) { /* ignore */ }
|
|
3328
3465
|
const proxyServer = new proxy_server_1.ProxyServer(dbManager, app);
|
|
3329
3466
|
// Initialize AccessKey module
|
|
3330
|
-
const accessKeyModule = new index_1.AccessKeyModule(dataDir);
|
|
3467
|
+
const accessKeyModule = new index_1.AccessKeyModule(dataDir, logStore);
|
|
3331
3468
|
try {
|
|
3332
3469
|
yield accessKeyModule.initialize();
|
|
3333
3470
|
proxyServer.setAccessKeyModule(accessKeyModule);
|
|
@@ -3335,6 +3472,25 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3335
3472
|
catch (error) {
|
|
3336
3473
|
console.error('[Server] AccessKey module initialization failed:', error);
|
|
3337
3474
|
}
|
|
3475
|
+
// 日志保留期定时清理(主库 global + 所有 AccessKey key:*),每 6h 一次
|
|
3476
|
+
const logRetentionTimer = setInterval(() => {
|
|
3477
|
+
Promise.all([
|
|
3478
|
+
logStore.retain('global', 30).catch(() => { }),
|
|
3479
|
+
accessKeyModule.keyLogger.cleanupOldLogs().catch(() => { }),
|
|
3480
|
+
]).catch(() => { });
|
|
3481
|
+
}, 6 * 60 * 60 * 1000);
|
|
3482
|
+
if (typeof logRetentionTimer.unref === 'function')
|
|
3483
|
+
logRetentionTimer.unref();
|
|
3484
|
+
// Initialize Service Performance Tracker (全局统计,与 AUTH 无关)
|
|
3485
|
+
const performanceTracker = new performance_tracker_1.ServicePerformanceTracker(dataDir);
|
|
3486
|
+
try {
|
|
3487
|
+
yield performanceTracker.initialize();
|
|
3488
|
+
performanceTracker.startAutoFlush();
|
|
3489
|
+
proxyServer.setPerformanceTracker(performanceTracker);
|
|
3490
|
+
}
|
|
3491
|
+
catch (error) {
|
|
3492
|
+
console.error('[Server] Performance tracker initialization failed:', error);
|
|
3493
|
+
}
|
|
3338
3494
|
// 恢复已写入本地的 AccessKey(在代理配置写入之后、AccessKey 模块初始化之后)
|
|
3339
3495
|
try {
|
|
3340
3496
|
applyWriteLocalRecords(proxyServer);
|
|
@@ -3361,15 +3517,34 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3361
3517
|
res.setHeader('Content-Type', 'application/json');
|
|
3362
3518
|
res.status(500).json({ error: err.message || 'Internal server error' });
|
|
3363
3519
|
});
|
|
3364
|
-
|
|
3520
|
+
// 端口检测:若被占用,可能是上一个进程仍在退出过程中(Ctrl+C 后 shutdown 尚未完全释放端口),
|
|
3521
|
+
// 此处轮询等待其释放后再继续启动,避免与正在退出的旧进程发生端口冲突。
|
|
3522
|
+
// 超时仍未释放才判定为真正的占用并报错退出。
|
|
3523
|
+
const PORT_POLL_INTERVAL = 300;
|
|
3524
|
+
const PORT_WAIT_TIMEOUT = 10000;
|
|
3525
|
+
let isPortUsable = yield (0, utils_1.checkPortUsable)(port);
|
|
3365
3526
|
if (!isPortUsable) {
|
|
3366
|
-
console.
|
|
3367
|
-
|
|
3527
|
+
console.warn(`端口 ${port} 当前被占用,可能是上一个服务进程仍在退出中,等待其释放...`);
|
|
3528
|
+
const portDeadline = Date.now() + PORT_WAIT_TIMEOUT;
|
|
3529
|
+
while (!isPortUsable && Date.now() < portDeadline) {
|
|
3530
|
+
yield new Promise(resolve => setTimeout(resolve, PORT_POLL_INTERVAL));
|
|
3531
|
+
isPortUsable = yield (0, utils_1.checkPortUsable)(port);
|
|
3532
|
+
}
|
|
3533
|
+
if (!isPortUsable) {
|
|
3534
|
+
console.error(`端口 ${port} 在 ${PORT_WAIT_TIMEOUT / 1000}s 后仍被占用,无法启动服务。请执行 aicos stop 后重启。`);
|
|
3535
|
+
process.exit(1);
|
|
3536
|
+
}
|
|
3537
|
+
console.log(`端口 ${port} 已释放,继续启动...`);
|
|
3368
3538
|
}
|
|
3369
3539
|
console.time('[Server] step "listen"');
|
|
3370
3540
|
const server = app.listen(port, host, () => {
|
|
3371
3541
|
listenReady = true;
|
|
3372
|
-
|
|
3542
|
+
const listenInfo = host === '0.0.0.0'
|
|
3543
|
+
? ` (listening on all interfaces, port ${port})`
|
|
3544
|
+
: '';
|
|
3545
|
+
console.log(`Admin server running on http://${clientHost}:${port}${listenInfo}`);
|
|
3546
|
+
// 点击 OS 通知时打开任务地图页(仅 terminal-notifier 路径生效;osascript 无法控制点击)
|
|
3547
|
+
(0, notifier_1.setNotifierAppUrl)(`http://${clientHost}:${port}/#/agent-map`);
|
|
3373
3548
|
console.timeEnd('[Server] step "listen"');
|
|
3374
3549
|
// 启动后异步执行延迟维护任务(分片校验/修复、日志清理、会话索引构建)
|
|
3375
3550
|
// 不阻塞服务启动,后台静默执行
|
|
@@ -3407,7 +3582,7 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3407
3582
|
socket.destroy();
|
|
3408
3583
|
}
|
|
3409
3584
|
});
|
|
3410
|
-
console.log(`WebSocket server for tool installation attached to ws://${
|
|
3585
|
+
console.log(`WebSocket server for tool installation attached to ws://${clientHost}:${port}/api/tools/install`);
|
|
3411
3586
|
let isShuttingDown = false;
|
|
3412
3587
|
let shutdownPromise = null;
|
|
3413
3588
|
const shutdown = (signal) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -3417,6 +3592,12 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3417
3592
|
isShuttingDown = true;
|
|
3418
3593
|
shutdownPromise = (() => __awaiter(void 0, void 0, void 0, function* () {
|
|
3419
3594
|
console.log(`[Server] Received ${signal}, shutting down...`);
|
|
3595
|
+
// 立即停止监听以释放端口(同步关闭监听句柄,端口马上可用),
|
|
3596
|
+
// 避免在漫长的清理流程期间端口仍被占用,导致此时重启产生 EADDRINUSE 冲突。
|
|
3597
|
+
// 现有连接会继续处理直到完成或超时;其回调与下面的清理流程并行等待。
|
|
3598
|
+
const serverClosedPromise = new Promise((resolve) => {
|
|
3599
|
+
server.close(() => resolve());
|
|
3600
|
+
});
|
|
3420
3601
|
// 服务终止前恢复配置文件(适用于 aicos stop 与 Ctrl+C)
|
|
3421
3602
|
try {
|
|
3422
3603
|
const claudeRestored = yield restoreClaudeConfig();
|
|
@@ -3439,13 +3620,27 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3439
3620
|
catch (error) {
|
|
3440
3621
|
console.error('[Shutdown ...] AccessKey module shutdown failed:', error);
|
|
3441
3622
|
}
|
|
3623
|
+
// Flush 服务性能统计(全局桶)后停止定时刷盘
|
|
3624
|
+
try {
|
|
3625
|
+
performanceTracker.stopAutoFlush();
|
|
3626
|
+
yield performanceTracker.flush();
|
|
3627
|
+
}
|
|
3628
|
+
catch (error) {
|
|
3629
|
+
console.error('[Shutdown ...] Performance tracker flush failed:', error);
|
|
3630
|
+
}
|
|
3442
3631
|
dbManager.close();
|
|
3632
|
+
// 落盘 LogStore 所有 namespace 的索引
|
|
3633
|
+
try {
|
|
3634
|
+
yield logStore.close();
|
|
3635
|
+
}
|
|
3636
|
+
catch (error) {
|
|
3637
|
+
console.error('[Shutdown ...] LogStore close failed:', error);
|
|
3638
|
+
}
|
|
3443
3639
|
// 清理规则状态广播器(关闭 SSE 连接)
|
|
3444
3640
|
rules_status_service_1.rulesStatusBroadcaster.destroy();
|
|
3641
|
+
// 等待监听句柄与现有连接关闭完成(最多 5s),确保端口彻底释放后再退出。
|
|
3445
3642
|
yield Promise.race([
|
|
3446
|
-
|
|
3447
|
-
server.close(() => resolve());
|
|
3448
|
-
}),
|
|
3643
|
+
serverClosedPromise,
|
|
3449
3644
|
new Promise((resolve) => {
|
|
3450
3645
|
setTimeout(resolve, 5000);
|
|
3451
3646
|
})
|
|
@@ -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
|
+
}
|