tt-help-cli-ycl 1.4.70 → 1.4.72
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 +370 -204
- package/src/watch/public/index.html +99 -0
- package/src/watch/watchdog-server.js +58 -0
- package/src/watchdog/agent.js +92 -0
- package/src/watchdog/llm-analyzer.js +164 -0
- package/src/watchdog/process-manager.js +67 -17
package/package.json
CHANGED
package/src/watch/public/app.js
CHANGED
|
@@ -3445,28 +3445,17 @@ function styled(el, style, content) {
|
|
|
3445
3445
|
return tag(el, { style: style }, content);
|
|
3446
3446
|
}
|
|
3447
3447
|
|
|
3448
|
-
function fmtThreshold(cmd) {
|
|
3449
|
-
if (typeof cmd === "string") return "";
|
|
3450
|
-
var maxUp = cmd.maxUptimeSeconds || 0,
|
|
3451
|
-
maxMem = cmd.maxMemoryMB || 0;
|
|
3452
|
-
if (!maxUp && !maxMem) return "";
|
|
3453
|
-
var parts = [];
|
|
3454
|
-
if (maxUp > 0)
|
|
3455
|
-
parts.push(
|
|
3456
|
-
"最长 " + (maxUp >= 3600 ? Math.round(maxUp / 3600) + "h" : maxUp + "s"),
|
|
3457
|
-
);
|
|
3458
|
-
if (maxMem > 0) parts.push("最大 " + maxMem + "MB");
|
|
3459
|
-
return styled(
|
|
3460
|
-
"span",
|
|
3461
|
-
"color:#f59e0b;font-size:9px;margin-left:4px",
|
|
3462
|
-
"[" + parts.join("/") + "]",
|
|
3463
|
-
);
|
|
3464
|
-
}
|
|
3465
|
-
|
|
3466
3448
|
function cmdObj(cmd) {
|
|
3467
3449
|
if (typeof cmd === "string")
|
|
3468
|
-
return {
|
|
3450
|
+
return {
|
|
3451
|
+
name: "",
|
|
3452
|
+
cmd: cmd,
|
|
3453
|
+
enabled: true,
|
|
3454
|
+
maxUptimeSeconds: 0,
|
|
3455
|
+
maxMemoryMB: 0,
|
|
3456
|
+
};
|
|
3469
3457
|
return {
|
|
3458
|
+
name: cmd.name || "",
|
|
3470
3459
|
cmd: cmd.cmd || "",
|
|
3471
3460
|
enabled: cmd.enabled !== false,
|
|
3472
3461
|
maxUptimeSeconds: cmd.maxUptimeSeconds || 0,
|
|
@@ -3537,10 +3526,7 @@ function renderWatchdogDashboard(clients, clientCommands) {
|
|
|
3537
3526
|
|
|
3538
3527
|
el.innerHTML = styled(
|
|
3539
3528
|
"div",
|
|
3540
|
-
|
|
3541
|
-
style:
|
|
3542
|
-
"display:grid;grid-template-columns:repeat(auto-fill,minmax(440px,1fr));gap:16px",
|
|
3543
|
-
},
|
|
3529
|
+
"display:grid;grid-template-columns:repeat(auto-fill,minmax(550px,1fr));gap:24px",
|
|
3544
3530
|
allCards.join(""),
|
|
3545
3531
|
);
|
|
3546
3532
|
}
|
|
@@ -3549,14 +3535,33 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3549
3535
|
var cmdCount = cmds ? cmds.length : 0;
|
|
3550
3536
|
return styled(
|
|
3551
3537
|
"div",
|
|
3552
|
-
"background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:16px;opacity:0.
|
|
3538
|
+
"background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:16px;opacity:0.9",
|
|
3553
3539
|
styled(
|
|
3554
3540
|
"div",
|
|
3555
3541
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
|
|
3556
3542
|
styled(
|
|
3557
3543
|
"div",
|
|
3558
3544
|
"",
|
|
3559
|
-
|
|
3545
|
+
tag(
|
|
3546
|
+
"span",
|
|
3547
|
+
{
|
|
3548
|
+
style:
|
|
3549
|
+
"color:#60a5fa;font-size:14px;font-weight:bold;cursor:pointer",
|
|
3550
|
+
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3551
|
+
title: "点击编辑客户端配置",
|
|
3552
|
+
},
|
|
3553
|
+
userId,
|
|
3554
|
+
) +
|
|
3555
|
+
tag(
|
|
3556
|
+
"button",
|
|
3557
|
+
{
|
|
3558
|
+
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3559
|
+
style:
|
|
3560
|
+
"padding:1px 5px;font-size:10px;background:transparent;color:#60a5fa;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3561
|
+
title: "编辑客户端配置",
|
|
3562
|
+
},
|
|
3563
|
+
"⚙️",
|
|
3564
|
+
) +
|
|
3560
3565
|
styled(
|
|
3561
3566
|
"span",
|
|
3562
3567
|
"color:#555;font-size:11px;margin-left:8px",
|
|
@@ -3585,10 +3590,51 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3585
3590
|
"color:#555;font-size:10px",
|
|
3586
3591
|
"该机器尚未连接看门狗,配置了 " + cmdCount + " 条命令",
|
|
3587
3592
|
) +
|
|
3588
|
-
|
|
3593
|
+
renderCmdRows(cmds || [], userId, [], false),
|
|
3589
3594
|
);
|
|
3590
3595
|
}
|
|
3591
3596
|
|
|
3597
|
+
// 匹配进程到命令(按命令行前缀匹配)
|
|
3598
|
+
function matchProcsToCmds(myCmds, procs) {
|
|
3599
|
+
var matched = {}; // cmdIdx -> proc
|
|
3600
|
+
var unmatchedProcs = [];
|
|
3601
|
+
procs.forEach(function (p) {
|
|
3602
|
+
var found = false;
|
|
3603
|
+
for (var i = 0; i < myCmds.length; i++) {
|
|
3604
|
+
var c = cmdObj(myCmds[i]);
|
|
3605
|
+
var pName = p.name || p.cmdId || "";
|
|
3606
|
+
if (
|
|
3607
|
+
c.cmd &&
|
|
3608
|
+
pName &&
|
|
3609
|
+
(c.cmd.indexOf(pName.substring(0, 30)) === 0 ||
|
|
3610
|
+
pName.indexOf(c.cmd.substring(0, 30)) === 0)
|
|
3611
|
+
) {
|
|
3612
|
+
matched[i] = p;
|
|
3613
|
+
found = true;
|
|
3614
|
+
break;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
if (!found) unmatchedProcs.push(p);
|
|
3618
|
+
});
|
|
3619
|
+
return { matched: matched, unmatched: unmatchedProcs };
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
// 根据命令行匹配 LLM 分析结果
|
|
3623
|
+
function findAnalysis(llmAnalysis, cmdLine) {
|
|
3624
|
+
if (!llmAnalysis || !cmdLine) return null;
|
|
3625
|
+
for (var i = 0; i < llmAnalysis.length; i++) {
|
|
3626
|
+
var a = llmAnalysis[i];
|
|
3627
|
+
if (
|
|
3628
|
+
a.cmdId &&
|
|
3629
|
+
(a.cmdId.indexOf(cmdLine.substring(0, 30)) === 0 ||
|
|
3630
|
+
cmdLine.indexOf(a.cmdId.substring(0, 30)) === 0)
|
|
3631
|
+
) {
|
|
3632
|
+
return a;
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
return null;
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3592
3638
|
function renderClientCard(c, clientCommands) {
|
|
3593
3639
|
var os = c.osInfo || {},
|
|
3594
3640
|
cfg = c.config || {},
|
|
@@ -3605,33 +3651,49 @@ function renderClientCard(c, clientCommands) {
|
|
|
3605
3651
|
]
|
|
3606
3652
|
.filter(Boolean)
|
|
3607
3653
|
.join(" | ");
|
|
3608
|
-
|
|
3609
|
-
// 信息行
|
|
3610
3654
|
var infoLine =
|
|
3611
|
-
"用户编号: " +
|
|
3612
3655
|
styled("span", "color:#f59e0b", userId || "未设置") +
|
|
3613
|
-
" |
|
|
3614
|
-
(c.ttHelpVersion || "?") +
|
|
3656
|
+
" | " +
|
|
3657
|
+
styled("span", "color:#a78bfa", "tt-help v" + (c.ttHelpVersion || "?")) +
|
|
3615
3658
|
" | Node " +
|
|
3616
3659
|
(os.nodeVersion || "?") +
|
|
3617
3660
|
" | 运行 " +
|
|
3618
3661
|
(os.uptime != null ? Math.round(os.uptime / 3600) + "h" : "?");
|
|
3619
|
-
|
|
3620
|
-
// 在线状态
|
|
3621
3662
|
var onlineBadge = c.online
|
|
3622
3663
|
? styled("span", "color:#4ade80", "● 在线")
|
|
3623
3664
|
: styled("span", "color:#f87171", "● 离线");
|
|
3624
3665
|
var lastSeen = new Date(c.lastHeartbeat).toLocaleTimeString("zh-CN");
|
|
3625
3666
|
|
|
3626
|
-
// 头部
|
|
3627
3667
|
var header = styled(
|
|
3628
3668
|
"div",
|
|
3629
3669
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px",
|
|
3630
3670
|
styled(
|
|
3631
3671
|
"div",
|
|
3632
3672
|
"",
|
|
3633
|
-
|
|
3634
|
-
|
|
3673
|
+
tag(
|
|
3674
|
+
"span",
|
|
3675
|
+
{
|
|
3676
|
+
style: "color:#60a5fa;font-size:14px;font-weight:bold;cursor:pointer",
|
|
3677
|
+
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3678
|
+
title: "点击编辑客户端配置",
|
|
3679
|
+
},
|
|
3680
|
+
userId || c.hostname || "?",
|
|
3681
|
+
) +
|
|
3682
|
+
tag(
|
|
3683
|
+
"button",
|
|
3684
|
+
{
|
|
3685
|
+
onclick: "wdShowConfigModal('" + esc(userId) + "')",
|
|
3686
|
+
style:
|
|
3687
|
+
"padding:1px 5px;font-size:10px;background:transparent;color:#60a5fa;border:1px solid #333;border-radius:3px;cursor:pointer;margin-left:6px",
|
|
3688
|
+
title: "编辑客户端配置",
|
|
3689
|
+
},
|
|
3690
|
+
"⚙️",
|
|
3691
|
+
) +
|
|
3692
|
+
styled(
|
|
3693
|
+
"span",
|
|
3694
|
+
"color:#555;font-size:10px;margin-left:8px",
|
|
3695
|
+
c.hostname || "",
|
|
3696
|
+
),
|
|
3635
3697
|
) +
|
|
3636
3698
|
styled(
|
|
3637
3699
|
"div",
|
|
@@ -3654,232 +3716,336 @@ function renderClientCard(c, clientCommands) {
|
|
|
3654
3716
|
),
|
|
3655
3717
|
);
|
|
3656
3718
|
|
|
3657
|
-
// 进程表格
|
|
3658
|
-
var procTable = "";
|
|
3659
|
-
if (procs.length > 0) {
|
|
3660
|
-
var rows = procs
|
|
3661
|
-
.map(function (p) {
|
|
3662
|
-
var sc = p.status === "running" ? "#4ade80" : "#f87171";
|
|
3663
|
-
return tag(
|
|
3664
|
-
"tr",
|
|
3665
|
-
{},
|
|
3666
|
-
tag(
|
|
3667
|
-
"td",
|
|
3668
|
-
{
|
|
3669
|
-
style:
|
|
3670
|
-
"padding:3px;font-family:monospace;font-size:11px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap",
|
|
3671
|
-
title: p.name || p.cmdId,
|
|
3672
|
-
},
|
|
3673
|
-
p.name || p.cmdId,
|
|
3674
|
-
) +
|
|
3675
|
-
tag(
|
|
3676
|
-
"td",
|
|
3677
|
-
{ style: "padding:3px;color:#aaa;font-size:10px" },
|
|
3678
|
-
p.pid || "-",
|
|
3679
|
-
) +
|
|
3680
|
-
tag(
|
|
3681
|
-
"td",
|
|
3682
|
-
{ style: "padding:3px;font-size:11px" },
|
|
3683
|
-
p.memoryMB != null ? p.memoryMB + "MB" : "-",
|
|
3684
|
-
) +
|
|
3685
|
-
tag(
|
|
3686
|
-
"td",
|
|
3687
|
-
{ style: "padding:3px;color:#aaa;font-size:10px" },
|
|
3688
|
-
p.uptimeDisplay || p.uptimeSeconds + "s",
|
|
3689
|
-
) +
|
|
3690
|
-
tag(
|
|
3691
|
-
"td",
|
|
3692
|
-
{ style: "padding:3px;color:" + sc + ";font-size:10px" },
|
|
3693
|
-
p.status,
|
|
3694
|
-
) +
|
|
3695
|
-
tag(
|
|
3696
|
-
"td",
|
|
3697
|
-
{ style: "padding:3px;color:#f59e0b;font-size:10px" },
|
|
3698
|
-
p.restartCount > 0 ? String(p.restartCount) : "0",
|
|
3699
|
-
),
|
|
3700
|
-
);
|
|
3701
|
-
})
|
|
3702
|
-
.join("");
|
|
3703
|
-
procTable = styled(
|
|
3704
|
-
"div",
|
|
3705
|
-
"margin-top:10px",
|
|
3706
|
-
tag(
|
|
3707
|
-
"table",
|
|
3708
|
-
{ style: "width:100%;font-size:11px;border-collapse:collapse" },
|
|
3709
|
-
tag(
|
|
3710
|
-
"thead",
|
|
3711
|
-
{},
|
|
3712
|
-
tag(
|
|
3713
|
-
"tr",
|
|
3714
|
-
{ style: "color:#888;font-size:10px" },
|
|
3715
|
-
tag("th", { style: "text-align:left;padding:3px" }, "命令") +
|
|
3716
|
-
tag("th", { style: "text-align:left;padding:3px" }, "PID") +
|
|
3717
|
-
tag("th", { style: "text-align:left;padding:3px" }, "内存") +
|
|
3718
|
-
tag("th", { style: "text-align:left;padding:3px" }, "运行") +
|
|
3719
|
-
tag("th", { style: "text-align:left;padding:3px" }, "状态") +
|
|
3720
|
-
tag("th", { style: "text-align:left;padding:3px" }, "重启"),
|
|
3721
|
-
),
|
|
3722
|
-
) + tag("tbody", {}, rows),
|
|
3723
|
-
),
|
|
3724
|
-
);
|
|
3725
|
-
}
|
|
3726
|
-
|
|
3727
3719
|
return styled(
|
|
3728
3720
|
"div",
|
|
3729
3721
|
"background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:16px",
|
|
3730
3722
|
header +
|
|
3731
3723
|
styled("div", "color:#888;font-size:10px", osStr) +
|
|
3732
|
-
styled("div", "color:#888;font-size:10px", infoLine) +
|
|
3733
|
-
|
|
3734
|
-
(procs.length > 0
|
|
3735
|
-
? procTable
|
|
3736
|
-
: styled(
|
|
3737
|
-
"div",
|
|
3738
|
-
"color:#555;font-size:11px;margin-top:6px",
|
|
3739
|
-
"无运行进程",
|
|
3740
|
-
)),
|
|
3724
|
+
styled("div", "color:#888;font-size:10px;margin-bottom:6px", infoLine) +
|
|
3725
|
+
renderCmdRows(myCmds, userId, procs, c.online, c.llmAnalysis),
|
|
3741
3726
|
);
|
|
3742
3727
|
}
|
|
3743
3728
|
|
|
3744
|
-
|
|
3729
|
+
// 统一的命令行视图:每条命令一行,含运行状态
|
|
3730
|
+
function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
3731
|
+
var match = matchProcsToCmds(myCmds, procs);
|
|
3745
3732
|
var html = styled(
|
|
3746
3733
|
"div",
|
|
3747
|
-
"margin-top:
|
|
3734
|
+
"margin-top:6px;border-top:1px solid #2a2a3a;padding-top:8px",
|
|
3748
3735
|
styled(
|
|
3749
3736
|
"div",
|
|
3750
3737
|
"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px",
|
|
3751
3738
|
styled(
|
|
3752
3739
|
"span",
|
|
3753
3740
|
"font-size:11px;color:#888",
|
|
3754
|
-
"
|
|
3741
|
+
"命令 (" + myCmds.length + "条)",
|
|
3755
3742
|
) +
|
|
3756
3743
|
tag(
|
|
3757
3744
|
"button",
|
|
3758
3745
|
{
|
|
3759
|
-
onclick: "
|
|
3746
|
+
onclick: "wdShowAddModal('" + esc(userId) + "')",
|
|
3760
3747
|
style:
|
|
3761
3748
|
"padding:4px 10px;background:#4ade80;color:#000;border:none;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600",
|
|
3762
3749
|
},
|
|
3763
|
-
"+
|
|
3750
|
+
"+ 添加",
|
|
3764
3751
|
),
|
|
3765
3752
|
),
|
|
3766
3753
|
);
|
|
3767
3754
|
|
|
3755
|
+
// 表头
|
|
3756
|
+
html += styled(
|
|
3757
|
+
"div",
|
|
3758
|
+
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;color:#666;font-size:9px;border-bottom:1px solid #222",
|
|
3759
|
+
"<span>命令行</span><span>PID</span><span>内存</span><span>运行</span><span>重启</span><span>操作</span>",
|
|
3760
|
+
);
|
|
3761
|
+
|
|
3762
|
+
// 命令行
|
|
3768
3763
|
myCmds.forEach(function (cmd, idx) {
|
|
3769
3764
|
var c = cmdObj(cmd);
|
|
3770
|
-
var
|
|
3771
|
-
var
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3765
|
+
var p = match.matched[idx] || null;
|
|
3766
|
+
var isRunning = p && p.status === "running";
|
|
3767
|
+
var rowColor = c.enabled ? (isRunning ? "#4ade80" : "#f59e0b") : "#555";
|
|
3768
|
+
var statusIcon = c.enabled ? (isRunning ? "●" : "○") : "⏸";
|
|
3769
|
+
|
|
3770
|
+
var label =
|
|
3771
|
+
c.name || (c.cmd.length > 40 ? c.cmd.substring(0, 37) + "..." : c.cmd);
|
|
3772
|
+
var cmdTitle = c.name ? c.name + "\n" + c.cmd : c.cmd;
|
|
3773
|
+
var pid = p ? p.pid : "-";
|
|
3774
|
+
var mem = p && p.memoryMB != null ? p.memoryMB + "MB" : "-";
|
|
3775
|
+
var uptime = p ? p.uptimeDisplay || p.uptimeSeconds + "s" : "-";
|
|
3776
|
+
var restart = p && p.restartCount > 0 ? String(p.restartCount) : "0";
|
|
3779
3777
|
|
|
3780
3778
|
html += styled(
|
|
3781
3779
|
"div",
|
|
3782
|
-
"display:
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3780
|
+
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:10px;border-top:1px solid #1a1a2a",
|
|
3781
|
+
styled(
|
|
3782
|
+
"span",
|
|
3783
|
+
"color:" +
|
|
3784
|
+
rowColor +
|
|
3785
|
+
";overflow:hidden;text-overflow:ellipsis;white-space:nowrap",
|
|
3786
|
+
statusIcon +
|
|
3787
|
+
" " +
|
|
3788
|
+
styled(
|
|
3789
|
+
"span",
|
|
3790
|
+
"color:" + rowColor + ";font-weight:" + (c.name ? "500" : "normal"),
|
|
3791
|
+
label,
|
|
3792
|
+
) +
|
|
3793
|
+
(c.name
|
|
3794
|
+
? styled(
|
|
3795
|
+
"span",
|
|
3796
|
+
"color:#555;font-size:8px;margin-left:4px;font-family:monospace",
|
|
3797
|
+
c.cmd.substring(0, 30) + (c.cmd.length > 30 ? "..." : ""),
|
|
3798
|
+
)
|
|
3799
|
+
: ""),
|
|
3800
|
+
) +
|
|
3801
|
+
styled("span", "color:#888", pid) +
|
|
3802
|
+
styled("span", "color:#aaa", mem) +
|
|
3803
|
+
styled("span", "color:#888", uptime) +
|
|
3804
|
+
styled(
|
|
3805
|
+
"span",
|
|
3806
|
+
"color:" + (p && p.restartCount > 0 ? "#f87171" : "#555"),
|
|
3807
|
+
restart,
|
|
3810
3808
|
) +
|
|
3811
|
-
|
|
3812
|
-
"
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3809
|
+
styled(
|
|
3810
|
+
"span",
|
|
3811
|
+
"display:flex;gap:2px",
|
|
3812
|
+
(isOnline
|
|
3813
|
+
? tag(
|
|
3814
|
+
"button",
|
|
3815
|
+
{
|
|
3816
|
+
onclick:
|
|
3817
|
+
"wdToggleClientCmd('" + esc(userId) + "'," + idx + ")",
|
|
3818
|
+
style:
|
|
3819
|
+
"padding:1px 5px;font-size:9px;background:" +
|
|
3820
|
+
(c.enabled ? "#f59e0b" : "#4ade80") +
|
|
3821
|
+
";color:#000;border:none;border-radius:2px;cursor:pointer",
|
|
3822
|
+
title: c.enabled ? "停止" : "启动",
|
|
3823
|
+
},
|
|
3824
|
+
c.enabled ? "⏹" : "▶",
|
|
3825
|
+
)
|
|
3826
|
+
: "") +
|
|
3827
|
+
tag(
|
|
3828
|
+
"button",
|
|
3829
|
+
{
|
|
3830
|
+
onclick: "wdShowEditModal('" + esc(userId) + "'," + idx + ")",
|
|
3831
|
+
style:
|
|
3832
|
+
"padding:1px 5px;font-size:9px;background:#60a5fa;color:#fff;border:none;border-radius:2px;cursor:pointer",
|
|
3833
|
+
title: "编辑",
|
|
3834
|
+
},
|
|
3835
|
+
"✎",
|
|
3836
|
+
) +
|
|
3837
|
+
tag(
|
|
3838
|
+
"button",
|
|
3839
|
+
{
|
|
3840
|
+
onclick: "wdRemoveClientCmd('" + esc(userId) + "'," + idx + ")",
|
|
3841
|
+
style:
|
|
3842
|
+
"padding:1px 5px;font-size:9px;background:#991b1b;color:#fff;border:none;border-radius:2px;cursor:pointer",
|
|
3843
|
+
title: "删除",
|
|
3844
|
+
},
|
|
3845
|
+
"✕",
|
|
3846
|
+
),
|
|
3820
3847
|
),
|
|
3821
3848
|
);
|
|
3849
|
+
|
|
3850
|
+
// AI 分析结果
|
|
3851
|
+
var ai = findAnalysis(llmAnalysis, c.cmd);
|
|
3852
|
+
if (ai) {
|
|
3853
|
+
var sevColor =
|
|
3854
|
+
ai.severity === "error"
|
|
3855
|
+
? "#f87171"
|
|
3856
|
+
: ai.severity === "warn"
|
|
3857
|
+
? "#fbbf24"
|
|
3858
|
+
: "#4ade80";
|
|
3859
|
+
var tip =
|
|
3860
|
+
ai.suggestions && ai.suggestions.length > 0
|
|
3861
|
+
? " 💡" + ai.suggestions.join(" | ")
|
|
3862
|
+
: "";
|
|
3863
|
+
html += styled(
|
|
3864
|
+
"div",
|
|
3865
|
+
"font-size:9px;color:#666;padding:1px 0 3px 8px;border-top:1px dashed #1a1a2a",
|
|
3866
|
+
styled("span", "color:" + sevColor, "🤖 " + ai.summary) + tip,
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3822
3869
|
});
|
|
3823
3870
|
|
|
3871
|
+
// 未匹配到的运行中进程(孤儿)
|
|
3872
|
+
match.unmatched.forEach(function (p) {
|
|
3873
|
+
var sc = p.status === "running" ? "#4ade80" : "#f87171";
|
|
3874
|
+
html += styled(
|
|
3875
|
+
"div",
|
|
3876
|
+
"display:grid;grid-template-columns:1fr 50px 55px 55px 50px 80px;gap:4px;padding:4px 0;align-items:center;font-size:10px;border-top:1px solid #1a1a2a;opacity:0.5",
|
|
3877
|
+
styled(
|
|
3878
|
+
"span",
|
|
3879
|
+
"color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace",
|
|
3880
|
+
"⚡ " + (p.name || p.cmdId),
|
|
3881
|
+
) +
|
|
3882
|
+
styled("span", "color:#888", p.pid || "-") +
|
|
3883
|
+
styled(
|
|
3884
|
+
"span",
|
|
3885
|
+
"color:#aaa",
|
|
3886
|
+
p.memoryMB != null ? p.memoryMB + "MB" : "-",
|
|
3887
|
+
) +
|
|
3888
|
+
styled("span", "color:#888", p.uptimeDisplay || p.uptimeSeconds + "s") +
|
|
3889
|
+
styled("span", "color:#555", "0") +
|
|
3890
|
+
styled("span", "color:#555;font-size:9px", "孤立"),
|
|
3891
|
+
);
|
|
3892
|
+
});
|
|
3893
|
+
|
|
3894
|
+
if (myCmds.length === 0 && match.unmatched.length === 0) {
|
|
3895
|
+
html += styled(
|
|
3896
|
+
"div",
|
|
3897
|
+
"color:#555;font-size:10px;padding:8px 0",
|
|
3898
|
+
"暂无命令,点击「+ 添加」配置",
|
|
3899
|
+
);
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3824
3902
|
return html;
|
|
3825
3903
|
}
|
|
3826
3904
|
|
|
3827
|
-
// ──
|
|
3905
|
+
// ── 弹窗:添加/编辑命令 ──
|
|
3828
3906
|
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
});
|
|
3843
|
-
wdSaveClientCmds(userId, myCmds);
|
|
3907
|
+
var _wdModalUserId = "";
|
|
3908
|
+
var _wdModalEditIdx = -1;
|
|
3909
|
+
|
|
3910
|
+
function wdShowAddModal(userId) {
|
|
3911
|
+
_wdModalUserId = userId;
|
|
3912
|
+
_wdModalEditIdx = -1;
|
|
3913
|
+
document.getElementById("wdModalTitle").textContent = "添加命令";
|
|
3914
|
+
document.getElementById("wdModalName").value = "";
|
|
3915
|
+
document.getElementById("wdModalCmd").value = "";
|
|
3916
|
+
document.getElementById("wdModalMaxUp").value = "0";
|
|
3917
|
+
document.getElementById("wdModalMaxMem").value = "0";
|
|
3918
|
+
document.getElementById("wdCmdModal").style.display = "flex";
|
|
3919
|
+
document.getElementById("wdModalName").focus();
|
|
3844
3920
|
}
|
|
3845
3921
|
|
|
3846
|
-
function
|
|
3922
|
+
function wdShowEditModal(userId, idx) {
|
|
3847
3923
|
if (!_wdData) return;
|
|
3848
3924
|
var allCmds = _wdData.clientCommands || {};
|
|
3849
|
-
var myCmds =
|
|
3925
|
+
var myCmds = allCmds[userId] || [];
|
|
3850
3926
|
if (idx >= myCmds.length) return;
|
|
3851
3927
|
var c = cmdObj(myCmds[idx]);
|
|
3852
3928
|
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3929
|
+
_wdModalUserId = userId;
|
|
3930
|
+
_wdModalEditIdx = idx;
|
|
3931
|
+
document.getElementById("wdModalTitle").textContent = "编辑命令";
|
|
3932
|
+
document.getElementById("wdModalName").value = c.name || "";
|
|
3933
|
+
document.getElementById("wdModalCmd").value = c.cmd;
|
|
3934
|
+
document.getElementById("wdModalMaxUp").value = c.maxUptimeSeconds || 0;
|
|
3935
|
+
document.getElementById("wdModalMaxMem").value = c.maxMemoryMB || 0;
|
|
3936
|
+
document.getElementById("wdCmdModal").style.display = "flex";
|
|
3937
|
+
document.getElementById("wdModalName").focus();
|
|
3938
|
+
}
|
|
3861
3939
|
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3940
|
+
function wdModalCancel() {
|
|
3941
|
+
document.getElementById("wdCmdModal").style.display = "none";
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
function wdModalSave() {
|
|
3945
|
+
var name = document.getElementById("wdModalName").value.trim();
|
|
3946
|
+
var cmd = document.getElementById("wdModalCmd").value.trim();
|
|
3947
|
+
if (!cmd) {
|
|
3948
|
+
alert("请输入命令行");
|
|
3949
|
+
return;
|
|
3950
|
+
}
|
|
3951
|
+
var maxUp = parseInt(document.getElementById("wdModalMaxUp").value) || 0;
|
|
3952
|
+
var maxMem = parseInt(document.getElementById("wdModalMaxMem").value) || 0;
|
|
3953
|
+
|
|
3954
|
+
if (!_wdData) return;
|
|
3955
|
+
var allCmds = _wdData.clientCommands || {};
|
|
3956
|
+
var myCmds = (allCmds[_wdModalUserId] || []).slice();
|
|
3957
|
+
|
|
3958
|
+
if (_wdModalEditIdx >= 0) {
|
|
3959
|
+
// 编辑模式
|
|
3960
|
+
if (_wdModalEditIdx < myCmds.length) {
|
|
3961
|
+
myCmds[_wdModalEditIdx] = {
|
|
3962
|
+
name: name,
|
|
3963
|
+
cmd: cmd,
|
|
3964
|
+
enabled: cmdObj(myCmds[_wdModalEditIdx]).enabled,
|
|
3965
|
+
maxUptimeSeconds: maxUp,
|
|
3966
|
+
maxMemoryMB: maxMem,
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3869
3969
|
} else {
|
|
3870
|
-
|
|
3871
|
-
myCmds
|
|
3872
|
-
|
|
3970
|
+
// 添加模式
|
|
3971
|
+
myCmds.push({
|
|
3972
|
+
name: name,
|
|
3973
|
+
cmd: cmd,
|
|
3974
|
+
enabled: true,
|
|
3975
|
+
maxUptimeSeconds: maxUp,
|
|
3976
|
+
maxMemoryMB: maxMem,
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
wdSaveClientCmds(_wdModalUserId, myCmds);
|
|
3981
|
+
document.getElementById("wdCmdModal").style.display = "none";
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
// ── 配置弹窗 ──
|
|
3985
|
+
|
|
3986
|
+
var _wdCfgUserId = "";
|
|
3987
|
+
|
|
3988
|
+
function wdShowConfigModal(userId) {
|
|
3989
|
+
_wdCfgUserId = userId;
|
|
3990
|
+
document.getElementById("wdConfigUserId").textContent = userId;
|
|
3991
|
+
// 从客户端数据中预填已有配置
|
|
3992
|
+
var cfg = {};
|
|
3993
|
+
if (_wdData && _wdData.clients) {
|
|
3994
|
+
for (var i = 0; i < _wdData.clients.length; i++) {
|
|
3995
|
+
var c = _wdData.clients[i];
|
|
3996
|
+
if ((c.config && c.config.userId) === userId) {
|
|
3997
|
+
cfg = c.config || {};
|
|
3998
|
+
break;
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
document.getElementById("wdCfgServer").value = cfg.server || "";
|
|
4003
|
+
document.getElementById("wdCfgProxy").value = cfg.proxy || "";
|
|
4004
|
+
document.getElementById("wdCfgBrowser").value = cfg.browser || "";
|
|
4005
|
+
document.getElementById("wdCfgUserId").value = cfg.userId || userId;
|
|
4006
|
+
document.getElementById("wdCfgMaxVideos").value = cfg.maxVideos || "";
|
|
4007
|
+
document.getElementById("wdCfgMaxComments").value = cfg.maxComments || "";
|
|
4008
|
+
document.getElementById("wdCfgMaxFollowing").value = cfg.maxFollowing || "";
|
|
4009
|
+
document.getElementById("wdCfgMaxFollowers").value = cfg.maxFollowers || "";
|
|
4010
|
+
document.getElementById("wdConfigModal").style.display = "flex";
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
async function wdSaveConfig() {
|
|
4014
|
+
var cfg = {
|
|
4015
|
+
server: document.getElementById("wdCfgServer").value.trim(),
|
|
4016
|
+
proxy: document.getElementById("wdCfgProxy").value.trim(),
|
|
4017
|
+
browser: document.getElementById("wdCfgBrowser").value.trim(),
|
|
4018
|
+
userId: document.getElementById("wdCfgUserId").value.trim(),
|
|
4019
|
+
maxVideos: parseInt(document.getElementById("wdCfgMaxVideos").value) || 0,
|
|
4020
|
+
maxComments:
|
|
4021
|
+
parseInt(document.getElementById("wdCfgMaxComments").value) || 0,
|
|
4022
|
+
maxFollowing:
|
|
4023
|
+
parseInt(document.getElementById("wdCfgMaxFollowing").value) || 0,
|
|
4024
|
+
maxFollowers:
|
|
4025
|
+
parseInt(document.getElementById("wdCfgMaxFollowers").value) || 0,
|
|
4026
|
+
};
|
|
4027
|
+
try {
|
|
4028
|
+
await fetch("/api/watchdog/client-config", {
|
|
4029
|
+
method: "POST",
|
|
4030
|
+
headers: { "Content-Type": "application/json" },
|
|
4031
|
+
body: JSON.stringify({ userId: _wdCfgUserId, config: cfg }),
|
|
4032
|
+
});
|
|
4033
|
+
document.getElementById("wdConfigModal").style.display = "none";
|
|
4034
|
+
// 配置保存后无需立即刷新(下次心跳回来后看门狗自动应用)
|
|
4035
|
+
} catch (e) {
|
|
4036
|
+
alert("保存失败: " + e.message);
|
|
3873
4037
|
}
|
|
3874
|
-
wdSaveClientCmds(userId, myCmds);
|
|
3875
4038
|
}
|
|
3876
4039
|
|
|
4040
|
+
// ── 操作 ──
|
|
4041
|
+
|
|
3877
4042
|
function wdRemoveClientCmd(userId, idx) {
|
|
3878
4043
|
if (!_wdData) return;
|
|
3879
4044
|
var allCmds = _wdData.clientCommands || {};
|
|
3880
4045
|
var myCmds = (allCmds[userId] || []).slice();
|
|
3881
4046
|
if (idx >= myCmds.length) return;
|
|
3882
|
-
var
|
|
4047
|
+
var obj = cmdObj(myCmds[idx]);
|
|
4048
|
+
var txt = obj.name ? obj.name + "\n" + obj.cmd : obj.cmd;
|
|
3883
4049
|
if (!confirm("确定删除命令?\n\n" + txt)) return;
|
|
3884
4050
|
myCmds.splice(idx, 1);
|
|
3885
4051
|
wdSaveClientCmds(userId, myCmds);
|
|
@@ -513,6 +513,105 @@
|
|
|
513
513
|
</div>
|
|
514
514
|
<div id="wdClientsList" style="color:#666;font-size:12px">加载中...</div>
|
|
515
515
|
</div>
|
|
516
|
+
|
|
517
|
+
<!-- 命令编辑弹窗 -->
|
|
518
|
+
<div id="wdCmdModal"
|
|
519
|
+
style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:9999;align-items:center;justify-content:center">
|
|
520
|
+
<div style="background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:24px;width:90%;max-width:560px">
|
|
521
|
+
<div style="font-size:16px;color:#60a5fa;margin-bottom:16px" id="wdModalTitle">添加命令</div>
|
|
522
|
+
<div style="margin-bottom:12px">
|
|
523
|
+
<label style="display:block;color:#888;font-size:11px;margin-bottom:4px">名称</label>
|
|
524
|
+
<input id="wdModalName"
|
|
525
|
+
style="width:100%;padding:8px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:6px;font-size:12px;box-sizing:border-box"
|
|
526
|
+
placeholder="例如:explore-DE、refresh-ES、comments">
|
|
527
|
+
</div>
|
|
528
|
+
<div style="margin-bottom:12px">
|
|
529
|
+
<label style="display:block;color:#888;font-size:11px;margin-bottom:4px">命令行</label>
|
|
530
|
+
<textarea id="wdModalCmd" rows="3"
|
|
531
|
+
style="width:100%;padding:8px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:6px;font-family:monospace;font-size:12px;box-sizing:border-box;resize:vertical"
|
|
532
|
+
placeholder="tt-help explore --location DE --server http://..."></textarea>
|
|
533
|
+
</div>
|
|
534
|
+
<div style="display:flex;gap:12px;margin-bottom:12px">
|
|
535
|
+
<div style="flex:1">
|
|
536
|
+
<label style="display:block;color:#888;font-size:11px;margin-bottom:4px">最长运行(秒,0=不限)</label>
|
|
537
|
+
<input id="wdModalMaxUp" type="number" min="0" value="0"
|
|
538
|
+
style="width:100%;padding:8px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:6px;font-size:12px;box-sizing:border-box">
|
|
539
|
+
</div>
|
|
540
|
+
<div style="flex:1">
|
|
541
|
+
<label style="display:block;color:#888;font-size:11px;margin-bottom:4px">最大内存(MB,0=不限)</label>
|
|
542
|
+
<input id="wdModalMaxMem" type="number" min="0" value="0"
|
|
543
|
+
style="width:100%;padding:8px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:6px;font-size:12px;box-sizing:border-box">
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
|
|
547
|
+
<button onclick="wdModalCancel()"
|
|
548
|
+
style="padding:8px 16px;background:#333;color:#ccc;border:none;border-radius:6px;cursor:pointer;font-size:12px">取消</button>
|
|
549
|
+
<button onclick="wdModalSave()"
|
|
550
|
+
style="padding:8px 16px;background:#4ade80;color:#000;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">保存</button>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
<!-- 客户端配置编辑弹窗 -->
|
|
556
|
+
<div id="wdConfigModal"
|
|
557
|
+
style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:9999;align-items:center;justify-content:center">
|
|
558
|
+
<div style="background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:24px;width:90%;max-width:480px">
|
|
559
|
+
<div style="font-size:16px;color:#60a5fa;margin-bottom:16px">⚙️ 客户端配置 — <span id="wdConfigUserId"></span></div>
|
|
560
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
561
|
+
<div>
|
|
562
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">服务端地址</label>
|
|
563
|
+
<input id="wdCfgServer"
|
|
564
|
+
style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box"
|
|
565
|
+
placeholder="http://...">
|
|
566
|
+
</div>
|
|
567
|
+
<div>
|
|
568
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">代理</label>
|
|
569
|
+
<input id="wdCfgProxy"
|
|
570
|
+
style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box"
|
|
571
|
+
placeholder="http://127.0.0.1:7890">
|
|
572
|
+
</div>
|
|
573
|
+
<div>
|
|
574
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">浏览器路径</label>
|
|
575
|
+
<input id="wdCfgBrowser"
|
|
576
|
+
style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box"
|
|
577
|
+
placeholder="默认">
|
|
578
|
+
</div>
|
|
579
|
+
<div>
|
|
580
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">用户编号</label>
|
|
581
|
+
<input id="wdCfgUserId"
|
|
582
|
+
style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box"
|
|
583
|
+
placeholder="自动生成">
|
|
584
|
+
</div>
|
|
585
|
+
<div>
|
|
586
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">最大视频数</label>
|
|
587
|
+
<input id="wdCfgMaxVideos" type="number" min="0"
|
|
588
|
+
style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box">
|
|
589
|
+
</div>
|
|
590
|
+
<div>
|
|
591
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">最大评论数</label>
|
|
592
|
+
<input id="wdCfgMaxComments" type="number" min="0"
|
|
593
|
+
style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box">
|
|
594
|
+
</div>
|
|
595
|
+
<div>
|
|
596
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">最大关注数</label>
|
|
597
|
+
<input id="wdCfgMaxFollowing" type="number" min="0"
|
|
598
|
+
style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box">
|
|
599
|
+
</div>
|
|
600
|
+
<div>
|
|
601
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">最大粉丝数</label>
|
|
602
|
+
<input id="wdCfgMaxFollowers" type="number" min="0"
|
|
603
|
+
style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box">
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
|
|
607
|
+
<button onclick="document.getElementById('wdConfigModal').style.display='none'"
|
|
608
|
+
style="padding:8px 16px;background:#333;color:#ccc;border:none;border-radius:6px;cursor:pointer;font-size:12px">取消</button>
|
|
609
|
+
<button onclick="wdSaveConfig()"
|
|
610
|
+
style="padding:8px 16px;background:#4ade80;color:#000;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">保存并下发</button>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
|
|
516
615
|
<script src="app.js"></script>
|
|
517
616
|
</body>
|
|
518
617
|
|
|
@@ -130,6 +130,21 @@ function getAllClientCommandsRaw() {
|
|
|
130
130
|
return cfg.clientCommands || {};
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// ── 客户端配置管理(用于远程下发 ~/.tt-help.json 配置) ──
|
|
134
|
+
|
|
135
|
+
function getClientConfigStored(userId) {
|
|
136
|
+
if (!userId) return null;
|
|
137
|
+
const cfg = loadConfig();
|
|
138
|
+
return (cfg.clientConfigs && cfg.clientConfigs[userId]) || null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function setClientConfigStored(userId, config) {
|
|
142
|
+
const cfg = loadConfig();
|
|
143
|
+
if (!cfg.clientConfigs) cfg.clientConfigs = {};
|
|
144
|
+
cfg.clientConfigs[userId] = config;
|
|
145
|
+
saveConfig(cfg);
|
|
146
|
+
}
|
|
147
|
+
|
|
133
148
|
// ── 工具函数 ──
|
|
134
149
|
|
|
135
150
|
function readBody(req) {
|
|
@@ -181,6 +196,7 @@ async function handleHeartbeat(params, body, res) {
|
|
|
181
196
|
// 按 userId 返回该客户端的专属命令列表
|
|
182
197
|
const userId = (body.config && body.config.userId) || "";
|
|
183
198
|
const commands = getClientCommands(userId);
|
|
199
|
+
|
|
184
200
|
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
185
201
|
console.error(
|
|
186
202
|
`[watchdog ${ts}] 心跳: ${clientId} (${body.hostname || "?"}, user=${userId || "?"}) ${body.processes?.length || 0}进程, 返回${commands.length}条命令`,
|
|
@@ -189,6 +205,7 @@ async function handleHeartbeat(params, body, res) {
|
|
|
189
205
|
sendJSON(res, 200, {
|
|
190
206
|
ok: true,
|
|
191
207
|
commands,
|
|
208
|
+
clientConfig: getClientConfigStored(userId),
|
|
192
209
|
serverTime: Date.now(),
|
|
193
210
|
});
|
|
194
211
|
}
|
|
@@ -219,6 +236,39 @@ async function handleSetClientCommands(params, body, res) {
|
|
|
219
236
|
sendJSON(res, 200, { ok: true, count: commands.length });
|
|
220
237
|
}
|
|
221
238
|
|
|
239
|
+
async function handleClientConfig(params, body, res) {
|
|
240
|
+
const userId = body.userId;
|
|
241
|
+
const config = body.config;
|
|
242
|
+
if (!userId || !config) {
|
|
243
|
+
sendJSON(res, 400, { error: "缺少 userId 或 config" });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
setClientConfigStored(userId, config);
|
|
247
|
+
console.error(`[watchdog] ${userId} 配置已更新`);
|
|
248
|
+
sendJSON(res, 200, { ok: true });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function handleAnalyzeLogs(params, body, res) {
|
|
252
|
+
const { processLogs } = body;
|
|
253
|
+
if (!processLogs || !Array.isArray(processLogs) || processLogs.length === 0) {
|
|
254
|
+
sendJSON(res, 200, { llmAnalysis: [] });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const { analyzeProcessLogs } = await import("../watchdog/llm-analyzer.js");
|
|
259
|
+
const { llmFetch } = await import("./llm-config.js");
|
|
260
|
+
console.error(`[watchdog] LLM 分析 ${processLogs.length} 个进程日志...`);
|
|
261
|
+
const llmAnalysis = await analyzeProcessLogs(processLogs, llmFetch);
|
|
262
|
+
console.error(
|
|
263
|
+
`[watchdog] LLM 分析完成: ${llmAnalysis.map((r) => r.severity).join(",")}`,
|
|
264
|
+
);
|
|
265
|
+
sendJSON(res, 200, { llmAnalysis });
|
|
266
|
+
} catch (e) {
|
|
267
|
+
console.error(`[watchdog] LLM 分析失败: ${e.message}`);
|
|
268
|
+
sendJSON(res, 500, { error: e.message });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
222
272
|
// ── 公开 ──
|
|
223
273
|
|
|
224
274
|
export function registerWatchdogRoutes(httpServer) {
|
|
@@ -245,5 +295,13 @@ export async function tryHandleWatchdogRoute(
|
|
|
245
295
|
await handleSetClientCommands(params, body, res);
|
|
246
296
|
return true;
|
|
247
297
|
}
|
|
298
|
+
if (method === "POST" && routePath === "/api/watchdog/client-config") {
|
|
299
|
+
await handleClientConfig(params, body, res);
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
if (method === "POST" && routePath === "/api/watchdog/analyze-logs") {
|
|
303
|
+
await handleAnalyzeLogs(params, body, res);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
248
306
|
return false;
|
|
249
307
|
}
|
package/src/watchdog/agent.js
CHANGED
|
@@ -15,6 +15,46 @@ import {
|
|
|
15
15
|
isGlobalInstall,
|
|
16
16
|
startUpgradeChecker,
|
|
17
17
|
} from "./upgrader.js";
|
|
18
|
+
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
|
|
22
|
+
const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
|
|
23
|
+
const TT_HELP_CONFIG = join(homeDir, ".tt-help.json");
|
|
24
|
+
|
|
25
|
+
/** 将服务端下发的配置合并写回 ~/.tt-help.json */
|
|
26
|
+
function applyClientConfig(newCfg) {
|
|
27
|
+
try {
|
|
28
|
+
let cfg = {};
|
|
29
|
+
if (existsSync(TT_HELP_CONFIG)) {
|
|
30
|
+
cfg = JSON.parse(readFileSync(TT_HELP_CONFIG, "utf-8"));
|
|
31
|
+
}
|
|
32
|
+
// 合并下发字段(只更新存在的 key)
|
|
33
|
+
const fields = [
|
|
34
|
+
"server",
|
|
35
|
+
"proxy",
|
|
36
|
+
"browser",
|
|
37
|
+
"userId",
|
|
38
|
+
"maxVideos",
|
|
39
|
+
"maxComments",
|
|
40
|
+
"maxFollowing",
|
|
41
|
+
"maxFollowers",
|
|
42
|
+
];
|
|
43
|
+
let changed = false;
|
|
44
|
+
for (const f of fields) {
|
|
45
|
+
if (newCfg[f] !== undefined && newCfg[f] !== cfg[f]) {
|
|
46
|
+
cfg[f] = newCfg[f];
|
|
47
|
+
changed = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (changed) {
|
|
51
|
+
writeFileSync(TT_HELP_CONFIG, JSON.stringify(cfg, null, 2), "utf-8");
|
|
52
|
+
console.error("[watchdog] ~/.tt-help.json 已更新(来自服务端下发)");
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error(`[watchdog] 配置更新失败: ${e.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
18
58
|
|
|
19
59
|
export class WatchdogAgent {
|
|
20
60
|
/**
|
|
@@ -30,11 +70,13 @@ export class WatchdogAgent {
|
|
|
30
70
|
this.running = false;
|
|
31
71
|
this.heartbeatTimer = null;
|
|
32
72
|
this.thresholdTimer = null;
|
|
73
|
+
this.llmAnalyzerTimer = null;
|
|
33
74
|
this.processManager = new ProcessManager();
|
|
34
75
|
this.consecutiveFailures = 0;
|
|
35
76
|
this.maxFailures = 10;
|
|
36
77
|
this.upgradeChecker = null;
|
|
37
78
|
this.ttHelpVersion = getVersion();
|
|
79
|
+
this._llmResults = []; // 服务端返回的 LLM 分析结果
|
|
38
80
|
this._heartbeatSeq = 0; // 心跳序号
|
|
39
81
|
this._lastProcCount = -1; // 上次进程数
|
|
40
82
|
this._lastCmdCount = -1; // 上次命令数
|
|
@@ -70,6 +112,9 @@ export class WatchdogAgent {
|
|
|
70
112
|
this.processManager.checkThresholds();
|
|
71
113
|
}, 5000);
|
|
72
114
|
|
|
115
|
+
// LLM 日志分析(每 10 分钟调服务端接口)
|
|
116
|
+
this._startLlmAnalyzer();
|
|
117
|
+
|
|
73
118
|
// 自动升级检测(仅全局安装模式)
|
|
74
119
|
this._startUpgradeChecker();
|
|
75
120
|
}
|
|
@@ -87,6 +132,10 @@ export class WatchdogAgent {
|
|
|
87
132
|
clearInterval(this.thresholdTimer);
|
|
88
133
|
this.thresholdTimer = null;
|
|
89
134
|
}
|
|
135
|
+
if (this.llmAnalyzerTimer) {
|
|
136
|
+
clearInterval(this.llmAnalyzerTimer);
|
|
137
|
+
this.llmAnalyzerTimer = null;
|
|
138
|
+
}
|
|
90
139
|
if (this.upgradeChecker) {
|
|
91
140
|
this.upgradeChecker.stop();
|
|
92
141
|
this.upgradeChecker = null;
|
|
@@ -116,6 +165,7 @@ export class WatchdogAgent {
|
|
|
116
165
|
},
|
|
117
166
|
config: getClientConfig(),
|
|
118
167
|
processes: this.processManager.getProcessesStatus(),
|
|
168
|
+
llmAnalysis: this._llmResults,
|
|
119
169
|
};
|
|
120
170
|
}
|
|
121
171
|
|
|
@@ -159,6 +209,11 @@ export class WatchdogAgent {
|
|
|
159
209
|
}
|
|
160
210
|
}
|
|
161
211
|
|
|
212
|
+
// 服务端下发的客户端配置(远程修改 ~/.tt-help.json)
|
|
213
|
+
if (data.clientConfig) {
|
|
214
|
+
applyClientConfig(data.clientConfig);
|
|
215
|
+
}
|
|
216
|
+
|
|
162
217
|
// 心跳日志:仅状态变化或每 10 次打印一次
|
|
163
218
|
this._heartbeatSeq++;
|
|
164
219
|
const cmdCount = data.commands?.length || 0;
|
|
@@ -185,6 +240,43 @@ export class WatchdogAgent {
|
|
|
185
240
|
}
|
|
186
241
|
}
|
|
187
242
|
|
|
243
|
+
/**
|
|
244
|
+
* 启动 LLM 日志分析定时器(每 10 分钟调服务端接口)
|
|
245
|
+
*/
|
|
246
|
+
_startLlmAnalyzer() {
|
|
247
|
+
const run = async () => {
|
|
248
|
+
try {
|
|
249
|
+
const logs = this.processManager.getProcessesLogs();
|
|
250
|
+
if (logs.length === 0) return;
|
|
251
|
+
console.error(
|
|
252
|
+
`[llm-analyzer] 发送 ${logs.length} 个进程日志到服务端分析...`,
|
|
253
|
+
);
|
|
254
|
+
const resp = await fetch(
|
|
255
|
+
`${this.serverUrl}/api/watchdog/analyze-logs`,
|
|
256
|
+
{
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: { "Content-Type": "application/json" },
|
|
259
|
+
body: JSON.stringify({ processLogs: logs }),
|
|
260
|
+
signal: AbortSignal.timeout(60000),
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
264
|
+
const data = await resp.json();
|
|
265
|
+
if (data.llmAnalysis && data.llmAnalysis.length > 0) {
|
|
266
|
+
this._llmResults = data.llmAnalysis;
|
|
267
|
+
console.error(
|
|
268
|
+
`[llm-analyzer] 分析完成: ${this._llmResults.map((r) => r.severity).join(",")}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
} catch (e) {
|
|
272
|
+
console.error(`[llm-analyzer] 调用失败: ${e.message}`);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
setTimeout(run, 60_000);
|
|
277
|
+
this.llmAnalyzerTimer = setInterval(run, 10 * 60_000);
|
|
278
|
+
}
|
|
279
|
+
|
|
188
280
|
/**
|
|
189
281
|
* 启动自动升级检测(每 10 分钟)
|
|
190
282
|
* 仅全局安装模式下启用
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 日志分析模块 — 供服务端调用(传入 llmFetch),或客户端独立使用(读环境变量)
|
|
3
|
+
*
|
|
4
|
+
* 分析结果通过心跳上报到 watch 服务端展示。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ── 分析入口 ──
|
|
8
|
+
|
|
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
|
+
export async function analyzeProcessLogs(processLogs, llmFetch) {
|
|
16
|
+
if (!processLogs || processLogs.length === 0) return [];
|
|
17
|
+
|
|
18
|
+
// 如果没有传入 llmFetch,使用内置的轻量调用(客户端模式)
|
|
19
|
+
if (!llmFetch) {
|
|
20
|
+
llmFetch = await createStandaloneFetch();
|
|
21
|
+
if (!llmFetch) return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const results = [];
|
|
25
|
+
for (const proc of processLogs) {
|
|
26
|
+
try {
|
|
27
|
+
console.error(`[llm-analyzer] 分析 ${proc.cmdId.substring(0, 40)}... (${proc.logs.length} 行日志)`);
|
|
28
|
+
const content = await llmFetch({
|
|
29
|
+
messages: [{ role: "user", content: buildPrompt(proc.cmdId, proc.logs.slice(-200).join("\n"), proc.uptimeSeconds, proc.restartCount) }],
|
|
30
|
+
max_tokens: 256,
|
|
31
|
+
temperature: 0.1,
|
|
32
|
+
enable_thinking: false,
|
|
33
|
+
});
|
|
34
|
+
const parsed = parseResponse(content);
|
|
35
|
+
console.error(`[llm-analyzer] ${proc.cmdId.substring(0, 30)} → ${parsed.severity}: ${parsed.summary}`);
|
|
36
|
+
results.push({ cmdId: proc.cmdId, ...parsed });
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error(`[llm-analyzer] ${proc.cmdId.substring(0, 30)} 失败: ${e.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── 客户端独立模式(从环境变量读配置) ──
|
|
45
|
+
|
|
46
|
+
async function createStandaloneFetch() {
|
|
47
|
+
try {
|
|
48
|
+
const { existsSync, readFileSync } = await import("fs");
|
|
49
|
+
const { join } = await import("path");
|
|
50
|
+
const { homedir } = await import("os");
|
|
51
|
+
const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
|
|
52
|
+
let fileCfg = {};
|
|
53
|
+
try {
|
|
54
|
+
const cfgPath = join(homeDir, ".tt-help.json");
|
|
55
|
+
if (existsSync(cfgPath)) fileCfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
56
|
+
} catch {}
|
|
57
|
+
const url = process.env.LLM_URL || fileCfg.llmUrl || "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
|
|
58
|
+
const apiKey = process.env.LLM_API_KEY || fileCfg.llmApiKey || "";
|
|
59
|
+
const model = process.env.LLM_MODEL || fileCfg.llmModel || "auto";
|
|
60
|
+
if (!apiKey) { console.error("[llm-analyzer] 未配置 LLM_API_KEY"); return null; }
|
|
61
|
+
console.error(`[llm-analyzer] 客户端模式: ${model} @ ${url}`);
|
|
62
|
+
|
|
63
|
+
// 返回兼容 llmFetch 签名的函数
|
|
64
|
+
return async ({ messages, max_tokens = 256, temperature = 0.1 }) => {
|
|
65
|
+
const resp = await fetch(url, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
68
|
+
body: JSON.stringify({ model, messages, max_tokens, temperature }),
|
|
69
|
+
signal: AbortSignal.timeout(30000),
|
|
70
|
+
});
|
|
71
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
72
|
+
const result = await resp.json();
|
|
73
|
+
const content = result?.choices?.[0]?.message?.content;
|
|
74
|
+
if (!content) throw new Error("LLM 返回空内容");
|
|
75
|
+
return content;
|
|
76
|
+
};
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error(`[llm-analyzer] 客户端模式初始化失败: ${e.message}`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
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
|
+
|
|
104
|
+
function buildPrompt(cmdId, logs, uptimeSeconds, restartCount) {
|
|
105
|
+
const uptime =
|
|
106
|
+
uptimeSeconds >= 3600
|
|
107
|
+
? Math.round(uptimeSeconds / 3600) + "h"
|
|
108
|
+
: uptimeSeconds + "s";
|
|
109
|
+
|
|
110
|
+
return `你是 TikTok 数据采集工具的运维助手。以下是进程 "${cmdId}" 的最近 10 分钟日志,请分析是否有异常。
|
|
111
|
+
|
|
112
|
+
进程信息:
|
|
113
|
+
- 命令行: ${cmdId}
|
|
114
|
+
- 已运行: ${uptime}
|
|
115
|
+
- 历史重启: ${restartCount} 次
|
|
116
|
+
|
|
117
|
+
最近日志:
|
|
118
|
+
${logs || "(无日志)"}
|
|
119
|
+
|
|
120
|
+
请返回 JSON(仅 JSON,无其他内容):
|
|
121
|
+
{"severity":"ok"|"warn"|"error","summary":"一句话中文总结,不超过 50 字","suggestions":["建议1","建议2"]}
|
|
122
|
+
|
|
123
|
+
判断标准:
|
|
124
|
+
- ok: 无异常,正常运行
|
|
125
|
+
- warn: 有偶发错误(验证码、403、超时 < 3次)或内存偏高
|
|
126
|
+
- error: 严重错误(连续崩溃、登录失效、端口占用、大量 403)
|
|
127
|
+
- 每个 suggestions 是简洁可操作的建议,不超过 3 条`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── 响应解析 ──
|
|
131
|
+
|
|
132
|
+
function parseResponse(content) {
|
|
133
|
+
try {
|
|
134
|
+
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
|
+
};
|
|
142
|
+
} catch {
|
|
143
|
+
// 尝试提取 JSON 块
|
|
144
|
+
const match = content.match(/\{[\s\S]*\}/);
|
|
145
|
+
if (match) {
|
|
146
|
+
try {
|
|
147
|
+
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
|
+
};
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
// 解析失败:将原始内容作为 summary
|
|
158
|
+
return {
|
|
159
|
+
severity: "warn",
|
|
160
|
+
summary: content.substring(0, 100),
|
|
161
|
+
suggestions: [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -12,8 +12,12 @@
|
|
|
12
12
|
import { spawn, execSync } from "child_process";
|
|
13
13
|
import { EventEmitter } from "events";
|
|
14
14
|
import { killEdgeProcesses } from "../lib/browser/cdp.js";
|
|
15
|
+
import { writeFileSync, unlinkSync } from "fs";
|
|
16
|
+
import { tmpdir } from "os";
|
|
17
|
+
import { join } from "path";
|
|
15
18
|
|
|
16
19
|
const MAX_RESTART = 3;
|
|
20
|
+
const MAX_LOG_LINES = 500; // 每个进程最多保留的日志行数
|
|
17
21
|
const IS_WIN = process.platform === "win32";
|
|
18
22
|
|
|
19
23
|
// 从命令行中提取 CDP 端口列表(解析 --base-port 和 --port-count)
|
|
@@ -59,27 +63,45 @@ function splitCommand(cmdLine) {
|
|
|
59
63
|
|
|
60
64
|
function getProcessStats(pid) {
|
|
61
65
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
if (IS_WIN) {
|
|
67
|
+
// 与 tag.js handleScoreAll 一致:用临时 .ps1 + Get-CimInstance Win32_Process + WorkingSetSize
|
|
68
|
+
const ps1Path = join(tmpdir(), `wd-proc-${pid}-${Date.now()}.ps1`);
|
|
69
|
+
try {
|
|
70
|
+
const ps1Content =
|
|
71
|
+
`$p = Get-CimInstance Win32_Process -Filter "ProcessId='${pid}'" -ErrorAction SilentlyContinue; ` +
|
|
72
|
+
`if ($p) { Write-Output $p.WorkingSetSize } else { Write-Output 0 }`;
|
|
73
|
+
writeFileSync(ps1Path, ps1Content);
|
|
74
|
+
const stdout = execSync(
|
|
75
|
+
`powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}"`,
|
|
76
|
+
{
|
|
77
|
+
encoding: "utf-8",
|
|
78
|
+
timeout: 5000,
|
|
79
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
80
|
+
},
|
|
81
|
+
).trim();
|
|
82
|
+
try {
|
|
83
|
+
unlinkSync(ps1Path);
|
|
84
|
+
} catch {}
|
|
85
|
+
const bytes = parseInt(stdout, 10);
|
|
86
|
+
return {
|
|
87
|
+
memoryMB: bytes > 0 ? Math.round((bytes / 1048576) * 10) / 10 : 0,
|
|
88
|
+
elapsed: null,
|
|
89
|
+
};
|
|
90
|
+
} catch {
|
|
91
|
+
try {
|
|
92
|
+
unlinkSync(ps1Path);
|
|
93
|
+
} catch {}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// macOS/Linux
|
|
99
|
+
const stdout = execSync(`ps -p ${pid} -o rss=,etime=`, {
|
|
66
100
|
encoding: "utf-8",
|
|
67
101
|
timeout: 3000,
|
|
68
102
|
stdio: ["ignore", "pipe", "ignore"],
|
|
69
103
|
}).trim();
|
|
70
104
|
if (!stdout) return null;
|
|
71
|
-
|
|
72
|
-
if (IS_WIN) {
|
|
73
|
-
// PowerShell 返回字节数,运行时长从内部 startTime 计算
|
|
74
|
-
const memBytes = parseInt(stdout, 10);
|
|
75
|
-
return {
|
|
76
|
-
memoryMB: isNaN(memBytes)
|
|
77
|
-
? 0
|
|
78
|
-
: Math.round((memBytes / 1048576) * 10) / 10,
|
|
79
|
-
elapsed: null,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
105
|
const parts = stdout.split(/\s+/);
|
|
84
106
|
const rssKB = parseInt(parts[0], 10);
|
|
85
107
|
const elapsed = parts.slice(1).join(" ") || "0";
|
|
@@ -157,7 +179,14 @@ export class ProcessManager extends EventEmitter {
|
|
|
157
179
|
child.stderr?.on("data", (data) => {
|
|
158
180
|
const msg = data.toString().trim();
|
|
159
181
|
if (msg) {
|
|
160
|
-
|
|
182
|
+
const short = msg.substring(0, 200);
|
|
183
|
+
console.error(`[${id}] ${short}`);
|
|
184
|
+
// 环形缓冲:保留最近 N 行供 LLM 分析
|
|
185
|
+
const info = this.processes.get(id);
|
|
186
|
+
if (info) {
|
|
187
|
+
info.logs.push(short);
|
|
188
|
+
if (info.logs.length > MAX_LOG_LINES) info.logs.shift();
|
|
189
|
+
}
|
|
161
190
|
}
|
|
162
191
|
});
|
|
163
192
|
|
|
@@ -208,6 +237,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
208
237
|
startTime,
|
|
209
238
|
restartCount: 0,
|
|
210
239
|
stopped: false,
|
|
240
|
+
logs: [],
|
|
211
241
|
});
|
|
212
242
|
}
|
|
213
243
|
|
|
@@ -302,6 +332,26 @@ export class ProcessManager extends EventEmitter {
|
|
|
302
332
|
return result;
|
|
303
333
|
}
|
|
304
334
|
|
|
335
|
+
/**
|
|
336
|
+
* 获取所有进程的日志快照(供 LLM 分析)
|
|
337
|
+
* @returns {Array<{cmdId:string, logs:string[], uptimeSeconds:number, restartCount:number}>}
|
|
338
|
+
*/
|
|
339
|
+
getProcessesLogs() {
|
|
340
|
+
const result = [];
|
|
341
|
+
for (const [cmdId, info] of this.processes) {
|
|
342
|
+
if (info.stopped || !info.logs || info.logs.length === 0) continue;
|
|
343
|
+
result.push({
|
|
344
|
+
cmdId,
|
|
345
|
+
logs: info.logs.slice(), // 快照,避免分析时被修改
|
|
346
|
+
uptimeSeconds: Math.round((Date.now() - info.startTime) / 1000),
|
|
347
|
+
restartCount: info.restartCount,
|
|
348
|
+
});
|
|
349
|
+
// 清空已读取的日志,避免重复分析
|
|
350
|
+
info.logs = [];
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
|
|
305
355
|
/**
|
|
306
356
|
* 与期望命令列表同步:
|
|
307
357
|
* - 启动期望列表中已启用但未运行的命令
|