tt-help-cli-ycl 1.4.73 → 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 +1 -1
- package/src/watch/public/app.js +234 -49
- package/src/watch/public/index.html +3 -3
- package/src/watch/public/style.css +3 -2
- package/src/watch/watchdog-server.js +100 -3
- package/src/watchdog/agent.js +16 -4
- package/src/watchdog/llm-analyzer.js +10 -60
- package/src/watchdog/process-manager.js +22 -6
package/package.json
CHANGED
package/src/watch/public/app.js
CHANGED
|
@@ -3503,41 +3503,99 @@ 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
|
-
if (
|
|
3520
|
-
|
|
3538
|
+
if (
|
|
3539
|
+
!userClients[uid] &&
|
|
3540
|
+
clientCommands[uid] &&
|
|
3541
|
+
clientCommands[uid].length > 0
|
|
3542
|
+
) {
|
|
3543
|
+
offlineUids.push(uid);
|
|
3521
3544
|
}
|
|
3522
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
|
+
});
|
|
3523
3552
|
|
|
3524
3553
|
// 数据指纹:只有变化时才重建卡片 HTML
|
|
3525
3554
|
var fingerprint = JSON.stringify(allCards);
|
|
3526
3555
|
if (fingerprint === _wdLastRendered) return;
|
|
3527
3556
|
_wdLastRendered = fingerprint;
|
|
3528
3557
|
|
|
3558
|
+
// 注入动画样式(仅首次)
|
|
3559
|
+
if (!document.getElementById("wdAnimStyle")) {
|
|
3560
|
+
var style = document.createElement("style");
|
|
3561
|
+
style.id = "wdAnimStyle";
|
|
3562
|
+
style.textContent =
|
|
3563
|
+
"@keyframes wdFlash{0%{color:#4ade80}50%{color:#60a5fa}100%{color:inherit}}" +
|
|
3564
|
+
".wd-flash{animation:wdFlash 1.5s ease}";
|
|
3565
|
+
document.head.appendChild(style);
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
// 重建前扫描旧值(按 data-wd-key 索引)
|
|
3569
|
+
var prevVals = {};
|
|
3570
|
+
var oldCells = el.querySelectorAll("[data-wd-key]");
|
|
3571
|
+
for (var i = 0; i < oldCells.length; i++) {
|
|
3572
|
+
prevVals[oldCells[i].getAttribute("data-wd-key")] = oldCells[i].textContent;
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3529
3575
|
el.innerHTML = styled(
|
|
3530
3576
|
"div",
|
|
3531
3577
|
"display:grid;grid-template-columns:repeat(auto-fill,minmax(550px,1fr));gap:24px",
|
|
3532
3578
|
allCards.join(""),
|
|
3533
3579
|
);
|
|
3580
|
+
|
|
3581
|
+
// 只对变化的值加闪烁动画
|
|
3582
|
+
var newCells = el.querySelectorAll("[data-wd-key]");
|
|
3583
|
+
for (var i = 0; i < newCells.length; i++) {
|
|
3584
|
+
var key = newCells[i].getAttribute("data-wd-key");
|
|
3585
|
+
if (
|
|
3586
|
+
prevVals[key] !== undefined &&
|
|
3587
|
+
prevVals[key] !== newCells[i].textContent
|
|
3588
|
+
) {
|
|
3589
|
+
newCells[i].className = "wd-flash";
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3534
3592
|
}
|
|
3535
3593
|
|
|
3536
3594
|
function renderOfflineCard(userId, cmds) {
|
|
3537
3595
|
var cmdCount = cmds ? cmds.length : 0;
|
|
3538
3596
|
return styled(
|
|
3539
3597
|
"div",
|
|
3540
|
-
"background:#1e1e2e;border:
|
|
3598
|
+
"background:#1e1e2e;border:2px solid #f87171;border-radius:12px;padding:16px;opacity:0.9",
|
|
3541
3599
|
styled(
|
|
3542
3600
|
"div",
|
|
3543
3601
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
|
|
@@ -3548,7 +3606,7 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3548
3606
|
"span",
|
|
3549
3607
|
{
|
|
3550
3608
|
style:
|
|
3551
|
-
"color:#
|
|
3609
|
+
"color:#f87171;font-size:16px;font-weight:bold;cursor:pointer",
|
|
3552
3610
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3553
3611
|
title: "点击编辑客户端配置",
|
|
3554
3612
|
},
|
|
@@ -3559,28 +3617,38 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3559
3617
|
{
|
|
3560
3618
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3561
3619
|
style:
|
|
3562
|
-
"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",
|
|
3563
3621
|
title: "编辑客户端配置",
|
|
3564
3622
|
},
|
|
3565
3623
|
"⚙️",
|
|
3566
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
|
+
) +
|
|
3567
3635
|
styled(
|
|
3568
3636
|
"span",
|
|
3569
|
-
"color:#555;font-size:
|
|
3637
|
+
"color:#555;font-size:12px;margin-left:8px",
|
|
3570
3638
|
"(未上线)",
|
|
3571
3639
|
),
|
|
3572
3640
|
) +
|
|
3573
3641
|
styled(
|
|
3574
3642
|
"div",
|
|
3575
3643
|
"display:flex;gap:10px;align-items:center",
|
|
3576
|
-
styled("span", "color:#f87171", "● 离线") +
|
|
3577
|
-
styled("span", "color:#555;font-size:
|
|
3644
|
+
styled("span", "color:#f87171;font-size:13px", "● 离线") +
|
|
3645
|
+
styled("span", "color:#555;font-size:12px", "—") +
|
|
3578
3646
|
tag(
|
|
3579
3647
|
"button",
|
|
3580
3648
|
{
|
|
3581
3649
|
onclick: "wdDeleteClient('" + esc(userId) + "')",
|
|
3582
3650
|
style:
|
|
3583
|
-
"padding:2px 8px;font-size:
|
|
3651
|
+
"padding:2px 8px;font-size:12px;background:#991b1b;color:#fff;border:none;border-radius:3px;cursor:pointer",
|
|
3584
3652
|
title: "删除此客户端配置",
|
|
3585
3653
|
},
|
|
3586
3654
|
"删除",
|
|
@@ -3589,7 +3657,7 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3589
3657
|
) +
|
|
3590
3658
|
styled(
|
|
3591
3659
|
"div",
|
|
3592
|
-
"color:#555;font-size:
|
|
3660
|
+
"color:#555;font-size:12px",
|
|
3593
3661
|
"该机器尚未连接看门狗,配置了 " + cmdCount + " 条命令",
|
|
3594
3662
|
) +
|
|
3595
3663
|
renderCmdRows(cmds || [], userId, [], false),
|
|
@@ -3632,6 +3700,7 @@ function renderClientCard(c, clientCommands) {
|
|
|
3632
3700
|
procs = c.processes || [];
|
|
3633
3701
|
var userId = cfg.userId || "";
|
|
3634
3702
|
var myCmds = userId && clientCommands[userId] ? clientCommands[userId] : [];
|
|
3703
|
+
var online = c.online;
|
|
3635
3704
|
|
|
3636
3705
|
var osStr = [
|
|
3637
3706
|
os.platform,
|
|
@@ -3650,11 +3719,15 @@ function renderClientCard(c, clientCommands) {
|
|
|
3650
3719
|
(os.nodeVersion || "?") +
|
|
3651
3720
|
" | 运行 " +
|
|
3652
3721
|
(os.uptime != null ? Math.round(os.uptime / 3600) + "h" : "?");
|
|
3653
|
-
var onlineBadge =
|
|
3722
|
+
var onlineBadge = online
|
|
3654
3723
|
? styled("span", "color:#4ade80", "● 在线")
|
|
3655
3724
|
: styled("span", "color:#f87171", "● 离线");
|
|
3656
3725
|
var lastSeen = new Date(c.lastHeartbeat).toLocaleTimeString("zh-CN");
|
|
3657
3726
|
|
|
3727
|
+
// 离线时标题和边框变红
|
|
3728
|
+
var titleColor = online ? "#60a5fa" : "#f87171";
|
|
3729
|
+
var borderColor = online ? "#333" : "#f87171";
|
|
3730
|
+
|
|
3658
3731
|
var header = styled(
|
|
3659
3732
|
"div",
|
|
3660
3733
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
|
|
@@ -3664,7 +3737,10 @@ function renderClientCard(c, clientCommands) {
|
|
|
3664
3737
|
tag(
|
|
3665
3738
|
"span",
|
|
3666
3739
|
{
|
|
3667
|
-
style:
|
|
3740
|
+
style:
|
|
3741
|
+
"color:" +
|
|
3742
|
+
titleColor +
|
|
3743
|
+
";font-size:16px;font-weight:bold;cursor:pointer",
|
|
3668
3744
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3669
3745
|
title: "点击编辑客户端配置",
|
|
3670
3746
|
},
|
|
@@ -3675,30 +3751,44 @@ function renderClientCard(c, clientCommands) {
|
|
|
3675
3751
|
{
|
|
3676
3752
|
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3677
3753
|
style:
|
|
3678
|
-
"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",
|
|
3679
3757
|
title: "编辑客户端配置",
|
|
3680
3758
|
},
|
|
3681
3759
|
"⚙️",
|
|
3682
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
|
+
: "") +
|
|
3683
3773
|
styled(
|
|
3684
3774
|
"span",
|
|
3685
|
-
"color:#555;font-size:
|
|
3775
|
+
"color:#555;font-size:12px;margin-left:8px",
|
|
3686
3776
|
c.hostname || "",
|
|
3687
3777
|
),
|
|
3688
3778
|
) +
|
|
3689
3779
|
styled(
|
|
3690
3780
|
"div",
|
|
3691
3781
|
"display:flex;gap:10px;align-items:center",
|
|
3692
|
-
styled("span", "color:#888;font-size:
|
|
3782
|
+
styled("span", "color:#888;font-size:13px", c.ip) +
|
|
3693
3783
|
onlineBadge +
|
|
3694
|
-
styled("span", "color:#666;font-size:
|
|
3695
|
-
(!
|
|
3784
|
+
styled("span", "color:#666;font-size:12px", lastSeen) +
|
|
3785
|
+
(!online && userId
|
|
3696
3786
|
? tag(
|
|
3697
3787
|
"button",
|
|
3698
3788
|
{
|
|
3699
3789
|
onclick: "wdDeleteClient('" + esc(userId) + "')",
|
|
3700
3790
|
style:
|
|
3701
|
-
"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",
|
|
3702
3792
|
title: "删除此客户端配置",
|
|
3703
3793
|
},
|
|
3704
3794
|
"删除",
|
|
@@ -3709,11 +3799,13 @@ function renderClientCard(c, clientCommands) {
|
|
|
3709
3799
|
|
|
3710
3800
|
return styled(
|
|
3711
3801
|
"div",
|
|
3712
|
-
"background:#1e1e2e;border:
|
|
3802
|
+
"background:#1e1e2e;border:2px solid " +
|
|
3803
|
+
borderColor +
|
|
3804
|
+
";border-radius:12px;padding:16px",
|
|
3713
3805
|
header +
|
|
3714
|
-
styled("div", "color:#888;font-size:
|
|
3715
|
-
styled("div", "color:#888;font-size:
|
|
3716
|
-
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),
|
|
3717
3809
|
);
|
|
3718
3810
|
}
|
|
3719
3811
|
|
|
@@ -3728,7 +3820,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3728
3820
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px",
|
|
3729
3821
|
styled(
|
|
3730
3822
|
"span",
|
|
3731
|
-
"font-size:
|
|
3823
|
+
"font-size:13px;color:#888",
|
|
3732
3824
|
"命令 (" + myCmds.length + "条)",
|
|
3733
3825
|
) +
|
|
3734
3826
|
tag(
|
|
@@ -3736,7 +3828,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3736
3828
|
{
|
|
3737
3829
|
onclick: "wdShowAddModal('" + esc(userId) + "')",
|
|
3738
3830
|
style:
|
|
3739
|
-
"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",
|
|
3740
3832
|
},
|
|
3741
3833
|
"+ 添加",
|
|
3742
3834
|
),
|
|
@@ -3746,7 +3838,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3746
3838
|
// 表头
|
|
3747
3839
|
html += styled(
|
|
3748
3840
|
"div",
|
|
3749
|
-
"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",
|
|
3750
3842
|
"<span>命令行</span><span>PID</span><span>内存</span><span>运行</span><span>重启</span><span>操作</span>",
|
|
3751
3843
|
);
|
|
3752
3844
|
|
|
@@ -3766,9 +3858,10 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3766
3858
|
var uptime = p ? p.uptimeDisplay || p.uptimeSeconds + "s" : "-";
|
|
3767
3859
|
var restart = p && p.restartCount > 0 ? String(p.restartCount) : "0";
|
|
3768
3860
|
|
|
3861
|
+
var prefix = "wd-" + esc(userId) + "-" + idx;
|
|
3769
3862
|
html += styled(
|
|
3770
3863
|
"div",
|
|
3771
|
-
"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",
|
|
3772
3865
|
styled(
|
|
3773
3866
|
"span",
|
|
3774
3867
|
"color:" +
|
|
@@ -3784,17 +3877,32 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3784
3877
|
(c.name
|
|
3785
3878
|
? styled(
|
|
3786
3879
|
"span",
|
|
3787
|
-
"color:#555;font-size:
|
|
3880
|
+
"color:#555;font-size:10px;margin-left:4px;font-family:monospace",
|
|
3788
3881
|
c.cmd.substring(0, 30) + (c.cmd.length > 30 ? "..." : ""),
|
|
3789
3882
|
)
|
|
3790
3883
|
: ""),
|
|
3791
3884
|
) +
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3885
|
+
tag(
|
|
3886
|
+
"span",
|
|
3887
|
+
{ style: "color:#888", "data-wd-key": prefix + "-pid" },
|
|
3888
|
+
pid,
|
|
3889
|
+
) +
|
|
3890
|
+
tag(
|
|
3891
|
+
"span",
|
|
3892
|
+
{ style: "color:#aaa", "data-wd-key": prefix + "-mem" },
|
|
3893
|
+
mem,
|
|
3894
|
+
) +
|
|
3895
|
+
tag(
|
|
3796
3896
|
"span",
|
|
3797
|
-
"color
|
|
3897
|
+
{ style: "color:#888", "data-wd-key": prefix + "-up" },
|
|
3898
|
+
uptime,
|
|
3899
|
+
) +
|
|
3900
|
+
tag(
|
|
3901
|
+
"span",
|
|
3902
|
+
{
|
|
3903
|
+
style: "color:" + (p && p.restartCount > 0 ? "#f87171" : "#555"),
|
|
3904
|
+
"data-wd-key": prefix + "-rst",
|
|
3905
|
+
},
|
|
3798
3906
|
restart,
|
|
3799
3907
|
) +
|
|
3800
3908
|
styled(
|
|
@@ -3807,7 +3915,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3807
3915
|
onclick:
|
|
3808
3916
|
"wdToggleClientCmd('" + esc(userId) + "'," + idx + ")",
|
|
3809
3917
|
style:
|
|
3810
|
-
"padding:1px 5px;font-size:
|
|
3918
|
+
"padding:1px 5px;font-size:11px;background:" +
|
|
3811
3919
|
(c.enabled ? "#f59e0b" : "#4ade80") +
|
|
3812
3920
|
";color:#000;border:none;border-radius:2px;cursor:pointer",
|
|
3813
3921
|
title: c.enabled ? "停止" : "启动",
|
|
@@ -3820,7 +3928,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3820
3928
|
{
|
|
3821
3929
|
onclick: "wdShowEditModal('" + esc(userId) + "'," + idx + ")",
|
|
3822
3930
|
style:
|
|
3823
|
-
"padding:1px 5px;font-size:
|
|
3931
|
+
"padding:1px 5px;font-size:11px;background:#60a5fa;color:#fff;border:none;border-radius:2px;cursor:pointer",
|
|
3824
3932
|
title: "编辑",
|
|
3825
3933
|
},
|
|
3826
3934
|
"✎",
|
|
@@ -3830,7 +3938,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3830
3938
|
{
|
|
3831
3939
|
onclick: "wdRemoveClientCmd('" + esc(userId) + "'," + idx + ")",
|
|
3832
3940
|
style:
|
|
3833
|
-
"padding:1px 5px;font-size:
|
|
3941
|
+
"padding:1px 5px;font-size:11px;background:#991b1b;color:#fff;border:none;border-radius:2px;cursor:pointer",
|
|
3834
3942
|
title: "删除",
|
|
3835
3943
|
},
|
|
3836
3944
|
"✕",
|
|
@@ -3853,7 +3961,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3853
3961
|
: "";
|
|
3854
3962
|
html += styled(
|
|
3855
3963
|
"div",
|
|
3856
|
-
"font-size:
|
|
3964
|
+
"font-size:11px;color:#666;padding:1px 0 3px 8px;border-top:1px dashed #1a1a2a",
|
|
3857
3965
|
styled("span", "color:" + sevColor, "🤖 " + ai.summary) + tip,
|
|
3858
3966
|
);
|
|
3859
3967
|
}
|
|
@@ -3862,23 +3970,37 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3862
3970
|
// 未匹配到的运行中进程(孤儿)
|
|
3863
3971
|
match.unmatched.forEach(function (p) {
|
|
3864
3972
|
var sc = p.status === "running" ? "#4ade80" : "#f87171";
|
|
3973
|
+
var oid = p.pid || p.cmdId || "?";
|
|
3974
|
+
var opre = "wd-" + esc(userId) + "-orphan-" + esc(String(oid));
|
|
3865
3975
|
html += styled(
|
|
3866
3976
|
"div",
|
|
3867
|
-
"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",
|
|
3868
3978
|
styled(
|
|
3869
3979
|
"span",
|
|
3870
3980
|
"color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace",
|
|
3871
3981
|
"⚡ " + (p.name || p.cmdId),
|
|
3872
3982
|
) +
|
|
3873
|
-
|
|
3874
|
-
styled(
|
|
3983
|
+
tag(
|
|
3875
3984
|
"span",
|
|
3876
|
-
"color:#
|
|
3985
|
+
{ style: "color:#888", "data-wd-key": opre + "-pid" },
|
|
3986
|
+
p.pid || "-",
|
|
3987
|
+
) +
|
|
3988
|
+
tag(
|
|
3989
|
+
"span",
|
|
3990
|
+
{ style: "color:#aaa", "data-wd-key": opre + "-mem" },
|
|
3877
3991
|
p.memoryMB != null ? p.memoryMB + "MB" : "-",
|
|
3878
3992
|
) +
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3993
|
+
tag(
|
|
3994
|
+
"span",
|
|
3995
|
+
{ style: "color:#888", "data-wd-key": opre + "-up" },
|
|
3996
|
+
p.uptimeDisplay || p.uptimeSeconds + "s",
|
|
3997
|
+
) +
|
|
3998
|
+
tag(
|
|
3999
|
+
"span",
|
|
4000
|
+
{ style: "color:#555", "data-wd-key": opre + "-rst" },
|
|
4001
|
+
"0",
|
|
4002
|
+
) +
|
|
4003
|
+
styled("span", "color:#555;font-size:11px", "孤立"),
|
|
3882
4004
|
);
|
|
3883
4005
|
});
|
|
3884
4006
|
|
|
@@ -4072,29 +4194,92 @@ function wdToggleClientCmd(userId, idx) {
|
|
|
4072
4194
|
wdSaveClientCmds(userId, myCmds);
|
|
4073
4195
|
}
|
|
4074
4196
|
|
|
4197
|
+
var _wdSaving = false;
|
|
4198
|
+
|
|
4199
|
+
function wdShowSaving() {
|
|
4200
|
+
_wdSaving = true;
|
|
4201
|
+
var el = document.getElementById("wdSavingTip");
|
|
4202
|
+
if (!el) {
|
|
4203
|
+
el = document.createElement("div");
|
|
4204
|
+
el.id = "wdSavingTip";
|
|
4205
|
+
el.style.cssText =
|
|
4206
|
+
"position:fixed;top:60px;right:20px;background:#f59e0b;color:#000;padding:8px 16px;border-radius:6px;font-size:12px;font-weight:600;z-index:9998;pointer-events:none";
|
|
4207
|
+
document.body.appendChild(el);
|
|
4208
|
+
}
|
|
4209
|
+
el.textContent = "⏳ 处理中...";
|
|
4210
|
+
el.style.display = "block";
|
|
4211
|
+
}
|
|
4212
|
+
function wdHideSaving() {
|
|
4213
|
+
_wdSaving = false;
|
|
4214
|
+
var el = document.getElementById("wdSavingTip");
|
|
4215
|
+
if (el) el.style.display = "none";
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4075
4218
|
async function wdSaveClientCmds(userId, commands) {
|
|
4219
|
+
wdShowSaving();
|
|
4076
4220
|
try {
|
|
4077
4221
|
await fetch("/api/watchdog/client-commands", {
|
|
4078
4222
|
method: "POST",
|
|
4079
4223
|
headers: { "Content-Type": "application/json" },
|
|
4080
4224
|
body: JSON.stringify({ userId: userId, commands: commands }),
|
|
4081
4225
|
});
|
|
4226
|
+
wdHideSaving();
|
|
4082
4227
|
fetchWatchdogData();
|
|
4083
4228
|
} catch (e) {
|
|
4229
|
+
wdHideSaving();
|
|
4084
4230
|
alert("保存失败: " + e.message);
|
|
4085
|
-
// 失败时立即刷新恢复按钮状态
|
|
4086
4231
|
fetchWatchdogData();
|
|
4087
4232
|
}
|
|
4088
4233
|
}
|
|
4089
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
|
+
|
|
4090
4261
|
function wdDeleteClient(userId) {
|
|
4091
4262
|
if (
|
|
4092
4263
|
!confirm(
|
|
4093
4264
|
"确定删除 " +
|
|
4094
4265
|
userId +
|
|
4095
|
-
"
|
|
4266
|
+
" 及其所有命令配置?\n\n删除后该客户端将从列表中移除。",
|
|
4096
4267
|
)
|
|
4097
4268
|
)
|
|
4098
4269
|
return;
|
|
4099
|
-
|
|
4270
|
+
wdShowSaving();
|
|
4271
|
+
fetch("/api/watchdog/delete-client", {
|
|
4272
|
+
method: "POST",
|
|
4273
|
+
headers: { "Content-Type": "application/json" },
|
|
4274
|
+
body: JSON.stringify({ userId: userId }),
|
|
4275
|
+
})
|
|
4276
|
+
.then(function () {
|
|
4277
|
+
wdHideSaving();
|
|
4278
|
+
fetchWatchdogData();
|
|
4279
|
+
})
|
|
4280
|
+
.catch(function (e) {
|
|
4281
|
+
wdHideSaving();
|
|
4282
|
+
alert("删除失败: " + e.message);
|
|
4283
|
+
fetchWatchdogData();
|
|
4284
|
+
});
|
|
4100
4285
|
}
|
|
@@ -577,9 +577,9 @@
|
|
|
577
577
|
placeholder="默认">
|
|
578
578
|
</div>
|
|
579
579
|
<div>
|
|
580
|
-
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px"
|
|
581
|
-
<input id="wdCfgUserId"
|
|
582
|
-
style="width:100%;padding:6px;background:#
|
|
580
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">用户编号(不可修改)</label>
|
|
581
|
+
<input id="wdCfgUserId" readonly
|
|
582
|
+
style="width:100%;padding:6px;background:#1a1a2a;border:1px solid #333;color:#666;border-radius:4px;font-size:11px;box-sizing:border-box;cursor:not-allowed"
|
|
583
583
|
placeholder="自动生成">
|
|
584
584
|
</div>
|
|
585
585
|
<div>
|
|
@@ -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,
|
|
@@ -134,7 +154,17 @@ function setClientCommands(userId, commands) {
|
|
|
134
154
|
});
|
|
135
155
|
const cfg = loadConfig();
|
|
136
156
|
if (!cfg.clientCommands) cfg.clientCommands = {};
|
|
137
|
-
|
|
157
|
+
if (cleaned.length === 0) {
|
|
158
|
+
delete cfg.clientCommands[userId];
|
|
159
|
+
console.error(
|
|
160
|
+
`[watchdog ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}] ${userId} 命令已清空,卡片已删除`,
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
cfg.clientCommands[userId] = cleaned;
|
|
164
|
+
console.error(
|
|
165
|
+
`[watchdog ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}] ${userId} 命令已更新: ${cleaned.length} 条`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
138
168
|
saveConfig(cfg);
|
|
139
169
|
}
|
|
140
170
|
|
|
@@ -276,13 +306,72 @@ async function handleAnalyzeLogs(params, body, res) {
|
|
|
276
306
|
console.error(
|
|
277
307
|
`[watchdog] LLM 分析完成: ${llmAnalysis.map((r) => r.severity).join(",")}`,
|
|
278
308
|
);
|
|
309
|
+
// 写入服务端缓存,前端直接读取,不再依赖 agent 转发
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
for (const item of llmAnalysis) {
|
|
312
|
+
llmCache.set(item.cmdId, { ...item, cachedAt: now });
|
|
313
|
+
}
|
|
279
314
|
sendJSON(res, 200, { llmAnalysis });
|
|
280
315
|
} catch (e) {
|
|
281
316
|
console.error(`[watchdog] LLM 分析失败: ${e.message}`);
|
|
282
|
-
sendJSON(res,
|
|
317
|
+
sendJSON(res, 200, { llmAnalysis: [] });
|
|
283
318
|
}
|
|
284
319
|
}
|
|
285
320
|
|
|
321
|
+
async function handleDeleteClient(params, body, res) {
|
|
322
|
+
const userId = body.userId;
|
|
323
|
+
if (!userId) {
|
|
324
|
+
sendJSON(res, 400, { error: "缺少 userId" });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// 1. 删除命令配置
|
|
328
|
+
const cfg = loadConfig();
|
|
329
|
+
if (cfg.clientCommands) delete cfg.clientCommands[userId];
|
|
330
|
+
if (cfg.clientConfigs) delete cfg.clientConfigs[userId];
|
|
331
|
+
saveConfig(cfg);
|
|
332
|
+
// 2. 从内存客户端列表中移除所有匹配的客户端
|
|
333
|
+
let removed = 0;
|
|
334
|
+
for (const [clientId, info] of clients) {
|
|
335
|
+
if ((info.config && info.config.userId) === userId) {
|
|
336
|
+
clients.delete(clientId);
|
|
337
|
+
removed++;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
console.error(
|
|
341
|
+
`[watchdog] ${userId} 已删除(${removed}个客户端,命令+配置已清除)`,
|
|
342
|
+
);
|
|
343
|
+
sendJSON(res, 200, { ok: true, removed });
|
|
344
|
+
}
|
|
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
|
+
|
|
286
375
|
// ── 公开 ──
|
|
287
376
|
|
|
288
377
|
export function registerWatchdogRoutes(httpServer) {
|
|
@@ -340,5 +429,13 @@ export async function tryHandleWatchdogRoute(
|
|
|
340
429
|
await handleAnalyzeLogs(params, body, res);
|
|
341
430
|
return true;
|
|
342
431
|
}
|
|
432
|
+
if (method === "POST" && routePath === "/api/watchdog/delete-client") {
|
|
433
|
+
await handleDeleteClient(params, body, res);
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
if (method === "POST" && routePath === "/api/watchdog/clear-llm") {
|
|
437
|
+
await handleClearLlmAnalysis(params, body, res);
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
343
440
|
return false;
|
|
344
441
|
}
|
package/src/watchdog/agent.js
CHANGED
|
@@ -42,9 +42,21 @@ function applyClientConfig(newCfg) {
|
|
|
42
42
|
];
|
|
43
43
|
let changed = false;
|
|
44
44
|
for (const f of fields) {
|
|
45
|
-
if (newCfg[f] !== undefined
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
if (newCfg[f] !== undefined) {
|
|
46
|
+
var newVal = newCfg[f],
|
|
47
|
+
oldVal = cfg[f];
|
|
48
|
+
// 忽略无意义的零值/空值(避免每次心跳都写文件)
|
|
49
|
+
if (
|
|
50
|
+
(typeof newVal === "number" &&
|
|
51
|
+
newVal === 0 &&
|
|
52
|
+
oldVal === undefined) ||
|
|
53
|
+
(typeof newVal === "string" && newVal === "" && oldVal === undefined)
|
|
54
|
+
)
|
|
55
|
+
continue;
|
|
56
|
+
if (newVal !== oldVal) {
|
|
57
|
+
cfg[f] = newVal;
|
|
58
|
+
changed = true;
|
|
59
|
+
}
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
62
|
if (changed) {
|
|
@@ -200,7 +212,7 @@ export class WatchdogAgent {
|
|
|
200
212
|
// 同步命令
|
|
201
213
|
if (Array.isArray(data.commands)) {
|
|
202
214
|
const prev = this.processManager.runningCount;
|
|
203
|
-
this.processManager.syncCommands(data.commands);
|
|
215
|
+
await this.processManager.syncCommands(data.commands);
|
|
204
216
|
const now = this.processManager.runningCount;
|
|
205
217
|
if (prev !== now) {
|
|
206
218
|
console.error(
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LLM 日志分析模块 — 供服务端调用(传入 llmFetch),或客户端独立使用(读环境变量)
|
|
3
|
-
*
|
|
4
|
-
* 分析结果通过心跳上报到 watch 服务端展示。
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
// ── 分析入口 ──
|
|
8
6
|
|
|
9
|
-
/**
|
|
10
|
-
* 对所有进程的日志调用 LLM 分析
|
|
11
|
-
* @param {Array} processLogs — [{cmdId, logs[], uptimeSeconds, restartCount}]
|
|
12
|
-
* @param {Function} [llmFetch] — 可选,服务端传入的 llmFetch;不传则读环境变量
|
|
13
|
-
* @returns {Promise<Array<{cmdId, severity, summary, suggestions}>>}
|
|
14
|
-
*/
|
|
15
7
|
export async function analyzeProcessLogs(processLogs, llmFetch) {
|
|
16
8
|
if (!processLogs || processLogs.length === 0) return [];
|
|
17
9
|
|
|
18
|
-
// 如果没有传入 llmFetch,使用内置的轻量调用(客户端模式)
|
|
19
10
|
if (!llmFetch) {
|
|
20
11
|
llmFetch = await createStandaloneFetch();
|
|
21
12
|
if (!llmFetch) return [];
|
|
@@ -41,7 +32,7 @@ export async function analyzeProcessLogs(processLogs, llmFetch) {
|
|
|
41
32
|
return results;
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
// ──
|
|
35
|
+
// ── 客户端独立模式 ──
|
|
45
36
|
|
|
46
37
|
async function createStandaloneFetch() {
|
|
47
38
|
try {
|
|
@@ -59,8 +50,6 @@ async function createStandaloneFetch() {
|
|
|
59
50
|
const model = process.env.LLM_MODEL || fileCfg.llmModel || "auto";
|
|
60
51
|
if (!apiKey) { console.error("[llm-analyzer] 未配置 LLM_API_KEY"); return null; }
|
|
61
52
|
console.error(`[llm-analyzer] 客户端模式: ${model} @ ${url}`);
|
|
62
|
-
|
|
63
|
-
// 返回兼容 llmFetch 签名的函数
|
|
64
53
|
return async ({ messages, max_tokens = 256, temperature = 0.1 }) => {
|
|
65
54
|
const resp = await fetch(url, {
|
|
66
55
|
method: "POST",
|
|
@@ -79,34 +68,13 @@ async function createStandaloneFetch() {
|
|
|
79
68
|
return null;
|
|
80
69
|
}
|
|
81
70
|
}
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
256,
|
|
85
|
-
);
|
|
86
|
-
const parsed = parseResponse(content);
|
|
87
|
-
console.error(
|
|
88
|
-
`[llm-analyzer] ${proc.cmdId.substring(0, 30)} → ${parsed.severity}: ${parsed.summary}`,
|
|
89
|
-
);
|
|
90
|
-
results.push({ cmdId: proc.cmdId, ...parsed });
|
|
91
|
-
} catch (e) {
|
|
92
|
-
console.error(
|
|
93
|
-
`[llm-analyzer] ${proc.cmdId.substring(0, 30)} 失败: ${e.message}`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return results;
|
|
98
|
-
} catch (e) {
|
|
99
|
-
console.error(`[llm-analyzer] 异常: ${e.message}`);
|
|
100
|
-
return [];
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
71
|
|
|
104
|
-
|
|
105
|
-
const uptime =
|
|
106
|
-
uptimeSeconds >= 3600
|
|
107
|
-
? Math.round(uptimeSeconds / 3600) + "h"
|
|
108
|
-
: uptimeSeconds + "s";
|
|
72
|
+
// ── Prompt ──
|
|
109
73
|
|
|
74
|
+
function buildPrompt(cmdId, logs, uptimeSeconds, restartCount) {
|
|
75
|
+
const uptime = uptimeSeconds >= 3600
|
|
76
|
+
? Math.round(uptimeSeconds / 3600) + "h"
|
|
77
|
+
: uptimeSeconds + "s";
|
|
110
78
|
return `你是 TikTok 数据采集工具的运维助手。以下是进程 "${cmdId}" 的最近 10 分钟日志,请分析是否有异常。
|
|
111
79
|
|
|
112
80
|
进程信息:
|
|
@@ -127,38 +95,20 @@ ${logs || "(无日志)"}
|
|
|
127
95
|
- 每个 suggestions 是简洁可操作的建议,不超过 3 条`;
|
|
128
96
|
}
|
|
129
97
|
|
|
130
|
-
// ──
|
|
98
|
+
// ── 解析 ──
|
|
131
99
|
|
|
132
100
|
function parseResponse(content) {
|
|
133
101
|
try {
|
|
134
102
|
const parsed = JSON.parse(content);
|
|
135
|
-
return {
|
|
136
|
-
severity: parsed.severity || "ok",
|
|
137
|
-
summary: parsed.summary || "",
|
|
138
|
-
suggestions: Array.isArray(parsed.suggestions)
|
|
139
|
-
? parsed.suggestions.slice(0, 3)
|
|
140
|
-
: [],
|
|
141
|
-
};
|
|
103
|
+
return { severity: parsed.severity || "ok", summary: parsed.summary || "", suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions.slice(0, 3) : [] };
|
|
142
104
|
} catch {
|
|
143
|
-
// 尝试提取 JSON 块
|
|
144
105
|
const match = content.match(/\{[\s\S]*\}/);
|
|
145
106
|
if (match) {
|
|
146
107
|
try {
|
|
147
108
|
const parsed = JSON.parse(match[0]);
|
|
148
|
-
return {
|
|
149
|
-
severity: parsed.severity || "ok",
|
|
150
|
-
summary: parsed.summary || "",
|
|
151
|
-
suggestions: Array.isArray(parsed.suggestions)
|
|
152
|
-
? parsed.suggestions.slice(0, 3)
|
|
153
|
-
: [],
|
|
154
|
-
};
|
|
109
|
+
return { severity: parsed.severity || "ok", summary: parsed.summary || "", suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions.slice(0, 3) : [] };
|
|
155
110
|
} catch {}
|
|
156
111
|
}
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
severity: "warn",
|
|
160
|
-
summary: content.substring(0, 100),
|
|
161
|
-
suggestions: [],
|
|
162
|
-
};
|
|
112
|
+
return { severity: "warn", summary: content.substring(0, 100), suggestions: [] };
|
|
163
113
|
}
|
|
164
114
|
}
|
|
@@ -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
|
}
|