tt-help-cli-ycl 1.4.70 → 1.4.71
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 +32 -0
- package/src/watchdog/agent.js +104 -0
- package/src/watchdog/llm-analyzer.js +137 -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) {
|
|
@@ -189,6 +204,7 @@ async function handleHeartbeat(params, body, res) {
|
|
|
189
204
|
sendJSON(res, 200, {
|
|
190
205
|
ok: true,
|
|
191
206
|
commands,
|
|
207
|
+
clientConfig: getClientConfigStored(userId),
|
|
192
208
|
serverTime: Date.now(),
|
|
193
209
|
});
|
|
194
210
|
}
|
|
@@ -219,6 +235,18 @@ async function handleSetClientCommands(params, body, res) {
|
|
|
219
235
|
sendJSON(res, 200, { ok: true, count: commands.length });
|
|
220
236
|
}
|
|
221
237
|
|
|
238
|
+
async function handleClientConfig(params, body, res) {
|
|
239
|
+
const userId = body.userId;
|
|
240
|
+
const config = body.config;
|
|
241
|
+
if (!userId || !config) {
|
|
242
|
+
sendJSON(res, 400, { error: "缺少 userId 或 config" });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
setClientConfigStored(userId, config);
|
|
246
|
+
console.error(`[watchdog] ${userId} 配置已更新`);
|
|
247
|
+
sendJSON(res, 200, { ok: true });
|
|
248
|
+
}
|
|
249
|
+
|
|
222
250
|
// ── 公开 ──
|
|
223
251
|
|
|
224
252
|
export function registerWatchdogRoutes(httpServer) {
|
|
@@ -245,5 +273,9 @@ export async function tryHandleWatchdogRoute(
|
|
|
245
273
|
await handleSetClientCommands(params, body, res);
|
|
246
274
|
return true;
|
|
247
275
|
}
|
|
276
|
+
if (method === "POST" && routePath === "/api/watchdog/client-config") {
|
|
277
|
+
await handleClientConfig(params, body, res);
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
248
280
|
return false;
|
|
249
281
|
}
|
package/src/watchdog/agent.js
CHANGED
|
@@ -15,6 +15,47 @@ import {
|
|
|
15
15
|
isGlobalInstall,
|
|
16
16
|
startUpgradeChecker,
|
|
17
17
|
} from "./upgrader.js";
|
|
18
|
+
import { analyzeProcessLogs } from "./llm-analyzer.js";
|
|
19
|
+
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
import { homedir } from "os";
|
|
22
|
+
|
|
23
|
+
const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
|
|
24
|
+
const TT_HELP_CONFIG = join(homeDir, ".tt-help.json");
|
|
25
|
+
|
|
26
|
+
/** 将服务端下发的配置合并写回 ~/.tt-help.json */
|
|
27
|
+
function applyClientConfig(newCfg) {
|
|
28
|
+
try {
|
|
29
|
+
let cfg = {};
|
|
30
|
+
if (existsSync(TT_HELP_CONFIG)) {
|
|
31
|
+
cfg = JSON.parse(readFileSync(TT_HELP_CONFIG, "utf-8"));
|
|
32
|
+
}
|
|
33
|
+
// 合并下发字段(只更新存在的 key)
|
|
34
|
+
const fields = [
|
|
35
|
+
"server",
|
|
36
|
+
"proxy",
|
|
37
|
+
"browser",
|
|
38
|
+
"userId",
|
|
39
|
+
"maxVideos",
|
|
40
|
+
"maxComments",
|
|
41
|
+
"maxFollowing",
|
|
42
|
+
"maxFollowers",
|
|
43
|
+
];
|
|
44
|
+
let changed = false;
|
|
45
|
+
for (const f of fields) {
|
|
46
|
+
if (newCfg[f] !== undefined && newCfg[f] !== cfg[f]) {
|
|
47
|
+
cfg[f] = newCfg[f];
|
|
48
|
+
changed = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (changed) {
|
|
52
|
+
writeFileSync(TT_HELP_CONFIG, JSON.stringify(cfg, null, 2), "utf-8");
|
|
53
|
+
console.error("[watchdog] ~/.tt-help.json 已更新(来自服务端下发)");
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error(`[watchdog] 配置更新失败: ${e.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
18
59
|
|
|
19
60
|
export class WatchdogAgent {
|
|
20
61
|
/**
|
|
@@ -30,6 +71,8 @@ export class WatchdogAgent {
|
|
|
30
71
|
this.running = false;
|
|
31
72
|
this.heartbeatTimer = null;
|
|
32
73
|
this.thresholdTimer = null;
|
|
74
|
+
this.llmAnalyzerTimer = null;
|
|
75
|
+
this._llmResults = []; // 最近一次 LLM 分析结果
|
|
33
76
|
this.processManager = new ProcessManager();
|
|
34
77
|
this.consecutiveFailures = 0;
|
|
35
78
|
this.maxFailures = 10;
|
|
@@ -70,6 +113,9 @@ export class WatchdogAgent {
|
|
|
70
113
|
this.processManager.checkThresholds();
|
|
71
114
|
}, 5000);
|
|
72
115
|
|
|
116
|
+
// LLM 日志分析(每 10 分钟)
|
|
117
|
+
this._startLlmAnalyzer();
|
|
118
|
+
|
|
73
119
|
// 自动升级检测(仅全局安装模式)
|
|
74
120
|
this._startUpgradeChecker();
|
|
75
121
|
}
|
|
@@ -87,6 +133,10 @@ export class WatchdogAgent {
|
|
|
87
133
|
clearInterval(this.thresholdTimer);
|
|
88
134
|
this.thresholdTimer = null;
|
|
89
135
|
}
|
|
136
|
+
if (this.llmAnalyzerTimer) {
|
|
137
|
+
clearInterval(this.llmAnalyzerTimer);
|
|
138
|
+
this.llmAnalyzerTimer = null;
|
|
139
|
+
}
|
|
90
140
|
if (this.upgradeChecker) {
|
|
91
141
|
this.upgradeChecker.stop();
|
|
92
142
|
this.upgradeChecker = null;
|
|
@@ -116,6 +166,7 @@ export class WatchdogAgent {
|
|
|
116
166
|
},
|
|
117
167
|
config: getClientConfig(),
|
|
118
168
|
processes: this.processManager.getProcessesStatus(),
|
|
169
|
+
llmAnalysis: this._llmResults,
|
|
119
170
|
};
|
|
120
171
|
}
|
|
121
172
|
|
|
@@ -159,6 +210,11 @@ export class WatchdogAgent {
|
|
|
159
210
|
}
|
|
160
211
|
}
|
|
161
212
|
|
|
213
|
+
// 服务端下发的客户端配置(远程修改 ~/.tt-help.json)
|
|
214
|
+
if (data.clientConfig) {
|
|
215
|
+
applyClientConfig(data.clientConfig);
|
|
216
|
+
}
|
|
217
|
+
|
|
162
218
|
// 心跳日志:仅状态变化或每 10 次打印一次
|
|
163
219
|
this._heartbeatSeq++;
|
|
164
220
|
const cmdCount = data.commands?.length || 0;
|
|
@@ -185,6 +241,54 @@ export class WatchdogAgent {
|
|
|
185
241
|
}
|
|
186
242
|
}
|
|
187
243
|
|
|
244
|
+
/**
|
|
245
|
+
* 启动 LLM 日志分析定时器(每 10 分钟)
|
|
246
|
+
*/
|
|
247
|
+
_startLlmAnalyzer() {
|
|
248
|
+
// 首次 60s 后执行(等进程启动稳定),之后每 10 分钟
|
|
249
|
+
const run = async () => {
|
|
250
|
+
try {
|
|
251
|
+
console.error("[llm-analyzer] 定时触发...");
|
|
252
|
+
const logs = this.processManager.getProcessesLogs();
|
|
253
|
+
if (logs.length === 0) {
|
|
254
|
+
console.error(
|
|
255
|
+
"[llm-analyzer] 无日志,跳过(进程可能无 stderr 输出)",
|
|
256
|
+
);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
console.error(`[llm-analyzer] 开始分析 ${logs.length} 个进程的日志...`);
|
|
260
|
+
this._llmResults = await analyzeProcessLogs(logs);
|
|
261
|
+
if (this._llmResults.length === 0) {
|
|
262
|
+
this._llmResults = [
|
|
263
|
+
{
|
|
264
|
+
cmdId: "_status",
|
|
265
|
+
severity: "warn",
|
|
266
|
+
summary: "LLM 分析未返回结果,请检查 LLM 配置",
|
|
267
|
+
suggestions: [],
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
const summary = this._llmResults
|
|
272
|
+
.map((r) => `${r.cmdId.substring(0, 30)}: ${r.severity}`)
|
|
273
|
+
.join(", ");
|
|
274
|
+
console.error(`[llm-analyzer] 分析完成: ${summary || "无异常"}`);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.error(`[llm-analyzer] 异常: ${e.message}`);
|
|
277
|
+
this._llmResults = [
|
|
278
|
+
{
|
|
279
|
+
cmdId: "_status",
|
|
280
|
+
severity: "error",
|
|
281
|
+
summary: "LLM 分析异常: " + e.message.substring(0, 80),
|
|
282
|
+
suggestions: [],
|
|
283
|
+
},
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
setTimeout(run, 60_000);
|
|
289
|
+
this.llmAnalyzerTimer = setInterval(run, 10 * 60_000);
|
|
290
|
+
}
|
|
291
|
+
|
|
188
292
|
/**
|
|
189
293
|
* 启动自动升级检测(每 10 分钟)
|
|
190
294
|
* 仅全局安装模式下启用
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 日志分析模块 — 定期收集各进程日志,调用 LLM 分析错误并给出建议
|
|
3
|
+
*
|
|
4
|
+
* 每 10 分钟执行一次,分析结果通过心跳上报到 watch 服务端。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ── 分析入口 ──
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 对所有进程的日志调用 LLM 分析
|
|
11
|
+
* @param {Array<{cmdId:string, logs:string[], uptimeSeconds:number, restartCount:number}>} processLogs
|
|
12
|
+
* @returns {Promise<Array<{cmdId:string, severity:string, summary:string, suggestions:string[]}>>}
|
|
13
|
+
*/
|
|
14
|
+
export async function analyzeProcessLogs(processLogs) {
|
|
15
|
+
if (!processLogs || processLogs.length === 0) return [];
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const { llmFetch } = await import("../watch/llm-config.js");
|
|
19
|
+
console.error("[llm-analyzer] llmFetch 模块加载成功");
|
|
20
|
+
const results = [];
|
|
21
|
+
|
|
22
|
+
for (const proc of processLogs) {
|
|
23
|
+
try {
|
|
24
|
+
console.error(
|
|
25
|
+
`[llm-analyzer] 分析 ${proc.cmdId.substring(0, 40)}... (${proc.logs.length} 行日志)`,
|
|
26
|
+
);
|
|
27
|
+
const analysis = await analyzeOneProcess(llmFetch, proc);
|
|
28
|
+
if (analysis) {
|
|
29
|
+
console.error(
|
|
30
|
+
`[llm-analyzer] ${proc.cmdId.substring(0, 30)} → ${analysis.severity}: ${analysis.summary}`,
|
|
31
|
+
);
|
|
32
|
+
results.push(analysis);
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error(
|
|
36
|
+
`[llm-analyzer] ${proc.cmdId.substring(0, 30)} 分析失败: ${e.message}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return results;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error(`[llm-analyzer] llmFetch 加载失败: ${e.message}`);
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── 单进程分析 ──
|
|
49
|
+
|
|
50
|
+
async function analyzeOneProcess(llmFetch, proc) {
|
|
51
|
+
const recentLogs = proc.logs.slice(-200).join("\n"); // 最多取最近 200 行
|
|
52
|
+
const prompt = buildPrompt(
|
|
53
|
+
proc.cmdId,
|
|
54
|
+
recentLogs,
|
|
55
|
+
proc.uptimeSeconds,
|
|
56
|
+
proc.restartCount,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const content = await llmFetch({
|
|
60
|
+
messages: [{ role: "user", content: prompt }],
|
|
61
|
+
max_tokens: 256,
|
|
62
|
+
temperature: 0.1,
|
|
63
|
+
enable_thinking: false,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const parsed = parseResponse(content);
|
|
67
|
+
return {
|
|
68
|
+
cmdId: proc.cmdId,
|
|
69
|
+
severity: parsed.severity || "ok",
|
|
70
|
+
summary: parsed.summary || "无异常",
|
|
71
|
+
suggestions: parsed.suggestions || [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Prompt 构建 ──
|
|
76
|
+
|
|
77
|
+
function buildPrompt(cmdId, logs, uptimeSeconds, restartCount) {
|
|
78
|
+
const uptime =
|
|
79
|
+
uptimeSeconds >= 3600
|
|
80
|
+
? Math.round(uptimeSeconds / 3600) + "h"
|
|
81
|
+
: uptimeSeconds + "s";
|
|
82
|
+
|
|
83
|
+
return `你是 TikTok 数据采集工具的运维助手。以下是进程 "${cmdId}" 的最近 10 分钟日志,请分析是否有异常。
|
|
84
|
+
|
|
85
|
+
进程信息:
|
|
86
|
+
- 命令行: ${cmdId}
|
|
87
|
+
- 已运行: ${uptime}
|
|
88
|
+
- 历史重启: ${restartCount} 次
|
|
89
|
+
|
|
90
|
+
最近日志:
|
|
91
|
+
${logs || "(无日志)"}
|
|
92
|
+
|
|
93
|
+
请返回 JSON(仅 JSON,无其他内容):
|
|
94
|
+
{"severity":"ok"|"warn"|"error","summary":"一句话中文总结,不超过 50 字","suggestions":["建议1","建议2"]}
|
|
95
|
+
|
|
96
|
+
判断标准:
|
|
97
|
+
- ok: 无异常,正常运行
|
|
98
|
+
- warn: 有偶发错误(验证码、403、超时 < 3次)或内存偏高
|
|
99
|
+
- error: 严重错误(连续崩溃、登录失效、端口占用、大量 403)
|
|
100
|
+
- 每个 suggestions 是简洁可操作的建议,不超过 3 条`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── 响应解析 ──
|
|
104
|
+
|
|
105
|
+
function parseResponse(content) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(content);
|
|
108
|
+
return {
|
|
109
|
+
severity: parsed.severity || "ok",
|
|
110
|
+
summary: parsed.summary || "",
|
|
111
|
+
suggestions: Array.isArray(parsed.suggestions)
|
|
112
|
+
? parsed.suggestions.slice(0, 3)
|
|
113
|
+
: [],
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
// 尝试提取 JSON 块
|
|
117
|
+
const match = content.match(/\{[\s\S]*\}/);
|
|
118
|
+
if (match) {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(match[0]);
|
|
121
|
+
return {
|
|
122
|
+
severity: parsed.severity || "ok",
|
|
123
|
+
summary: parsed.summary || "",
|
|
124
|
+
suggestions: Array.isArray(parsed.suggestions)
|
|
125
|
+
? parsed.suggestions.slice(0, 3)
|
|
126
|
+
: [],
|
|
127
|
+
};
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
// 解析失败:将原始内容作为 summary
|
|
131
|
+
return {
|
|
132
|
+
severity: "warn",
|
|
133
|
+
summary: content.substring(0, 100),
|
|
134
|
+
suggestions: [],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -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
|
* - 启动期望列表中已启用但未运行的命令
|