tt-help-cli-ycl 1.4.74 → 1.4.76
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/package.json +1 -1
- package/src/watch/public/app.js +125 -34
- package/src/watch/public/style.css +3 -2
- package/src/watch/watchdog-server.js +64 -1
- package/src/watchdog/agent.js +148 -4
- package/src/watchdog/process-manager.js +24 -6
- package/src/watchdog/reporter.js +24 -1
package/package.json
CHANGED
package/src/watch/public/app.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// === 2026-08-07 变更 ===
|
|
2
|
+
// 1. renderWatchdogDashboard: 客户端按名称排序(在线优先,离线排最后)
|
|
3
|
+
// 2. renderClientCard / renderOfflineCard: 离线客户端红色标题+红色边框
|
|
4
|
+
// 3. 整体字体调大一号(12px→14px, 14px→16px, 10px→12px 等)
|
|
5
|
+
// 4. 新增 wdClearLlm() + 🧹 按钮(清除指定客户端 LLM 分析提示)
|
|
6
|
+
// 5. renderCmdRows 新增 llmAnalysis 参数,显示 🤖 AI 分析行
|
|
1
7
|
const COLORS = [
|
|
2
8
|
"#fe2c55",
|
|
3
9
|
"#60a5fa",
|
|
@@ -3503,27 +3509,52 @@ function renderWatchdogDashboard(clients, clientCommands) {
|
|
|
3503
3509
|
return;
|
|
3504
3510
|
}
|
|
3505
3511
|
|
|
3512
|
+
// 排序:按名称升序,离线排最后
|
|
3513
|
+
var sortedClients = clients.slice().sort(function (a, b) {
|
|
3514
|
+
var nameA = (
|
|
3515
|
+
(a.config && a.config.userId) ||
|
|
3516
|
+
a.hostname ||
|
|
3517
|
+
""
|
|
3518
|
+
).toLowerCase();
|
|
3519
|
+
var nameB = (
|
|
3520
|
+
(b.config && b.config.userId) ||
|
|
3521
|
+
b.hostname ||
|
|
3522
|
+
""
|
|
3523
|
+
).toLowerCase();
|
|
3524
|
+
// 在线优先
|
|
3525
|
+
if (a.online !== b.online) return a.online ? -1 : 1;
|
|
3526
|
+
return nameA.localeCompare(nameB);
|
|
3527
|
+
});
|
|
3528
|
+
|
|
3506
3529
|
// 构建 userId → 在线客户端 映射
|
|
3507
3530
|
var userClients = {};
|
|
3508
|
-
|
|
3531
|
+
sortedClients.forEach(function (c) {
|
|
3509
3532
|
var uid = (c.config && c.config.userId) || "";
|
|
3510
3533
|
if (uid) userClients[uid] = c;
|
|
3511
3534
|
});
|
|
3512
3535
|
|
|
3513
3536
|
// 收集所有卡片:全部注册客户端 + 未上线但有命令配置的 userId
|
|
3514
3537
|
var allCards = [];
|
|
3515
|
-
|
|
3538
|
+
sortedClients.forEach(function (c) {
|
|
3516
3539
|
allCards.push(renderClientCard(c, clientCommands));
|
|
3517
3540
|
});
|
|
3541
|
+
// 离线卡也按名称排序
|
|
3542
|
+
var offlineUids = [];
|
|
3518
3543
|
for (var uid in clientCommands) {
|
|
3519
3544
|
if (
|
|
3520
3545
|
!userClients[uid] &&
|
|
3521
3546
|
clientCommands[uid] &&
|
|
3522
3547
|
clientCommands[uid].length > 0
|
|
3523
3548
|
) {
|
|
3524
|
-
|
|
3549
|
+
offlineUids.push(uid);
|
|
3525
3550
|
}
|
|
3526
3551
|
}
|
|
3552
|
+
offlineUids.sort(function (a, b) {
|
|
3553
|
+
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
3554
|
+
});
|
|
3555
|
+
offlineUids.forEach(function (uid) {
|
|
3556
|
+
allCards.push(renderOfflineCard(uid, clientCommands[uid]));
|
|
3557
|
+
});
|
|
3527
3558
|
|
|
3528
3559
|
// 数据指纹:只有变化时才重建卡片 HTML
|
|
3529
3560
|
var fingerprint = JSON.stringify(allCards);
|
|
@@ -3570,7 +3601,7 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3570
3601
|
var cmdCount = cmds ? cmds.length : 0;
|
|
3571
3602
|
return styled(
|
|
3572
3603
|
"div",
|
|
3573
|
-
"background:#1e1e2e;border:
|
|
3604
|
+
"background:#1e1e2e;border:2px solid #f87171;border-radius:12px;padding:16px;opacity:0.9",
|
|
3574
3605
|
styled(
|
|
3575
3606
|
"div",
|
|
3576
3607
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
|
|
@@ -3581,7 +3612,7 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3581
3612
|
"span",
|
|
3582
3613
|
{
|
|
3583
3614
|
style:
|
|
3584
|
-
"color:#
|
|
3615
|
+
"color:#f87171;font-size:16px;font-weight:bold;cursor:pointer",
|
|
3585
3616
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3586
3617
|
title: "点击编辑客户端配置",
|
|
3587
3618
|
},
|
|
@@ -3592,28 +3623,38 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3592
3623
|
{
|
|
3593
3624
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3594
3625
|
style:
|
|
3595
|
-
"padding:1px 5px;font-size:
|
|
3626
|
+
"padding:1px 5px;font-size:12px;background:transparent;color:#f87171;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3596
3627
|
title: "编辑客户端配置",
|
|
3597
3628
|
},
|
|
3598
3629
|
"⚙️",
|
|
3599
3630
|
) +
|
|
3631
|
+
tag(
|
|
3632
|
+
"button",
|
|
3633
|
+
{
|
|
3634
|
+
onclick: "wdClearLlm('" + esc(userId) + "')",
|
|
3635
|
+
style:
|
|
3636
|
+
"padding:1px 5px;font-size:11px;background:transparent;color:#888;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3637
|
+
title: "清除此客户端的 LLM 分析提示",
|
|
3638
|
+
},
|
|
3639
|
+
"🧹",
|
|
3640
|
+
) +
|
|
3600
3641
|
styled(
|
|
3601
3642
|
"span",
|
|
3602
|
-
"color:#555;font-size:
|
|
3643
|
+
"color:#555;font-size:12px;margin-left:8px",
|
|
3603
3644
|
"(未上线)",
|
|
3604
3645
|
),
|
|
3605
3646
|
) +
|
|
3606
3647
|
styled(
|
|
3607
3648
|
"div",
|
|
3608
3649
|
"display:flex;gap:10px;align-items:center",
|
|
3609
|
-
styled("span", "color:#f87171", "● 离线") +
|
|
3610
|
-
styled("span", "color:#555;font-size:
|
|
3650
|
+
styled("span", "color:#f87171;font-size:13px", "● 离线") +
|
|
3651
|
+
styled("span", "color:#555;font-size:12px", "—") +
|
|
3611
3652
|
tag(
|
|
3612
3653
|
"button",
|
|
3613
3654
|
{
|
|
3614
3655
|
onclick: "wdDeleteClient('" + esc(userId) + "')",
|
|
3615
3656
|
style:
|
|
3616
|
-
"padding:2px 8px;font-size:
|
|
3657
|
+
"padding:2px 8px;font-size:12px;background:#991b1b;color:#fff;border:none;border-radius:3px;cursor:pointer",
|
|
3617
3658
|
title: "删除此客户端配置",
|
|
3618
3659
|
},
|
|
3619
3660
|
"删除",
|
|
@@ -3622,7 +3663,7 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3622
3663
|
) +
|
|
3623
3664
|
styled(
|
|
3624
3665
|
"div",
|
|
3625
|
-
"color:#555;font-size:
|
|
3666
|
+
"color:#555;font-size:12px",
|
|
3626
3667
|
"该机器尚未连接看门狗,配置了 " + cmdCount + " 条命令",
|
|
3627
3668
|
) +
|
|
3628
3669
|
renderCmdRows(cmds || [], userId, [], false),
|
|
@@ -3665,6 +3706,7 @@ function renderClientCard(c, clientCommands) {
|
|
|
3665
3706
|
procs = c.processes || [];
|
|
3666
3707
|
var userId = cfg.userId || "";
|
|
3667
3708
|
var myCmds = userId && clientCommands[userId] ? clientCommands[userId] : [];
|
|
3709
|
+
var online = c.online;
|
|
3668
3710
|
|
|
3669
3711
|
var osStr = [
|
|
3670
3712
|
os.platform,
|
|
@@ -3683,11 +3725,15 @@ function renderClientCard(c, clientCommands) {
|
|
|
3683
3725
|
(os.nodeVersion || "?") +
|
|
3684
3726
|
" | 运行 " +
|
|
3685
3727
|
(os.uptime != null ? Math.round(os.uptime / 3600) + "h" : "?");
|
|
3686
|
-
var onlineBadge =
|
|
3728
|
+
var onlineBadge = online
|
|
3687
3729
|
? styled("span", "color:#4ade80", "● 在线")
|
|
3688
3730
|
: styled("span", "color:#f87171", "● 离线");
|
|
3689
3731
|
var lastSeen = new Date(c.lastHeartbeat).toLocaleTimeString("zh-CN");
|
|
3690
3732
|
|
|
3733
|
+
// 离线时标题和边框变红
|
|
3734
|
+
var titleColor = online ? "#60a5fa" : "#f87171";
|
|
3735
|
+
var borderColor = online ? "#333" : "#f87171";
|
|
3736
|
+
|
|
3691
3737
|
var header = styled(
|
|
3692
3738
|
"div",
|
|
3693
3739
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
|
|
@@ -3697,7 +3743,10 @@ function renderClientCard(c, clientCommands) {
|
|
|
3697
3743
|
tag(
|
|
3698
3744
|
"span",
|
|
3699
3745
|
{
|
|
3700
|
-
style:
|
|
3746
|
+
style:
|
|
3747
|
+
"color:" +
|
|
3748
|
+
titleColor +
|
|
3749
|
+
";font-size:16px;font-weight:bold;cursor:pointer",
|
|
3701
3750
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3702
3751
|
title: "点击编辑客户端配置",
|
|
3703
3752
|
},
|
|
@@ -3708,30 +3757,44 @@ function renderClientCard(c, clientCommands) {
|
|
|
3708
3757
|
{
|
|
3709
3758
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3710
3759
|
style:
|
|
3711
|
-
"padding:1px 5px;font-size:
|
|
3760
|
+
"padding:1px 5px;font-size:12px;background:transparent;color:" +
|
|
3761
|
+
titleColor +
|
|
3762
|
+
";border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3712
3763
|
title: "编辑客户端配置",
|
|
3713
3764
|
},
|
|
3714
3765
|
"⚙️",
|
|
3715
3766
|
) +
|
|
3767
|
+
(userId
|
|
3768
|
+
? tag(
|
|
3769
|
+
"button",
|
|
3770
|
+
{
|
|
3771
|
+
onclick: "wdClearLlm('" + esc(userId) + "')",
|
|
3772
|
+
style:
|
|
3773
|
+
"padding:1px 5px;font-size:11px;background:transparent;color:#888;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3774
|
+
title: "清除此客户端的 LLM 分析提示",
|
|
3775
|
+
},
|
|
3776
|
+
"🧹",
|
|
3777
|
+
)
|
|
3778
|
+
: "") +
|
|
3716
3779
|
styled(
|
|
3717
3780
|
"span",
|
|
3718
|
-
"color:#555;font-size:
|
|
3781
|
+
"color:#555;font-size:12px;margin-left:8px",
|
|
3719
3782
|
c.hostname || "",
|
|
3720
3783
|
),
|
|
3721
3784
|
) +
|
|
3722
3785
|
styled(
|
|
3723
3786
|
"div",
|
|
3724
3787
|
"display:flex;gap:10px;align-items:center",
|
|
3725
|
-
styled("span", "color:#888;font-size:
|
|
3788
|
+
styled("span", "color:#888;font-size:13px", c.ip) +
|
|
3726
3789
|
onlineBadge +
|
|
3727
|
-
styled("span", "color:#666;font-size:
|
|
3728
|
-
(!
|
|
3790
|
+
styled("span", "color:#666;font-size:12px", lastSeen) +
|
|
3791
|
+
(!online && userId
|
|
3729
3792
|
? tag(
|
|
3730
3793
|
"button",
|
|
3731
3794
|
{
|
|
3732
3795
|
onclick: "wdDeleteClient('" + esc(userId) + "')",
|
|
3733
3796
|
style:
|
|
3734
|
-
"padding:2px 8px;font-size:
|
|
3797
|
+
"padding:2px 8px;font-size:12px;background:#991b1b;color:#fff;border:none;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3735
3798
|
title: "删除此客户端配置",
|
|
3736
3799
|
},
|
|
3737
3800
|
"删除",
|
|
@@ -3742,11 +3805,13 @@ function renderClientCard(c, clientCommands) {
|
|
|
3742
3805
|
|
|
3743
3806
|
return styled(
|
|
3744
3807
|
"div",
|
|
3745
|
-
"background:#1e1e2e;border:
|
|
3808
|
+
"background:#1e1e2e;border:2px solid " +
|
|
3809
|
+
borderColor +
|
|
3810
|
+
";border-radius:12px;padding:16px",
|
|
3746
3811
|
header +
|
|
3747
|
-
styled("div", "color:#888;font-size:
|
|
3748
|
-
styled("div", "color:#888;font-size:
|
|
3749
|
-
renderCmdRows(myCmds, userId, procs,
|
|
3812
|
+
styled("div", "color:#888;font-size:12px", osStr) +
|
|
3813
|
+
styled("div", "color:#888;font-size:12px;margin-bottom:6px", infoLine) +
|
|
3814
|
+
renderCmdRows(myCmds, userId, procs, online, c.llmAnalysis),
|
|
3750
3815
|
);
|
|
3751
3816
|
}
|
|
3752
3817
|
|
|
@@ -3761,7 +3826,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3761
3826
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px",
|
|
3762
3827
|
styled(
|
|
3763
3828
|
"span",
|
|
3764
|
-
"font-size:
|
|
3829
|
+
"font-size:13px;color:#888",
|
|
3765
3830
|
"命令 (" + myCmds.length + "条)",
|
|
3766
3831
|
) +
|
|
3767
3832
|
tag(
|
|
@@ -3769,7 +3834,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3769
3834
|
{
|
|
3770
3835
|
onclick: "wdShowAddModal('" + esc(userId) + "')",
|
|
3771
3836
|
style:
|
|
3772
|
-
"padding:4px 10px;background:#4ade80;color:#000;border:none;border-radius:4px;cursor:pointer;font-size:
|
|
3837
|
+
"padding:4px 10px;background:#4ade80;color:#000;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:600",
|
|
3773
3838
|
},
|
|
3774
3839
|
"+ 添加",
|
|
3775
3840
|
),
|
|
@@ -3779,7 +3844,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3779
3844
|
// 表头
|
|
3780
3845
|
html += styled(
|
|
3781
3846
|
"div",
|
|
3782
|
-
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;color:#666;font-size:
|
|
3847
|
+
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;color:#666;font-size:11px;border-bottom:1px solid #222",
|
|
3783
3848
|
"<span>命令行</span><span>PID</span><span>内存</span><span>运行</span><span>重启</span><span>操作</span>",
|
|
3784
3849
|
);
|
|
3785
3850
|
|
|
@@ -3802,7 +3867,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3802
3867
|
var prefix = "wd-" + esc(userId) + "-" + idx;
|
|
3803
3868
|
html += styled(
|
|
3804
3869
|
"div",
|
|
3805
|
-
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:
|
|
3870
|
+
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:12px;border-top:1px solid #1a1a2a",
|
|
3806
3871
|
styled(
|
|
3807
3872
|
"span",
|
|
3808
3873
|
"color:" +
|
|
@@ -3818,7 +3883,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3818
3883
|
(c.name
|
|
3819
3884
|
? styled(
|
|
3820
3885
|
"span",
|
|
3821
|
-
"color:#555;font-size:
|
|
3886
|
+
"color:#555;font-size:10px;margin-left:4px;font-family:monospace",
|
|
3822
3887
|
c.cmd.substring(0, 30) + (c.cmd.length > 30 ? "..." : ""),
|
|
3823
3888
|
)
|
|
3824
3889
|
: ""),
|
|
@@ -3856,7 +3921,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3856
3921
|
onclick:
|
|
3857
3922
|
"wdToggleClientCmd('" + esc(userId) + "'," + idx + ")",
|
|
3858
3923
|
style:
|
|
3859
|
-
"padding:1px 5px;font-size:
|
|
3924
|
+
"padding:1px 5px;font-size:11px;background:" +
|
|
3860
3925
|
(c.enabled ? "#f59e0b" : "#4ade80") +
|
|
3861
3926
|
";color:#000;border:none;border-radius:2px;cursor:pointer",
|
|
3862
3927
|
title: c.enabled ? "停止" : "启动",
|
|
@@ -3869,7 +3934,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3869
3934
|
{
|
|
3870
3935
|
onclick: "wdShowEditModal('" + esc(userId) + "'," + idx + ")",
|
|
3871
3936
|
style:
|
|
3872
|
-
"padding:1px 5px;font-size:
|
|
3937
|
+
"padding:1px 5px;font-size:11px;background:#60a5fa;color:#fff;border:none;border-radius:2px;cursor:pointer",
|
|
3873
3938
|
title: "编辑",
|
|
3874
3939
|
},
|
|
3875
3940
|
"✎",
|
|
@@ -3879,7 +3944,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3879
3944
|
{
|
|
3880
3945
|
onclick: "wdRemoveClientCmd('" + esc(userId) + "'," + idx + ")",
|
|
3881
3946
|
style:
|
|
3882
|
-
"padding:1px 5px;font-size:
|
|
3947
|
+
"padding:1px 5px;font-size:11px;background:#991b1b;color:#fff;border:none;border-radius:2px;cursor:pointer",
|
|
3883
3948
|
title: "删除",
|
|
3884
3949
|
},
|
|
3885
3950
|
"✕",
|
|
@@ -3902,7 +3967,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3902
3967
|
: "";
|
|
3903
3968
|
html += styled(
|
|
3904
3969
|
"div",
|
|
3905
|
-
"font-size:
|
|
3970
|
+
"font-size:11px;color:#666;padding:1px 0 3px 8px;border-top:1px dashed #1a1a2a",
|
|
3906
3971
|
styled("span", "color:" + sevColor, "🤖 " + ai.summary) + tip,
|
|
3907
3972
|
);
|
|
3908
3973
|
}
|
|
@@ -3915,7 +3980,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3915
3980
|
var opre = "wd-" + esc(userId) + "-orphan-" + esc(String(oid));
|
|
3916
3981
|
html += styled(
|
|
3917
3982
|
"div",
|
|
3918
|
-
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:
|
|
3983
|
+
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:12px;border-top:1px solid #1a1a2a;opacity:0.5",
|
|
3919
3984
|
styled(
|
|
3920
3985
|
"span",
|
|
3921
3986
|
"color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace",
|
|
@@ -3941,7 +4006,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3941
4006
|
{ style: "color:#555", "data-wd-key": opre + "-rst" },
|
|
3942
4007
|
"0",
|
|
3943
4008
|
) +
|
|
3944
|
-
styled("span", "color:#555;font-size:
|
|
4009
|
+
styled("span", "color:#555;font-size:11px", "孤立"),
|
|
3945
4010
|
);
|
|
3946
4011
|
});
|
|
3947
4012
|
|
|
@@ -4173,6 +4238,32 @@ async function wdSaveClientCmds(userId, commands) {
|
|
|
4173
4238
|
}
|
|
4174
4239
|
}
|
|
4175
4240
|
|
|
4241
|
+
function wdClearLlm(userId) {
|
|
4242
|
+
if (
|
|
4243
|
+
!confirm(
|
|
4244
|
+
"清除 " +
|
|
4245
|
+
userId +
|
|
4246
|
+
" 的所有 LLM 分析提示?\n\n下次 agent 心跳或分析周期会自动重新生成。",
|
|
4247
|
+
)
|
|
4248
|
+
)
|
|
4249
|
+
return;
|
|
4250
|
+
wdShowSaving();
|
|
4251
|
+
fetch("/api/watchdog/clear-llm", {
|
|
4252
|
+
method: "POST",
|
|
4253
|
+
headers: { "Content-Type": "application/json" },
|
|
4254
|
+
body: JSON.stringify({ userId: userId }),
|
|
4255
|
+
})
|
|
4256
|
+
.then(function () {
|
|
4257
|
+
wdHideSaving();
|
|
4258
|
+
fetchWatchdogData();
|
|
4259
|
+
})
|
|
4260
|
+
.catch(function (e) {
|
|
4261
|
+
wdHideSaving();
|
|
4262
|
+
alert("清除失败: " + e.message);
|
|
4263
|
+
fetchWatchdogData();
|
|
4264
|
+
});
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4176
4267
|
function wdDeleteClient(userId) {
|
|
4177
4268
|
if (
|
|
4178
4269
|
!confirm(
|
|
@@ -9,6 +9,7 @@ body {
|
|
|
9
9
|
background: #0f0f13;
|
|
10
10
|
color: #e0e0e0;
|
|
11
11
|
padding: 16px;
|
|
12
|
+
font-size: 14px;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
.header {
|
|
@@ -65,13 +66,13 @@ body {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
.stat-card .label {
|
|
68
|
-
font-size:
|
|
69
|
+
font-size: 14px;
|
|
69
70
|
color: #888;
|
|
70
71
|
margin-bottom: 8px;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
.stat-card .value {
|
|
74
|
-
font-size:
|
|
75
|
+
font-size: 32px;
|
|
75
76
|
font-weight: 700;
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 独立模块。每条命令是完整命令行字符串(如 "tt-help explore --location DE"),
|
|
5
5
|
* 按 userId(~/.tt-help.json 的 userId 字段)分组存储。
|
|
6
|
+
*
|
|
7
|
+
* === 2026-08-07 变更 ===
|
|
8
|
+
* 1. 新增 llmCache 内存缓存,handleAnalyzeLogs 分析完成后写入缓存
|
|
9
|
+
* 2. getOnlineClients() 优先用缓存填充 llmAnalysis(不再依赖 agent 转发)
|
|
10
|
+
* 3. 新增 POST /api/watchdog/clear-llm 端点,手动清除指定客户端的 LLM 提示
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
@@ -17,6 +22,10 @@ const clients = new Map();
|
|
|
17
22
|
const CLIENT_TIMEOUT_MS = 60 * 1000;
|
|
18
23
|
const CLIENT_CLEANUP_MS = 24 * 60 * 60 * 1000; // 离线超过 24h 清理
|
|
19
24
|
|
|
25
|
+
// LLM 分析缓存:cmdId → { severity, summary, suggestions, cachedAt }
|
|
26
|
+
const llmCache = new Map();
|
|
27
|
+
const LLM_CACHE_TTL = 15 * 60 * 1000; // 15 分钟过期
|
|
28
|
+
|
|
20
29
|
// ── 配置文件读写 ──
|
|
21
30
|
|
|
22
31
|
function loadConfig() {
|
|
@@ -56,9 +65,24 @@ function getOnlineClients() {
|
|
|
56
65
|
console.error(`[watchdog] 清理离线客户端: ${clientId}`);
|
|
57
66
|
}
|
|
58
67
|
}
|
|
68
|
+
// 清理过期缓存
|
|
69
|
+
for (const [cmdId, entry] of llmCache) {
|
|
70
|
+
if (now - entry.cachedAt > LLM_CACHE_TTL) llmCache.delete(cmdId);
|
|
71
|
+
}
|
|
59
72
|
const result = [];
|
|
60
73
|
for (const [clientId, info] of clients) {
|
|
61
74
|
const online = now - info.lastHeartbeat < CLIENT_TIMEOUT_MS;
|
|
75
|
+
// 合并:agent 上报的 llmAnalysis + 服务端缓存(以服务端缓存为准,更新)
|
|
76
|
+
const processes = info.processes || [];
|
|
77
|
+
const cachedAnalysis = [];
|
|
78
|
+
for (const p of processes) {
|
|
79
|
+
if (p.cmdId && llmCache.has(p.cmdId)) {
|
|
80
|
+
cachedAnalysis.push(llmCache.get(p.cmdId));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// 优先用服务端缓存(更新),fallback 到 agent 上报的
|
|
84
|
+
const llmAnalysis =
|
|
85
|
+
cachedAnalysis.length > 0 ? cachedAnalysis : info.llmAnalysis || [];
|
|
62
86
|
result.push({
|
|
63
87
|
clientId,
|
|
64
88
|
hostname: info.hostname || "unknown",
|
|
@@ -66,7 +90,8 @@ function getOnlineClients() {
|
|
|
66
90
|
ttHelpVersion: info.ttHelpVersion || "unknown",
|
|
67
91
|
osInfo: info.osInfo || {},
|
|
68
92
|
config: info.config || {},
|
|
69
|
-
processes
|
|
93
|
+
processes,
|
|
94
|
+
llmAnalysis,
|
|
70
95
|
lastHeartbeat: info.lastHeartbeat,
|
|
71
96
|
registeredAt: info.registeredAt,
|
|
72
97
|
online,
|
|
@@ -286,6 +311,11 @@ async function handleAnalyzeLogs(params, body, res) {
|
|
|
286
311
|
console.error(
|
|
287
312
|
`[watchdog] LLM 分析完成: ${llmAnalysis.map((r) => r.severity).join(",")}`,
|
|
288
313
|
);
|
|
314
|
+
// 写入服务端缓存,前端直接读取,不再依赖 agent 转发
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
for (const item of llmAnalysis) {
|
|
317
|
+
llmCache.set(item.cmdId, { ...item, cachedAt: now });
|
|
318
|
+
}
|
|
289
319
|
sendJSON(res, 200, { llmAnalysis });
|
|
290
320
|
} catch (e) {
|
|
291
321
|
console.error(`[watchdog] LLM 分析失败: ${e.message}`);
|
|
@@ -318,6 +348,35 @@ async function handleDeleteClient(params, body, res) {
|
|
|
318
348
|
sendJSON(res, 200, { ok: true, removed });
|
|
319
349
|
}
|
|
320
350
|
|
|
351
|
+
async function handleClearLlmAnalysis(params, body, res) {
|
|
352
|
+
const userId = body.userId;
|
|
353
|
+
if (!userId) {
|
|
354
|
+
sendJSON(res, 400, { error: "缺少 userId" });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// 收集该 userId 下所有客户端进程的 cmdId,从缓存中清除
|
|
358
|
+
let cleared = 0;
|
|
359
|
+
for (const [clientId, info] of clients) {
|
|
360
|
+
const uid = (info.config && info.config.userId) || "";
|
|
361
|
+
if (uid !== userId) continue;
|
|
362
|
+
for (const p of info.processes || []) {
|
|
363
|
+
if (p.cmdId && llmCache.has(p.cmdId)) {
|
|
364
|
+
llmCache.delete(p.cmdId);
|
|
365
|
+
cleared++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// 同时清除 agent 上报的 llmAnalysis(如果存在)
|
|
370
|
+
for (const [clientId, info] of clients) {
|
|
371
|
+
const uid = (info.config && info.config.userId) || "";
|
|
372
|
+
if (uid === userId && info.llmAnalysis) {
|
|
373
|
+
info.llmAnalysis = [];
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
console.error(`[watchdog] ${userId} LLM 分析已清除(${cleared}条缓存)`);
|
|
377
|
+
sendJSON(res, 200, { ok: true, cleared });
|
|
378
|
+
}
|
|
379
|
+
|
|
321
380
|
// ── 公开 ──
|
|
322
381
|
|
|
323
382
|
export function registerWatchdogRoutes(httpServer) {
|
|
@@ -379,5 +438,9 @@ export async function tryHandleWatchdogRoute(
|
|
|
379
438
|
await handleDeleteClient(params, body, res);
|
|
380
439
|
return true;
|
|
381
440
|
}
|
|
441
|
+
if (method === "POST" && routePath === "/api/watchdog/clear-llm") {
|
|
442
|
+
await handleClearLlmAnalysis(params, body, res);
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
382
445
|
return false;
|
|
383
446
|
}
|
package/src/watchdog/agent.js
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
* 3. 处理信号优雅退出
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getSystemInfo,
|
|
12
|
+
getClientConfig,
|
|
13
|
+
getLocalIP,
|
|
14
|
+
getClashHealth,
|
|
15
|
+
} from "./reporter.js";
|
|
11
16
|
import { ProcessManager } from "./process-manager.js";
|
|
12
17
|
import {
|
|
13
18
|
getVersion,
|
|
@@ -16,12 +21,46 @@ import {
|
|
|
16
21
|
startUpgradeChecker,
|
|
17
22
|
} from "./upgrader.js";
|
|
18
23
|
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
24
|
+
import { spawn } from "child_process";
|
|
25
|
+
import { join, dirname } from "path";
|
|
26
|
+
import { homedir, tmpdir } from "os";
|
|
27
|
+
import { fileURLToPath } from "url";
|
|
21
28
|
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
30
|
const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
|
|
23
31
|
const TT_HELP_CONFIG = join(homeDir, ".tt-help.json");
|
|
24
32
|
|
|
33
|
+
/** 自动发现 daemon.js 路径 */
|
|
34
|
+
function findDaemonPath() {
|
|
35
|
+
// 1. 从 ~/.tt-help.json 读取配置
|
|
36
|
+
try {
|
|
37
|
+
if (existsSync(TT_HELP_CONFIG)) {
|
|
38
|
+
const cfg = JSON.parse(readFileSync(TT_HELP_CONFIG, "utf-8"));
|
|
39
|
+
if (cfg.clashDaemonPath && existsSync(cfg.clashDaemonPath)) {
|
|
40
|
+
return cfg.clashDaemonPath;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
|
|
45
|
+
// 2. 相对于 tiktok-cli 同级目录
|
|
46
|
+
const candidates = [
|
|
47
|
+
join(__dirname, "..", "..", "..", "clash-verge-explore", "daemon.js"),
|
|
48
|
+
join(__dirname, "..", "..", "..", "..", "clash-verge-explore", "daemon.js"),
|
|
49
|
+
join(process.cwd(), "..", "clash-verge-explore", "daemon.js"),
|
|
50
|
+
];
|
|
51
|
+
for (const p of candidates) {
|
|
52
|
+
if (existsSync(p)) return p;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// daemon 路径(启动时解析一次)
|
|
58
|
+
let _daemonPath = null;
|
|
59
|
+
function getDaemonPath() {
|
|
60
|
+
if (_daemonPath === null) _daemonPath = findDaemonPath();
|
|
61
|
+
return _daemonPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
/** 将服务端下发的配置合并写回 ~/.tt-help.json */
|
|
26
65
|
function applyClientConfig(newCfg) {
|
|
27
66
|
try {
|
|
@@ -92,6 +131,11 @@ export class WatchdogAgent {
|
|
|
92
131
|
this._heartbeatSeq = 0; // 心跳序号
|
|
93
132
|
this._lastProcCount = -1; // 上次进程数
|
|
94
133
|
this._lastCmdCount = -1; // 上次命令数
|
|
134
|
+
// clash daemon 集成
|
|
135
|
+
this.clashDaemon = null; // daemon 子进程引用
|
|
136
|
+
this.gracePeriodMs = 120_000; // 启动宽限期 2 分钟
|
|
137
|
+
this.startTime = 0; // 启动时间戳
|
|
138
|
+
this._graceEnded = false; // 宽限期是否已结束
|
|
95
139
|
}
|
|
96
140
|
|
|
97
141
|
/**
|
|
@@ -100,6 +144,7 @@ export class WatchdogAgent {
|
|
|
100
144
|
async start() {
|
|
101
145
|
if (this.running) return;
|
|
102
146
|
this.running = true;
|
|
147
|
+
this.startTime = Date.now();
|
|
103
148
|
|
|
104
149
|
console.error(
|
|
105
150
|
`[watchdog] 启动 — 客户端: ${this.clientId}, 版本: ${this.ttHelpVersion}, 间隔: ${this.intervalMs / 1000}s`,
|
|
@@ -111,6 +156,9 @@ export class WatchdogAgent {
|
|
|
111
156
|
// 注册信号处理
|
|
112
157
|
this._setupSignalHandlers();
|
|
113
158
|
|
|
159
|
+
// 启动 clash daemon(保代理通道)
|
|
160
|
+
this._spawnClashDaemon();
|
|
161
|
+
|
|
114
162
|
// 首次立即发送心跳
|
|
115
163
|
await this._sendHeartbeat();
|
|
116
164
|
|
|
@@ -152,6 +200,8 @@ export class WatchdogAgent {
|
|
|
152
200
|
this.upgradeChecker.stop();
|
|
153
201
|
this.upgradeChecker = null;
|
|
154
202
|
}
|
|
203
|
+
// 停止 clash daemon
|
|
204
|
+
this._killClashDaemon();
|
|
155
205
|
this.processManager.stopAll();
|
|
156
206
|
console.error("[watchdog] 看门狗已停止");
|
|
157
207
|
}
|
|
@@ -178,6 +228,7 @@ export class WatchdogAgent {
|
|
|
178
228
|
config: getClientConfig(),
|
|
179
229
|
processes: this.processManager.getProcessesStatus(),
|
|
180
230
|
llmAnalysis: this._llmResults,
|
|
231
|
+
clashHealth: getClashHealth(), // daemon 写入的代理健康状态
|
|
181
232
|
};
|
|
182
233
|
}
|
|
183
234
|
|
|
@@ -212,7 +263,7 @@ export class WatchdogAgent {
|
|
|
212
263
|
// 同步命令
|
|
213
264
|
if (Array.isArray(data.commands)) {
|
|
214
265
|
const prev = this.processManager.runningCount;
|
|
215
|
-
this.processManager.syncCommands(data.commands);
|
|
266
|
+
await this.processManager.syncCommands(data.commands);
|
|
216
267
|
const now = this.processManager.runningCount;
|
|
217
268
|
if (prev !== now) {
|
|
218
269
|
console.error(
|
|
@@ -244,6 +295,29 @@ export class WatchdogAgent {
|
|
|
244
295
|
console.error(
|
|
245
296
|
`[watchdog] 心跳失败 (${this.consecutiveFailures}/${this.maxFailures}): ${e.message}`,
|
|
246
297
|
);
|
|
298
|
+
|
|
299
|
+
// 启动宽限期:daemon 可能还在切节点,心跳失败不计入自杀计数
|
|
300
|
+
if (!this._graceEnded) {
|
|
301
|
+
const elapsed = Date.now() - this.startTime;
|
|
302
|
+
if (elapsed < this.gracePeriodMs) {
|
|
303
|
+
const remain = Math.round((this.gracePeriodMs - elapsed) / 1000);
|
|
304
|
+
// daemon 已报告健康 → 提前结束宽限期
|
|
305
|
+
const health = getClashHealth();
|
|
306
|
+
if (health && health.healthy) {
|
|
307
|
+
this._graceEnded = true;
|
|
308
|
+
console.error(`[watchdog] 代理已健康,宽限期提前结束`);
|
|
309
|
+
this.consecutiveFailures = 0;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
console.error(
|
|
313
|
+
`[watchdog] 宽限期内 (剩余${remain}s),等待 daemon 修复代理...`,
|
|
314
|
+
);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this._graceEnded = true;
|
|
318
|
+
console.error(`[watchdog] 宽限期结束,恢复正常心跳失败计数`);
|
|
319
|
+
}
|
|
320
|
+
|
|
247
321
|
if (this.consecutiveFailures >= this.maxFailures) {
|
|
248
322
|
console.error("[watchdog] 连续心跳失败次数过多,看门狗退出");
|
|
249
323
|
await this.stop();
|
|
@@ -315,6 +389,76 @@ export class WatchdogAgent {
|
|
|
315
389
|
});
|
|
316
390
|
}
|
|
317
391
|
|
|
392
|
+
/**
|
|
393
|
+
* 启动 clash daemon 子进程
|
|
394
|
+
* daemon 负责代理节点健康巡检和故障切换,watchdog 不阻塞等待
|
|
395
|
+
*/
|
|
396
|
+
_spawnClashDaemon() {
|
|
397
|
+
const daemonPath = getDaemonPath();
|
|
398
|
+
if (!daemonPath) {
|
|
399
|
+
console.error("[watchdog] 未找到 clash daemon (daemon.js),跳过代理守护");
|
|
400
|
+
console.error(
|
|
401
|
+
"[watchdog] 提示: 在 ~/.tt-help.json 中设置 clashDaemonPath",
|
|
402
|
+
);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
console.error(`[watchdog] 启动代理守护: ${daemonPath}`);
|
|
407
|
+
try {
|
|
408
|
+
const child = spawn(process.execPath, [daemonPath], {
|
|
409
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
410
|
+
detached: false,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
child.stdout?.on("data", (d) => {
|
|
414
|
+
// daemon 日志通过 stderr 输出,stdout 静默
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
child.stderr?.on("data", (d) => {
|
|
418
|
+
const msg = d.toString().trim();
|
|
419
|
+
if (msg) {
|
|
420
|
+
console.error(`[clash-daemon] ${msg.substring(0, 300)}`);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
child.on("exit", (code, signal) => {
|
|
425
|
+
console.error(
|
|
426
|
+
`[watchdog] clash daemon 退出 (code=${code}, signal=${signal})`,
|
|
427
|
+
);
|
|
428
|
+
this.clashDaemon = null;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
child.on("error", (err) => {
|
|
432
|
+
console.error(`[watchdog] clash daemon 启动失败: ${err.message}`);
|
|
433
|
+
this.clashDaemon = null;
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
this.clashDaemon = child;
|
|
437
|
+
} catch (e) {
|
|
438
|
+
console.error(`[watchdog] clash daemon 启动异常: ${e.message}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* 停止 clash daemon 子进程
|
|
444
|
+
*/
|
|
445
|
+
_killClashDaemon() {
|
|
446
|
+
if (!this.clashDaemon) return;
|
|
447
|
+
try {
|
|
448
|
+
this.clashDaemon.kill("SIGTERM");
|
|
449
|
+
setTimeout(() => {
|
|
450
|
+
try {
|
|
451
|
+
if (this.clashDaemon && this.clashDaemon.exitCode === null) {
|
|
452
|
+
this.clashDaemon.kill("SIGKILL");
|
|
453
|
+
}
|
|
454
|
+
} catch {}
|
|
455
|
+
}, 3000);
|
|
456
|
+
} catch (e) {
|
|
457
|
+
console.error(`[watchdog] 停止 clash daemon 失败: ${e.message}`);
|
|
458
|
+
}
|
|
459
|
+
this.clashDaemon = null;
|
|
460
|
+
}
|
|
461
|
+
|
|
318
462
|
/**
|
|
319
463
|
* 信号处理
|
|
320
464
|
*/
|
|
@@ -16,9 +16,12 @@ import { writeFileSync, unlinkSync } from "fs";
|
|
|
16
16
|
import { tmpdir } from "os";
|
|
17
17
|
import { join } from "path";
|
|
18
18
|
|
|
19
|
+
// === 2026-08-07 变更 ===
|
|
20
|
+
// syncCommands 改为 async,批量启动时每个进程间隔 SPAWN_STAGGER_MS(30s),避免抢网络
|
|
19
21
|
const MAX_RESTART = 3;
|
|
20
22
|
const MAX_LOG_LINES = 500; // 每个进程最多保留的日志行数
|
|
21
23
|
const IS_WIN = process.platform === "win32";
|
|
24
|
+
const SPAWN_STAGGER_MS = 30000; // 批量启动时每个进程间隔 30 秒,避免抢网络
|
|
22
25
|
|
|
23
26
|
// 从命令行中提取 CDP 端口列表(解析 --base-port 和 --port-count)
|
|
24
27
|
function extractCdpPorts(cmdLine) {
|
|
@@ -385,9 +388,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
385
388
|
* 与期望命令列表同步:
|
|
386
389
|
* - 启动期望列表中已启用但未运行的命令
|
|
387
390
|
* - 停止正在运行但不在期望列表中的命令
|
|
391
|
+
* - 批量启动时每个进程间隔 SPAWN_STAGGER_MS,避免同时抢网络
|
|
388
392
|
* @param {Array} expectedCmds - 服务端返回的命令列表,格式 [{cmd, maxUptimeSeconds, maxMemoryMB}]
|
|
389
393
|
*/
|
|
390
|
-
syncCommands(expectedCmds) {
|
|
394
|
+
async syncCommands(expectedCmds) {
|
|
391
395
|
if (!Array.isArray(expectedCmds)) return;
|
|
392
396
|
|
|
393
397
|
// 标准化:使用服务端分配的 id,无 id 时用命令行截断兜底
|
|
@@ -428,12 +432,26 @@ export class ProcessManager extends EventEmitter {
|
|
|
428
432
|
}
|
|
429
433
|
}
|
|
430
434
|
|
|
431
|
-
//
|
|
435
|
+
// 收集需要新启动的命令
|
|
436
|
+
const toStart = normalized.filter((cmd) => !runningIds.has(cmd.id));
|
|
437
|
+
|
|
438
|
+
if (toStart.length > 1) {
|
|
439
|
+
console.error(
|
|
440
|
+
`[watchdog] 批量启动 ${toStart.length} 个命令(间隔 ${SPAWN_STAGGER_MS / 1000}s)...`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 启动缺失的,错峰延迟
|
|
445
|
+
for (let i = 0; i < toStart.length; i++) {
|
|
446
|
+
if (i > 0) {
|
|
447
|
+
await new Promise((r) => setTimeout(r, SPAWN_STAGGER_MS));
|
|
448
|
+
}
|
|
449
|
+
this.startCommand(toStart[i]);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 更新已有进程的阈值(可能被管理员修改了)
|
|
432
453
|
for (const cmd of normalized) {
|
|
433
|
-
if (
|
|
434
|
-
this.startCommand(cmd);
|
|
435
|
-
} else {
|
|
436
|
-
// 更新阈值(可能被管理员修改了)
|
|
454
|
+
if (runningIds.has(cmd.id)) {
|
|
437
455
|
const info = this.processes.get(cmd.id);
|
|
438
456
|
if (info) info.thresholds = cmd.thresholds;
|
|
439
457
|
}
|
package/src/watchdog/reporter.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
import os from "os";
|
|
7
7
|
import { existsSync, readFileSync } from "fs";
|
|
8
8
|
import { join } from "path";
|
|
9
|
-
import { homedir } from "os";
|
|
9
|
+
import { homedir, tmpdir } from "os";
|
|
10
10
|
|
|
11
11
|
const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
|
|
12
12
|
const CONFIG_PATH = join(homeDir, ".tt-help.json");
|
|
13
|
+
const CLASH_HEALTH_FILE = join(tmpdir(), "clash-health.json");
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* 获取系统信息
|
|
@@ -67,3 +68,25 @@ export function getLocalIP() {
|
|
|
67
68
|
}
|
|
68
69
|
return "127.0.0.1";
|
|
69
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 读取 clash daemon 写入的健康状态文件
|
|
74
|
+
* @returns {object|null} { node, healthy, httpCode, timeTotal, lastCheck, lastSwitch } 或 null
|
|
75
|
+
*/
|
|
76
|
+
export function getClashHealth() {
|
|
77
|
+
try {
|
|
78
|
+
if (existsSync(CLASH_HEALTH_FILE)) {
|
|
79
|
+
const raw = readFileSync(CLASH_HEALTH_FILE, "utf-8");
|
|
80
|
+
const data = JSON.parse(raw);
|
|
81
|
+
// 检查数据是否过期(超过 2 分钟没更新视为 stale)
|
|
82
|
+
const age = Date.now() - new Date(data.lastCheck || 0).getTime();
|
|
83
|
+
if (age > 120_000) {
|
|
84
|
+
return { ...data, stale: true, ageSeconds: Math.round(age / 1000) };
|
|
85
|
+
}
|
|
86
|
+
return { ...data, stale: false };
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// 文件不存在或解析失败
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|