tt-help-cli-ycl 1.4.74 → 1.4.75
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
CHANGED
package/src/watch/public/app.js
CHANGED
|
@@ -3503,27 +3503,52 @@ function renderWatchdogDashboard(clients, clientCommands) {
|
|
|
3503
3503
|
return;
|
|
3504
3504
|
}
|
|
3505
3505
|
|
|
3506
|
+
// 排序:按名称升序,离线排最后
|
|
3507
|
+
var sortedClients = clients.slice().sort(function (a, b) {
|
|
3508
|
+
var nameA = (
|
|
3509
|
+
(a.config && a.config.userId) ||
|
|
3510
|
+
a.hostname ||
|
|
3511
|
+
""
|
|
3512
|
+
).toLowerCase();
|
|
3513
|
+
var nameB = (
|
|
3514
|
+
(b.config && b.config.userId) ||
|
|
3515
|
+
b.hostname ||
|
|
3516
|
+
""
|
|
3517
|
+
).toLowerCase();
|
|
3518
|
+
// 在线优先
|
|
3519
|
+
if (a.online !== b.online) return a.online ? -1 : 1;
|
|
3520
|
+
return nameA.localeCompare(nameB);
|
|
3521
|
+
});
|
|
3522
|
+
|
|
3506
3523
|
// 构建 userId → 在线客户端 映射
|
|
3507
3524
|
var userClients = {};
|
|
3508
|
-
|
|
3525
|
+
sortedClients.forEach(function (c) {
|
|
3509
3526
|
var uid = (c.config && c.config.userId) || "";
|
|
3510
3527
|
if (uid) userClients[uid] = c;
|
|
3511
3528
|
});
|
|
3512
3529
|
|
|
3513
3530
|
// 收集所有卡片:全部注册客户端 + 未上线但有命令配置的 userId
|
|
3514
3531
|
var allCards = [];
|
|
3515
|
-
|
|
3532
|
+
sortedClients.forEach(function (c) {
|
|
3516
3533
|
allCards.push(renderClientCard(c, clientCommands));
|
|
3517
3534
|
});
|
|
3535
|
+
// 离线卡也按名称排序
|
|
3536
|
+
var offlineUids = [];
|
|
3518
3537
|
for (var uid in clientCommands) {
|
|
3519
3538
|
if (
|
|
3520
3539
|
!userClients[uid] &&
|
|
3521
3540
|
clientCommands[uid] &&
|
|
3522
3541
|
clientCommands[uid].length > 0
|
|
3523
3542
|
) {
|
|
3524
|
-
|
|
3543
|
+
offlineUids.push(uid);
|
|
3525
3544
|
}
|
|
3526
3545
|
}
|
|
3546
|
+
offlineUids.sort(function (a, b) {
|
|
3547
|
+
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
3548
|
+
});
|
|
3549
|
+
offlineUids.forEach(function (uid) {
|
|
3550
|
+
allCards.push(renderOfflineCard(uid, clientCommands[uid]));
|
|
3551
|
+
});
|
|
3527
3552
|
|
|
3528
3553
|
// 数据指纹:只有变化时才重建卡片 HTML
|
|
3529
3554
|
var fingerprint = JSON.stringify(allCards);
|
|
@@ -3570,7 +3595,7 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3570
3595
|
var cmdCount = cmds ? cmds.length : 0;
|
|
3571
3596
|
return styled(
|
|
3572
3597
|
"div",
|
|
3573
|
-
"background:#1e1e2e;border:
|
|
3598
|
+
"background:#1e1e2e;border:2px solid #f87171;border-radius:12px;padding:16px;opacity:0.9",
|
|
3574
3599
|
styled(
|
|
3575
3600
|
"div",
|
|
3576
3601
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
|
|
@@ -3581,7 +3606,7 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3581
3606
|
"span",
|
|
3582
3607
|
{
|
|
3583
3608
|
style:
|
|
3584
|
-
"color:#
|
|
3609
|
+
"color:#f87171;font-size:16px;font-weight:bold;cursor:pointer",
|
|
3585
3610
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3586
3611
|
title: "点击编辑客户端配置",
|
|
3587
3612
|
},
|
|
@@ -3592,28 +3617,38 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3592
3617
|
{
|
|
3593
3618
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3594
3619
|
style:
|
|
3595
|
-
"padding:1px 5px;font-size:
|
|
3620
|
+
"padding:1px 5px;font-size:12px;background:transparent;color:#f87171;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3596
3621
|
title: "编辑客户端配置",
|
|
3597
3622
|
},
|
|
3598
3623
|
"⚙️",
|
|
3599
3624
|
) +
|
|
3625
|
+
tag(
|
|
3626
|
+
"button",
|
|
3627
|
+
{
|
|
3628
|
+
onclick: "wdClearLlm('" + esc(userId) + "')",
|
|
3629
|
+
style:
|
|
3630
|
+
"padding:1px 5px;font-size:11px;background:transparent;color:#888;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3631
|
+
title: "清除此客户端的 LLM 分析提示",
|
|
3632
|
+
},
|
|
3633
|
+
"🧹",
|
|
3634
|
+
) +
|
|
3600
3635
|
styled(
|
|
3601
3636
|
"span",
|
|
3602
|
-
"color:#555;font-size:
|
|
3637
|
+
"color:#555;font-size:12px;margin-left:8px",
|
|
3603
3638
|
"(未上线)",
|
|
3604
3639
|
),
|
|
3605
3640
|
) +
|
|
3606
3641
|
styled(
|
|
3607
3642
|
"div",
|
|
3608
3643
|
"display:flex;gap:10px;align-items:center",
|
|
3609
|
-
styled("span", "color:#f87171", "● 离线") +
|
|
3610
|
-
styled("span", "color:#555;font-size:
|
|
3644
|
+
styled("span", "color:#f87171;font-size:13px", "● 离线") +
|
|
3645
|
+
styled("span", "color:#555;font-size:12px", "—") +
|
|
3611
3646
|
tag(
|
|
3612
3647
|
"button",
|
|
3613
3648
|
{
|
|
3614
3649
|
onclick: "wdDeleteClient('" + esc(userId) + "')",
|
|
3615
3650
|
style:
|
|
3616
|
-
"padding:2px 8px;font-size:
|
|
3651
|
+
"padding:2px 8px;font-size:12px;background:#991b1b;color:#fff;border:none;border-radius:3px;cursor:pointer",
|
|
3617
3652
|
title: "删除此客户端配置",
|
|
3618
3653
|
},
|
|
3619
3654
|
"删除",
|
|
@@ -3622,7 +3657,7 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3622
3657
|
) +
|
|
3623
3658
|
styled(
|
|
3624
3659
|
"div",
|
|
3625
|
-
"color:#555;font-size:
|
|
3660
|
+
"color:#555;font-size:12px",
|
|
3626
3661
|
"该机器尚未连接看门狗,配置了 " + cmdCount + " 条命令",
|
|
3627
3662
|
) +
|
|
3628
3663
|
renderCmdRows(cmds || [], userId, [], false),
|
|
@@ -3665,6 +3700,7 @@ function renderClientCard(c, clientCommands) {
|
|
|
3665
3700
|
procs = c.processes || [];
|
|
3666
3701
|
var userId = cfg.userId || "";
|
|
3667
3702
|
var myCmds = userId && clientCommands[userId] ? clientCommands[userId] : [];
|
|
3703
|
+
var online = c.online;
|
|
3668
3704
|
|
|
3669
3705
|
var osStr = [
|
|
3670
3706
|
os.platform,
|
|
@@ -3683,11 +3719,15 @@ function renderClientCard(c, clientCommands) {
|
|
|
3683
3719
|
(os.nodeVersion || "?") +
|
|
3684
3720
|
" | 运行 " +
|
|
3685
3721
|
(os.uptime != null ? Math.round(os.uptime / 3600) + "h" : "?");
|
|
3686
|
-
var onlineBadge =
|
|
3722
|
+
var onlineBadge = online
|
|
3687
3723
|
? styled("span", "color:#4ade80", "● 在线")
|
|
3688
3724
|
: styled("span", "color:#f87171", "● 离线");
|
|
3689
3725
|
var lastSeen = new Date(c.lastHeartbeat).toLocaleTimeString("zh-CN");
|
|
3690
3726
|
|
|
3727
|
+
// 离线时标题和边框变红
|
|
3728
|
+
var titleColor = online ? "#60a5fa" : "#f87171";
|
|
3729
|
+
var borderColor = online ? "#333" : "#f87171";
|
|
3730
|
+
|
|
3691
3731
|
var header = styled(
|
|
3692
3732
|
"div",
|
|
3693
3733
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
|
|
@@ -3697,7 +3737,10 @@ function renderClientCard(c, clientCommands) {
|
|
|
3697
3737
|
tag(
|
|
3698
3738
|
"span",
|
|
3699
3739
|
{
|
|
3700
|
-
style:
|
|
3740
|
+
style:
|
|
3741
|
+
"color:" +
|
|
3742
|
+
titleColor +
|
|
3743
|
+
";font-size:16px;font-weight:bold;cursor:pointer",
|
|
3701
3744
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3702
3745
|
title: "点击编辑客户端配置",
|
|
3703
3746
|
},
|
|
@@ -3708,30 +3751,44 @@ function renderClientCard(c, clientCommands) {
|
|
|
3708
3751
|
{
|
|
3709
3752
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3710
3753
|
style:
|
|
3711
|
-
"padding:1px 5px;font-size:
|
|
3754
|
+
"padding:1px 5px;font-size:12px;background:transparent;color:" +
|
|
3755
|
+
titleColor +
|
|
3756
|
+
";border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3712
3757
|
title: "编辑客户端配置",
|
|
3713
3758
|
},
|
|
3714
3759
|
"⚙️",
|
|
3715
3760
|
) +
|
|
3761
|
+
(userId
|
|
3762
|
+
? tag(
|
|
3763
|
+
"button",
|
|
3764
|
+
{
|
|
3765
|
+
onclick: "wdClearLlm('" + esc(userId) + "')",
|
|
3766
|
+
style:
|
|
3767
|
+
"padding:1px 5px;font-size:11px;background:transparent;color:#888;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3768
|
+
title: "清除此客户端的 LLM 分析提示",
|
|
3769
|
+
},
|
|
3770
|
+
"🧹",
|
|
3771
|
+
)
|
|
3772
|
+
: "") +
|
|
3716
3773
|
styled(
|
|
3717
3774
|
"span",
|
|
3718
|
-
"color:#555;font-size:
|
|
3775
|
+
"color:#555;font-size:12px;margin-left:8px",
|
|
3719
3776
|
c.hostname || "",
|
|
3720
3777
|
),
|
|
3721
3778
|
) +
|
|
3722
3779
|
styled(
|
|
3723
3780
|
"div",
|
|
3724
3781
|
"display:flex;gap:10px;align-items:center",
|
|
3725
|
-
styled("span", "color:#888;font-size:
|
|
3782
|
+
styled("span", "color:#888;font-size:13px", c.ip) +
|
|
3726
3783
|
onlineBadge +
|
|
3727
|
-
styled("span", "color:#666;font-size:
|
|
3728
|
-
(!
|
|
3784
|
+
styled("span", "color:#666;font-size:12px", lastSeen) +
|
|
3785
|
+
(!online && userId
|
|
3729
3786
|
? tag(
|
|
3730
3787
|
"button",
|
|
3731
3788
|
{
|
|
3732
3789
|
onclick: "wdDeleteClient('" + esc(userId) + "')",
|
|
3733
3790
|
style:
|
|
3734
|
-
"padding:2px 8px;font-size:
|
|
3791
|
+
"padding:2px 8px;font-size:12px;background:#991b1b;color:#fff;border:none;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3735
3792
|
title: "删除此客户端配置",
|
|
3736
3793
|
},
|
|
3737
3794
|
"删除",
|
|
@@ -3742,11 +3799,13 @@ function renderClientCard(c, clientCommands) {
|
|
|
3742
3799
|
|
|
3743
3800
|
return styled(
|
|
3744
3801
|
"div",
|
|
3745
|
-
"background:#1e1e2e;border:
|
|
3802
|
+
"background:#1e1e2e;border:2px solid " +
|
|
3803
|
+
borderColor +
|
|
3804
|
+
";border-radius:12px;padding:16px",
|
|
3746
3805
|
header +
|
|
3747
|
-
styled("div", "color:#888;font-size:
|
|
3748
|
-
styled("div", "color:#888;font-size:
|
|
3749
|
-
renderCmdRows(myCmds, userId, procs,
|
|
3806
|
+
styled("div", "color:#888;font-size:12px", osStr) +
|
|
3807
|
+
styled("div", "color:#888;font-size:12px;margin-bottom:6px", infoLine) +
|
|
3808
|
+
renderCmdRows(myCmds, userId, procs, online, c.llmAnalysis),
|
|
3750
3809
|
);
|
|
3751
3810
|
}
|
|
3752
3811
|
|
|
@@ -3761,7 +3820,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3761
3820
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px",
|
|
3762
3821
|
styled(
|
|
3763
3822
|
"span",
|
|
3764
|
-
"font-size:
|
|
3823
|
+
"font-size:13px;color:#888",
|
|
3765
3824
|
"命令 (" + myCmds.length + "条)",
|
|
3766
3825
|
) +
|
|
3767
3826
|
tag(
|
|
@@ -3769,7 +3828,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3769
3828
|
{
|
|
3770
3829
|
onclick: "wdShowAddModal('" + esc(userId) + "')",
|
|
3771
3830
|
style:
|
|
3772
|
-
"padding:4px 10px;background:#4ade80;color:#000;border:none;border-radius:4px;cursor:pointer;font-size:
|
|
3831
|
+
"padding:4px 10px;background:#4ade80;color:#000;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:600",
|
|
3773
3832
|
},
|
|
3774
3833
|
"+ 添加",
|
|
3775
3834
|
),
|
|
@@ -3779,7 +3838,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3779
3838
|
// 表头
|
|
3780
3839
|
html += styled(
|
|
3781
3840
|
"div",
|
|
3782
|
-
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;color:#666;font-size:
|
|
3841
|
+
"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
3842
|
"<span>命令行</span><span>PID</span><span>内存</span><span>运行</span><span>重启</span><span>操作</span>",
|
|
3784
3843
|
);
|
|
3785
3844
|
|
|
@@ -3802,7 +3861,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3802
3861
|
var prefix = "wd-" + esc(userId) + "-" + idx;
|
|
3803
3862
|
html += styled(
|
|
3804
3863
|
"div",
|
|
3805
|
-
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:
|
|
3864
|
+
"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
3865
|
styled(
|
|
3807
3866
|
"span",
|
|
3808
3867
|
"color:" +
|
|
@@ -3818,7 +3877,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3818
3877
|
(c.name
|
|
3819
3878
|
? styled(
|
|
3820
3879
|
"span",
|
|
3821
|
-
"color:#555;font-size:
|
|
3880
|
+
"color:#555;font-size:10px;margin-left:4px;font-family:monospace",
|
|
3822
3881
|
c.cmd.substring(0, 30) + (c.cmd.length > 30 ? "..." : ""),
|
|
3823
3882
|
)
|
|
3824
3883
|
: ""),
|
|
@@ -3856,7 +3915,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3856
3915
|
onclick:
|
|
3857
3916
|
"wdToggleClientCmd('" + esc(userId) + "'," + idx + ")",
|
|
3858
3917
|
style:
|
|
3859
|
-
"padding:1px 5px;font-size:
|
|
3918
|
+
"padding:1px 5px;font-size:11px;background:" +
|
|
3860
3919
|
(c.enabled ? "#f59e0b" : "#4ade80") +
|
|
3861
3920
|
";color:#000;border:none;border-radius:2px;cursor:pointer",
|
|
3862
3921
|
title: c.enabled ? "停止" : "启动",
|
|
@@ -3869,7 +3928,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3869
3928
|
{
|
|
3870
3929
|
onclick: "wdShowEditModal('" + esc(userId) + "'," + idx + ")",
|
|
3871
3930
|
style:
|
|
3872
|
-
"padding:1px 5px;font-size:
|
|
3931
|
+
"padding:1px 5px;font-size:11px;background:#60a5fa;color:#fff;border:none;border-radius:2px;cursor:pointer",
|
|
3873
3932
|
title: "编辑",
|
|
3874
3933
|
},
|
|
3875
3934
|
"✎",
|
|
@@ -3879,7 +3938,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3879
3938
|
{
|
|
3880
3939
|
onclick: "wdRemoveClientCmd('" + esc(userId) + "'," + idx + ")",
|
|
3881
3940
|
style:
|
|
3882
|
-
"padding:1px 5px;font-size:
|
|
3941
|
+
"padding:1px 5px;font-size:11px;background:#991b1b;color:#fff;border:none;border-radius:2px;cursor:pointer",
|
|
3883
3942
|
title: "删除",
|
|
3884
3943
|
},
|
|
3885
3944
|
"✕",
|
|
@@ -3902,7 +3961,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3902
3961
|
: "";
|
|
3903
3962
|
html += styled(
|
|
3904
3963
|
"div",
|
|
3905
|
-
"font-size:
|
|
3964
|
+
"font-size:11px;color:#666;padding:1px 0 3px 8px;border-top:1px dashed #1a1a2a",
|
|
3906
3965
|
styled("span", "color:" + sevColor, "🤖 " + ai.summary) + tip,
|
|
3907
3966
|
);
|
|
3908
3967
|
}
|
|
@@ -3915,7 +3974,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3915
3974
|
var opre = "wd-" + esc(userId) + "-orphan-" + esc(String(oid));
|
|
3916
3975
|
html += styled(
|
|
3917
3976
|
"div",
|
|
3918
|
-
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:
|
|
3977
|
+
"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
3978
|
styled(
|
|
3920
3979
|
"span",
|
|
3921
3980
|
"color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace",
|
|
@@ -3941,7 +4000,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3941
4000
|
{ style: "color:#555", "data-wd-key": opre + "-rst" },
|
|
3942
4001
|
"0",
|
|
3943
4002
|
) +
|
|
3944
|
-
styled("span", "color:#555;font-size:
|
|
4003
|
+
styled("span", "color:#555;font-size:11px", "孤立"),
|
|
3945
4004
|
);
|
|
3946
4005
|
});
|
|
3947
4006
|
|
|
@@ -4173,6 +4232,32 @@ async function wdSaveClientCmds(userId, commands) {
|
|
|
4173
4232
|
}
|
|
4174
4233
|
}
|
|
4175
4234
|
|
|
4235
|
+
function wdClearLlm(userId) {
|
|
4236
|
+
if (
|
|
4237
|
+
!confirm(
|
|
4238
|
+
"清除 " +
|
|
4239
|
+
userId +
|
|
4240
|
+
" 的所有 LLM 分析提示?\n\n下次 agent 心跳或分析周期会自动重新生成。",
|
|
4241
|
+
)
|
|
4242
|
+
)
|
|
4243
|
+
return;
|
|
4244
|
+
wdShowSaving();
|
|
4245
|
+
fetch("/api/watchdog/clear-llm", {
|
|
4246
|
+
method: "POST",
|
|
4247
|
+
headers: { "Content-Type": "application/json" },
|
|
4248
|
+
body: JSON.stringify({ userId: userId }),
|
|
4249
|
+
})
|
|
4250
|
+
.then(function () {
|
|
4251
|
+
wdHideSaving();
|
|
4252
|
+
fetchWatchdogData();
|
|
4253
|
+
})
|
|
4254
|
+
.catch(function (e) {
|
|
4255
|
+
wdHideSaving();
|
|
4256
|
+
alert("清除失败: " + e.message);
|
|
4257
|
+
fetchWatchdogData();
|
|
4258
|
+
});
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4176
4261
|
function wdDeleteClient(userId) {
|
|
4177
4262
|
if (
|
|
4178
4263
|
!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
|
|
|
@@ -17,6 +17,10 @@ const clients = new Map();
|
|
|
17
17
|
const CLIENT_TIMEOUT_MS = 60 * 1000;
|
|
18
18
|
const CLIENT_CLEANUP_MS = 24 * 60 * 60 * 1000; // 离线超过 24h 清理
|
|
19
19
|
|
|
20
|
+
// LLM 分析缓存:cmdId → { severity, summary, suggestions, cachedAt }
|
|
21
|
+
const llmCache = new Map();
|
|
22
|
+
const LLM_CACHE_TTL = 15 * 60 * 1000; // 15 分钟过期
|
|
23
|
+
|
|
20
24
|
// ── 配置文件读写 ──
|
|
21
25
|
|
|
22
26
|
function loadConfig() {
|
|
@@ -56,9 +60,24 @@ function getOnlineClients() {
|
|
|
56
60
|
console.error(`[watchdog] 清理离线客户端: ${clientId}`);
|
|
57
61
|
}
|
|
58
62
|
}
|
|
63
|
+
// 清理过期缓存
|
|
64
|
+
for (const [cmdId, entry] of llmCache) {
|
|
65
|
+
if (now - entry.cachedAt > LLM_CACHE_TTL) llmCache.delete(cmdId);
|
|
66
|
+
}
|
|
59
67
|
const result = [];
|
|
60
68
|
for (const [clientId, info] of clients) {
|
|
61
69
|
const online = now - info.lastHeartbeat < CLIENT_TIMEOUT_MS;
|
|
70
|
+
// 合并:agent 上报的 llmAnalysis + 服务端缓存(以服务端缓存为准,更新)
|
|
71
|
+
const processes = info.processes || [];
|
|
72
|
+
const cachedAnalysis = [];
|
|
73
|
+
for (const p of processes) {
|
|
74
|
+
if (p.cmdId && llmCache.has(p.cmdId)) {
|
|
75
|
+
cachedAnalysis.push(llmCache.get(p.cmdId));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 优先用服务端缓存(更新),fallback 到 agent 上报的
|
|
79
|
+
const llmAnalysis =
|
|
80
|
+
cachedAnalysis.length > 0 ? cachedAnalysis : info.llmAnalysis || [];
|
|
62
81
|
result.push({
|
|
63
82
|
clientId,
|
|
64
83
|
hostname: info.hostname || "unknown",
|
|
@@ -66,7 +85,8 @@ function getOnlineClients() {
|
|
|
66
85
|
ttHelpVersion: info.ttHelpVersion || "unknown",
|
|
67
86
|
osInfo: info.osInfo || {},
|
|
68
87
|
config: info.config || {},
|
|
69
|
-
processes
|
|
88
|
+
processes,
|
|
89
|
+
llmAnalysis,
|
|
70
90
|
lastHeartbeat: info.lastHeartbeat,
|
|
71
91
|
registeredAt: info.registeredAt,
|
|
72
92
|
online,
|
|
@@ -286,6 +306,11 @@ async function handleAnalyzeLogs(params, body, res) {
|
|
|
286
306
|
console.error(
|
|
287
307
|
`[watchdog] LLM 分析完成: ${llmAnalysis.map((r) => r.severity).join(",")}`,
|
|
288
308
|
);
|
|
309
|
+
// 写入服务端缓存,前端直接读取,不再依赖 agent 转发
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
for (const item of llmAnalysis) {
|
|
312
|
+
llmCache.set(item.cmdId, { ...item, cachedAt: now });
|
|
313
|
+
}
|
|
289
314
|
sendJSON(res, 200, { llmAnalysis });
|
|
290
315
|
} catch (e) {
|
|
291
316
|
console.error(`[watchdog] LLM 分析失败: ${e.message}`);
|
|
@@ -318,6 +343,35 @@ async function handleDeleteClient(params, body, res) {
|
|
|
318
343
|
sendJSON(res, 200, { ok: true, removed });
|
|
319
344
|
}
|
|
320
345
|
|
|
346
|
+
async function handleClearLlmAnalysis(params, body, res) {
|
|
347
|
+
const userId = body.userId;
|
|
348
|
+
if (!userId) {
|
|
349
|
+
sendJSON(res, 400, { error: "缺少 userId" });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// 收集该 userId 下所有客户端进程的 cmdId,从缓存中清除
|
|
353
|
+
let cleared = 0;
|
|
354
|
+
for (const [clientId, info] of clients) {
|
|
355
|
+
const uid = (info.config && info.config.userId) || "";
|
|
356
|
+
if (uid !== userId) continue;
|
|
357
|
+
for (const p of info.processes || []) {
|
|
358
|
+
if (p.cmdId && llmCache.has(p.cmdId)) {
|
|
359
|
+
llmCache.delete(p.cmdId);
|
|
360
|
+
cleared++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// 同时清除 agent 上报的 llmAnalysis(如果存在)
|
|
365
|
+
for (const [clientId, info] of clients) {
|
|
366
|
+
const uid = (info.config && info.config.userId) || "";
|
|
367
|
+
if (uid === userId && info.llmAnalysis) {
|
|
368
|
+
info.llmAnalysis = [];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
console.error(`[watchdog] ${userId} LLM 分析已清除(${cleared}条缓存)`);
|
|
372
|
+
sendJSON(res, 200, { ok: true, cleared });
|
|
373
|
+
}
|
|
374
|
+
|
|
321
375
|
// ── 公开 ──
|
|
322
376
|
|
|
323
377
|
export function registerWatchdogRoutes(httpServer) {
|
|
@@ -379,5 +433,9 @@ export async function tryHandleWatchdogRoute(
|
|
|
379
433
|
await handleDeleteClient(params, body, res);
|
|
380
434
|
return true;
|
|
381
435
|
}
|
|
436
|
+
if (method === "POST" && routePath === "/api/watchdog/clear-llm") {
|
|
437
|
+
await handleClearLlmAnalysis(params, body, res);
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
382
440
|
return false;
|
|
383
441
|
}
|
package/src/watchdog/agent.js
CHANGED
|
@@ -212,7 +212,7 @@ export class WatchdogAgent {
|
|
|
212
212
|
// 同步命令
|
|
213
213
|
if (Array.isArray(data.commands)) {
|
|
214
214
|
const prev = this.processManager.runningCount;
|
|
215
|
-
this.processManager.syncCommands(data.commands);
|
|
215
|
+
await this.processManager.syncCommands(data.commands);
|
|
216
216
|
const now = this.processManager.runningCount;
|
|
217
217
|
if (prev !== now) {
|
|
218
218
|
console.error(
|
|
@@ -19,6 +19,7 @@ import { join } from "path";
|
|
|
19
19
|
const MAX_RESTART = 3;
|
|
20
20
|
const MAX_LOG_LINES = 500; // 每个进程最多保留的日志行数
|
|
21
21
|
const IS_WIN = process.platform === "win32";
|
|
22
|
+
const SPAWN_STAGGER_MS = 30000; // 批量启动时每个进程间隔 30 秒,避免抢网络
|
|
22
23
|
|
|
23
24
|
// 从命令行中提取 CDP 端口列表(解析 --base-port 和 --port-count)
|
|
24
25
|
function extractCdpPorts(cmdLine) {
|
|
@@ -385,9 +386,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
385
386
|
* 与期望命令列表同步:
|
|
386
387
|
* - 启动期望列表中已启用但未运行的命令
|
|
387
388
|
* - 停止正在运行但不在期望列表中的命令
|
|
389
|
+
* - 批量启动时每个进程间隔 SPAWN_STAGGER_MS,避免同时抢网络
|
|
388
390
|
* @param {Array} expectedCmds - 服务端返回的命令列表,格式 [{cmd, maxUptimeSeconds, maxMemoryMB}]
|
|
389
391
|
*/
|
|
390
|
-
syncCommands(expectedCmds) {
|
|
392
|
+
async syncCommands(expectedCmds) {
|
|
391
393
|
if (!Array.isArray(expectedCmds)) return;
|
|
392
394
|
|
|
393
395
|
// 标准化:使用服务端分配的 id,无 id 时用命令行截断兜底
|
|
@@ -428,12 +430,26 @@ export class ProcessManager extends EventEmitter {
|
|
|
428
430
|
}
|
|
429
431
|
}
|
|
430
432
|
|
|
431
|
-
//
|
|
433
|
+
// 收集需要新启动的命令
|
|
434
|
+
const toStart = normalized.filter((cmd) => !runningIds.has(cmd.id));
|
|
435
|
+
|
|
436
|
+
if (toStart.length > 1) {
|
|
437
|
+
console.error(
|
|
438
|
+
`[watchdog] 批量启动 ${toStart.length} 个命令(间隔 ${SPAWN_STAGGER_MS / 1000}s)...`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 启动缺失的,错峰延迟
|
|
443
|
+
for (let i = 0; i < toStart.length; i++) {
|
|
444
|
+
if (i > 0) {
|
|
445
|
+
await new Promise((r) => setTimeout(r, SPAWN_STAGGER_MS));
|
|
446
|
+
}
|
|
447
|
+
this.startCommand(toStart[i]);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 更新已有进程的阈值(可能被管理员修改了)
|
|
432
451
|
for (const cmd of normalized) {
|
|
433
|
-
if (
|
|
434
|
-
this.startCommand(cmd);
|
|
435
|
-
} else {
|
|
436
|
-
// 更新阈值(可能被管理员修改了)
|
|
452
|
+
if (runningIds.has(cmd.id)) {
|
|
437
453
|
const info = this.processes.get(cmd.id);
|
|
438
454
|
if (info) info.thresholds = cmd.thresholds;
|
|
439
455
|
}
|