aicodeswitch 5.2.12 → 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.
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -25,6 +25,9 @@ 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");
@@ -47,8 +50,15 @@ const upgradeHashFilePath = path_1.default.join(appDir, 'upgrade-hash');
47
50
  if (fs_1.default.existsSync(dotenvPath)) {
48
51
  dotenv_1.default.config({ path: dotenvPath });
49
52
  }
50
- const host = process.env.HOST || '0.0.0.0';
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';
51
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';
52
62
  let globalProxyConfig = null;
53
63
  function updateProxyConfig(config) {
54
64
  if (config.proxyEnabled && config.proxyUrl) {
@@ -283,7 +293,7 @@ const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermiss
283
293
  // 构建代理配置
284
294
  const claudeSettingsEnv = {
285
295
  ANTHROPIC_AUTH_TOKEN: "api_key",
286
- ANTHROPIC_BASE_URL: `http://${host}:${port}/claude-code`,
296
+ ANTHROPIC_BASE_URL: `http://${clientHost}:${port}/claude-code`,
287
297
  API_TIMEOUT_MS: "3000000",
288
298
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
289
299
  CLAUDE_CODE_MAX_RETRIES: 3
@@ -444,7 +454,7 @@ const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbMana
444
454
  model_providers: {
445
455
  aicodeswitch: {
446
456
  name: "aicodeswitch",
447
- base_url: `http://${host}:${process.env.PORT ? parseInt(process.env.PORT, 10) : 4567}/codex`,
457
+ base_url: `http://${clientHost}:${port}/codex`,
448
458
  wire_api: "responses",
449
459
  stream_max_retries: 3,
450
460
  stream_retry_backoff: "fixed"
@@ -1341,6 +1351,8 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
1341
1351
  clearInterval(heartbeat);
1342
1352
  });
1343
1353
  })));
1354
+ // Agent Map(任务可视化节点地图)路由:SSE 实时流 + REST 快照/事件
1355
+ (0, agent_map_1.registerAgentMapRoutes)(app, agent_map_1.agentMapService);
1344
1356
  // 清除规则的错误状态(广播 idle 状态给所有客户端)
1345
1357
  app.post('/api/rules/:id/clear-status', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1346
1358
  const { id } = req.params;
@@ -1388,8 +1400,28 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
1388
1400
  const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
1389
1401
  const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
1390
1402
  const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
1391
- const logs = yield dbManager.getLogs(limit, offset);
1392
- res.json(logs);
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
+ }
1393
1425
  })));
1394
1426
  app.delete('/api/logs', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
1395
1427
  yield dbManager.clearLogs();
@@ -1400,8 +1432,27 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
1400
1432
  const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
1401
1433
  const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
1402
1434
  const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
1403
- const logs = yield dbManager.getErrorLogs(limit, offset);
1404
- res.json(logs);
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
+ }
1405
1456
  })));
1406
1457
  app.delete('/api/error-logs', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
1407
1458
  yield dbManager.clearErrorLogs();
@@ -2189,8 +2240,21 @@ ${instruction}
2189
2240
  const rawOffset = typeof req.query.offset === 'string' ? parseInt(req.query.offset, 10) : NaN;
2190
2241
  const limit = Number.isFinite(rawLimit) ? rawLimit : 100;
2191
2242
  const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
2192
- const sessions = yield dbManager.getSessions(undefined, limit, offset);
2193
- res.json(sessions);
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 });
2194
2258
  })));
2195
2259
  app.get('/api/sessions/count', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
2196
2260
  const count = yield dbManager.getSessionsCount();
@@ -3369,6 +3433,20 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3369
3433
  // 自动检测数据库类型并执行迁移(如果需要)
3370
3434
  console.time('[Server] step "database-init"');
3371
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);
3372
3450
  console.timeEnd('[Server] step "database-init"');
3373
3451
  // 服务启动时自动同步配置文件(适用于 CLI 和 dev:server)
3374
3452
  console.time('[Server] step "sync-configs"');
@@ -3386,7 +3464,7 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3386
3464
  catch ( /* ignore */_a) { /* ignore */ }
3387
3465
  const proxyServer = new proxy_server_1.ProxyServer(dbManager, app);
3388
3466
  // Initialize AccessKey module
3389
- const accessKeyModule = new index_1.AccessKeyModule(dataDir);
3467
+ const accessKeyModule = new index_1.AccessKeyModule(dataDir, logStore);
3390
3468
  try {
3391
3469
  yield accessKeyModule.initialize();
3392
3470
  proxyServer.setAccessKeyModule(accessKeyModule);
@@ -3394,6 +3472,15 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3394
3472
  catch (error) {
3395
3473
  console.error('[Server] AccessKey module initialization failed:', error);
3396
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();
3397
3484
  // Initialize Service Performance Tracker (全局统计,与 AUTH 无关)
3398
3485
  const performanceTracker = new performance_tracker_1.ServicePerformanceTracker(dataDir);
3399
3486
  try {
@@ -3430,15 +3517,34 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3430
3517
  res.setHeader('Content-Type', 'application/json');
3431
3518
  res.status(500).json({ error: err.message || 'Internal server error' });
3432
3519
  });
3433
- const isPortUsable = yield (0, utils_1.checkPortUsable)(port);
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);
3434
3526
  if (!isPortUsable) {
3435
- console.error(`端口 ${port} 已被占用,无法启动服务。请执行 aicos stop 后重启。`);
3436
- process.exit(1);
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} 已释放,继续启动...`);
3437
3538
  }
3438
3539
  console.time('[Server] step "listen"');
3439
3540
  const server = app.listen(port, host, () => {
3440
3541
  listenReady = true;
3441
- console.log(`Admin server running on http://${host}:${port}`);
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`);
3442
3548
  console.timeEnd('[Server] step "listen"');
3443
3549
  // 启动后异步执行延迟维护任务(分片校验/修复、日志清理、会话索引构建)
3444
3550
  // 不阻塞服务启动,后台静默执行
@@ -3476,7 +3582,7 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3476
3582
  socket.destroy();
3477
3583
  }
3478
3584
  });
3479
- console.log(`WebSocket server for tool installation attached to ws://${host}:${port}/api/tools/install`);
3585
+ console.log(`WebSocket server for tool installation attached to ws://${clientHost}:${port}/api/tools/install`);
3480
3586
  let isShuttingDown = false;
3481
3587
  let shutdownPromise = null;
3482
3588
  const shutdown = (signal) => __awaiter(void 0, void 0, void 0, function* () {
@@ -3486,6 +3592,12 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3486
3592
  isShuttingDown = true;
3487
3593
  shutdownPromise = (() => __awaiter(void 0, void 0, void 0, function* () {
3488
3594
  console.log(`[Server] Received ${signal}, shutting down...`);
3595
+ // 立即停止监听以释放端口(同步关闭监听句柄,端口马上可用),
3596
+ // 避免在漫长的清理流程期间端口仍被占用,导致此时重启产生 EADDRINUSE 冲突。
3597
+ // 现有连接会继续处理直到完成或超时;其回调与下面的清理流程并行等待。
3598
+ const serverClosedPromise = new Promise((resolve) => {
3599
+ server.close(() => resolve());
3600
+ });
3489
3601
  // 服务终止前恢复配置文件(适用于 aicos stop 与 Ctrl+C)
3490
3602
  try {
3491
3603
  const claudeRestored = yield restoreClaudeConfig();
@@ -3517,12 +3629,18 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3517
3629
  console.error('[Shutdown ...] Performance tracker flush failed:', error);
3518
3630
  }
3519
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
+ }
3520
3639
  // 清理规则状态广播器(关闭 SSE 连接)
3521
3640
  rules_status_service_1.rulesStatusBroadcaster.destroy();
3641
+ // 等待监听句柄与现有连接关闭完成(最多 5s),确保端口彻底释放后再退出。
3522
3642
  yield Promise.race([
3523
- new Promise((resolve) => {
3524
- server.close(() => resolve());
3525
- }),
3643
+ serverClosedPromise,
3526
3644
  new Promise((resolve) => {
3527
3645
  setTimeout(resolve, 5000);
3528
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
+ }