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.
- 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 +163 -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 +136 -18
- package/dist/server/notifier.js +100 -0
- package/dist/server/proxy-server.js +73 -34
- 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-BFVjD9Y2.js +0 -799
- package/dist/ui/assets/index-Dm34-4zP.css +0 -1
package/dist/server/main.js
CHANGED
|
@@ -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
|
-
|
|
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://${
|
|
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://${
|
|
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
|
|
1392
|
-
|
|
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
|
|
1404
|
-
|
|
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
|
|
2193
|
-
|
|
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
|
-
|
|
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.
|
|
3436
|
-
|
|
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
|
-
|
|
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://${
|
|
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
|
-
|
|
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
|
+
}
|