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.
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
- 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';
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://${host}:${port}/claude-code`,
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://${host}:${process.env.PORT ? parseInt(process.env.PORT, 10) : 4567}/codex`,
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 logs = yield dbManager.getLogs(limit, offset);
1391
- 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
+ }
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 logs = yield dbManager.getErrorLogs(limit, offset);
1403
- 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
+ }
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 sessions = yield dbManager.getSessions(undefined, limit, offset);
2192
- 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 });
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
- 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);
3365
3526
  if (!isPortUsable) {
3366
- console.error(`端口 ${port} 已被占用,无法启动服务。请执行 aicos stop 后重启。`);
3367
- 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} 已释放,继续启动...`);
3368
3538
  }
3369
3539
  console.time('[Server] step "listen"');
3370
3540
  const server = app.listen(port, host, () => {
3371
3541
  listenReady = true;
3372
- 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`);
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://${host}:${port}/api/tools/install`);
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
- new Promise((resolve) => {
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
+ }