tt-help-cli-ycl 1.4.72 → 1.4.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/watch/public/app.js
CHANGED
|
@@ -3448,6 +3448,7 @@ function styled(el, style, content) {
|
|
|
3448
3448
|
function cmdObj(cmd) {
|
|
3449
3449
|
if (typeof cmd === "string")
|
|
3450
3450
|
return {
|
|
3451
|
+
id: "",
|
|
3451
3452
|
name: "",
|
|
3452
3453
|
cmd: cmd,
|
|
3453
3454
|
enabled: true,
|
|
@@ -3455,6 +3456,7 @@ function cmdObj(cmd) {
|
|
|
3455
3456
|
maxMemoryMB: 0,
|
|
3456
3457
|
};
|
|
3457
3458
|
return {
|
|
3459
|
+
id: cmd.id || "",
|
|
3458
3460
|
name: cmd.name || "",
|
|
3459
3461
|
cmd: cmd.cmd || "",
|
|
3460
3462
|
enabled: cmd.enabled !== false,
|
|
@@ -3508,13 +3510,17 @@ function renderWatchdogDashboard(clients, clientCommands) {
|
|
|
3508
3510
|
if (uid) userClients[uid] = c;
|
|
3509
3511
|
});
|
|
3510
3512
|
|
|
3511
|
-
//
|
|
3513
|
+
// 收集所有卡片:全部注册客户端 + 未上线但有命令配置的 userId
|
|
3512
3514
|
var allCards = [];
|
|
3513
3515
|
clients.forEach(function (c) {
|
|
3514
3516
|
allCards.push(renderClientCard(c, clientCommands));
|
|
3515
3517
|
});
|
|
3516
3518
|
for (var uid in clientCommands) {
|
|
3517
|
-
if (
|
|
3519
|
+
if (
|
|
3520
|
+
!userClients[uid] &&
|
|
3521
|
+
clientCommands[uid] &&
|
|
3522
|
+
clientCommands[uid].length > 0
|
|
3523
|
+
) {
|
|
3518
3524
|
allCards.push(renderOfflineCard(uid, clientCommands[uid]));
|
|
3519
3525
|
}
|
|
3520
3526
|
}
|
|
@@ -3524,11 +3530,40 @@ function renderWatchdogDashboard(clients, clientCommands) {
|
|
|
3524
3530
|
if (fingerprint === _wdLastRendered) return;
|
|
3525
3531
|
_wdLastRendered = fingerprint;
|
|
3526
3532
|
|
|
3533
|
+
// 注入动画样式(仅首次)
|
|
3534
|
+
if (!document.getElementById("wdAnimStyle")) {
|
|
3535
|
+
var style = document.createElement("style");
|
|
3536
|
+
style.id = "wdAnimStyle";
|
|
3537
|
+
style.textContent =
|
|
3538
|
+
"@keyframes wdFlash{0%{color:#4ade80}50%{color:#60a5fa}100%{color:inherit}}" +
|
|
3539
|
+
".wd-flash{animation:wdFlash 1.5s ease}";
|
|
3540
|
+
document.head.appendChild(style);
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
// 重建前扫描旧值(按 data-wd-key 索引)
|
|
3544
|
+
var prevVals = {};
|
|
3545
|
+
var oldCells = el.querySelectorAll("[data-wd-key]");
|
|
3546
|
+
for (var i = 0; i < oldCells.length; i++) {
|
|
3547
|
+
prevVals[oldCells[i].getAttribute("data-wd-key")] = oldCells[i].textContent;
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3527
3550
|
el.innerHTML = styled(
|
|
3528
3551
|
"div",
|
|
3529
3552
|
"display:grid;grid-template-columns:repeat(auto-fill,minmax(550px,1fr));gap:24px",
|
|
3530
3553
|
allCards.join(""),
|
|
3531
3554
|
);
|
|
3555
|
+
|
|
3556
|
+
// 只对变化的值加闪烁动画
|
|
3557
|
+
var newCells = el.querySelectorAll("[data-wd-key]");
|
|
3558
|
+
for (var i = 0; i < newCells.length; i++) {
|
|
3559
|
+
var key = newCells[i].getAttribute("data-wd-key");
|
|
3560
|
+
if (
|
|
3561
|
+
prevVals[key] !== undefined &&
|
|
3562
|
+
prevVals[key] !== newCells[i].textContent
|
|
3563
|
+
) {
|
|
3564
|
+
newCells[i].className = "wd-flash";
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3532
3567
|
}
|
|
3533
3568
|
|
|
3534
3569
|
function renderOfflineCard(userId, cmds) {
|
|
@@ -3594,43 +3629,32 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3594
3629
|
);
|
|
3595
3630
|
}
|
|
3596
3631
|
|
|
3597
|
-
//
|
|
3632
|
+
// 匹配进程到命令(按唯一 ID 精确匹配)
|
|
3598
3633
|
function matchProcsToCmds(myCmds, procs) {
|
|
3599
3634
|
var matched = {}; // cmdIdx -> proc
|
|
3600
3635
|
var unmatchedProcs = [];
|
|
3636
|
+
// 建 ID → 命令索引 映射
|
|
3637
|
+
var idToIdx = {};
|
|
3638
|
+
myCmds.forEach(function (cmd, i) {
|
|
3639
|
+
var c = cmdObj(cmd);
|
|
3640
|
+
if (c.id) idToIdx[c.id] = i;
|
|
3641
|
+
});
|
|
3601
3642
|
procs.forEach(function (p) {
|
|
3602
3643
|
var found = false;
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
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
|
-
}
|
|
3644
|
+
if (p.cmdId && idToIdx[p.cmdId] !== undefined) {
|
|
3645
|
+
matched[idToIdx[p.cmdId]] = p;
|
|
3646
|
+
found = true;
|
|
3616
3647
|
}
|
|
3617
3648
|
if (!found) unmatchedProcs.push(p);
|
|
3618
3649
|
});
|
|
3619
3650
|
return { matched: matched, unmatched: unmatchedProcs };
|
|
3620
3651
|
}
|
|
3621
3652
|
|
|
3622
|
-
//
|
|
3623
|
-
function findAnalysis(llmAnalysis,
|
|
3624
|
-
if (!llmAnalysis || !
|
|
3653
|
+
// 按命令 ID 匹配 LLM 分析结果
|
|
3654
|
+
function findAnalysis(llmAnalysis, cmdId) {
|
|
3655
|
+
if (!llmAnalysis || !cmdId) return null;
|
|
3625
3656
|
for (var i = 0; i < llmAnalysis.length; i++) {
|
|
3626
|
-
|
|
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
|
-
}
|
|
3657
|
+
if (llmAnalysis[i].cmdId === cmdId) return llmAnalysis[i];
|
|
3634
3658
|
}
|
|
3635
3659
|
return null;
|
|
3636
3660
|
}
|
|
@@ -3775,6 +3799,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3775
3799
|
var uptime = p ? p.uptimeDisplay || p.uptimeSeconds + "s" : "-";
|
|
3776
3800
|
var restart = p && p.restartCount > 0 ? String(p.restartCount) : "0";
|
|
3777
3801
|
|
|
3802
|
+
var prefix = "wd-" + esc(userId) + "-" + idx;
|
|
3778
3803
|
html += styled(
|
|
3779
3804
|
"div",
|
|
3780
3805
|
"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",
|
|
@@ -3798,12 +3823,27 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3798
3823
|
)
|
|
3799
3824
|
: ""),
|
|
3800
3825
|
) +
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3826
|
+
tag(
|
|
3827
|
+
"span",
|
|
3828
|
+
{ style: "color:#888", "data-wd-key": prefix + "-pid" },
|
|
3829
|
+
pid,
|
|
3830
|
+
) +
|
|
3831
|
+
tag(
|
|
3805
3832
|
"span",
|
|
3806
|
-
"color
|
|
3833
|
+
{ style: "color:#aaa", "data-wd-key": prefix + "-mem" },
|
|
3834
|
+
mem,
|
|
3835
|
+
) +
|
|
3836
|
+
tag(
|
|
3837
|
+
"span",
|
|
3838
|
+
{ style: "color:#888", "data-wd-key": prefix + "-up" },
|
|
3839
|
+
uptime,
|
|
3840
|
+
) +
|
|
3841
|
+
tag(
|
|
3842
|
+
"span",
|
|
3843
|
+
{
|
|
3844
|
+
style: "color:" + (p && p.restartCount > 0 ? "#f87171" : "#555"),
|
|
3845
|
+
"data-wd-key": prefix + "-rst",
|
|
3846
|
+
},
|
|
3807
3847
|
restart,
|
|
3808
3848
|
) +
|
|
3809
3849
|
styled(
|
|
@@ -3848,7 +3888,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3848
3888
|
);
|
|
3849
3889
|
|
|
3850
3890
|
// AI 分析结果
|
|
3851
|
-
var ai = findAnalysis(llmAnalysis, c.
|
|
3891
|
+
var ai = findAnalysis(llmAnalysis, c.id);
|
|
3852
3892
|
if (ai) {
|
|
3853
3893
|
var sevColor =
|
|
3854
3894
|
ai.severity === "error"
|
|
@@ -3871,6 +3911,8 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3871
3911
|
// 未匹配到的运行中进程(孤儿)
|
|
3872
3912
|
match.unmatched.forEach(function (p) {
|
|
3873
3913
|
var sc = p.status === "running" ? "#4ade80" : "#f87171";
|
|
3914
|
+
var oid = p.pid || p.cmdId || "?";
|
|
3915
|
+
var opre = "wd-" + esc(userId) + "-orphan-" + esc(String(oid));
|
|
3874
3916
|
html += styled(
|
|
3875
3917
|
"div",
|
|
3876
3918
|
"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",
|
|
@@ -3879,14 +3921,26 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3879
3921
|
"color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:monospace",
|
|
3880
3922
|
"⚡ " + (p.name || p.cmdId),
|
|
3881
3923
|
) +
|
|
3882
|
-
|
|
3883
|
-
|
|
3924
|
+
tag(
|
|
3925
|
+
"span",
|
|
3926
|
+
{ style: "color:#888", "data-wd-key": opre + "-pid" },
|
|
3927
|
+
p.pid || "-",
|
|
3928
|
+
) +
|
|
3929
|
+
tag(
|
|
3884
3930
|
"span",
|
|
3885
|
-
"color:#aaa",
|
|
3931
|
+
{ style: "color:#aaa", "data-wd-key": opre + "-mem" },
|
|
3886
3932
|
p.memoryMB != null ? p.memoryMB + "MB" : "-",
|
|
3887
3933
|
) +
|
|
3888
|
-
|
|
3889
|
-
|
|
3934
|
+
tag(
|
|
3935
|
+
"span",
|
|
3936
|
+
{ style: "color:#888", "data-wd-key": opre + "-up" },
|
|
3937
|
+
p.uptimeDisplay || p.uptimeSeconds + "s",
|
|
3938
|
+
) +
|
|
3939
|
+
tag(
|
|
3940
|
+
"span",
|
|
3941
|
+
{ style: "color:#555", "data-wd-key": opre + "-rst" },
|
|
3942
|
+
"0",
|
|
3943
|
+
) +
|
|
3890
3944
|
styled("span", "color:#555;font-size:9px", "孤立"),
|
|
3891
3945
|
);
|
|
3892
3946
|
});
|
|
@@ -3956,19 +4010,22 @@ function wdModalSave() {
|
|
|
3956
4010
|
var myCmds = (allCmds[_wdModalUserId] || []).slice();
|
|
3957
4011
|
|
|
3958
4012
|
if (_wdModalEditIdx >= 0) {
|
|
3959
|
-
//
|
|
4013
|
+
// 编辑模式:保留已有 ID
|
|
3960
4014
|
if (_wdModalEditIdx < myCmds.length) {
|
|
4015
|
+
var oldObj = cmdObj(myCmds[_wdModalEditIdx]);
|
|
3961
4016
|
myCmds[_wdModalEditIdx] = {
|
|
4017
|
+
id: oldObj.id,
|
|
3962
4018
|
name: name,
|
|
3963
4019
|
cmd: cmd,
|
|
3964
|
-
enabled:
|
|
4020
|
+
enabled: oldObj.enabled,
|
|
3965
4021
|
maxUptimeSeconds: maxUp,
|
|
3966
4022
|
maxMemoryMB: maxMem,
|
|
3967
4023
|
};
|
|
3968
4024
|
}
|
|
3969
4025
|
} else {
|
|
3970
|
-
//
|
|
4026
|
+
// 添加模式:生成新 ID
|
|
3971
4027
|
myCmds.push({
|
|
4028
|
+
id: String(Date.now()),
|
|
3972
4029
|
name: name,
|
|
3973
4030
|
cmd: cmd,
|
|
3974
4031
|
enabled: true,
|
|
@@ -4059,6 +4116,7 @@ function wdToggleClientCmd(userId, idx) {
|
|
|
4059
4116
|
var cmd = myCmds[idx];
|
|
4060
4117
|
if (typeof cmd === "string") {
|
|
4061
4118
|
myCmds[idx] = {
|
|
4119
|
+
id: String(Date.now()),
|
|
4062
4120
|
cmd: cmd,
|
|
4063
4121
|
enabled: false,
|
|
4064
4122
|
maxUptimeSeconds: 0,
|
|
@@ -4077,17 +4135,40 @@ function wdToggleClientCmd(userId, idx) {
|
|
|
4077
4135
|
wdSaveClientCmds(userId, myCmds);
|
|
4078
4136
|
}
|
|
4079
4137
|
|
|
4138
|
+
var _wdSaving = false;
|
|
4139
|
+
|
|
4140
|
+
function wdShowSaving() {
|
|
4141
|
+
_wdSaving = true;
|
|
4142
|
+
var el = document.getElementById("wdSavingTip");
|
|
4143
|
+
if (!el) {
|
|
4144
|
+
el = document.createElement("div");
|
|
4145
|
+
el.id = "wdSavingTip";
|
|
4146
|
+
el.style.cssText =
|
|
4147
|
+
"position:fixed;top:60px;right:20px;background:#f59e0b;color:#000;padding:8px 16px;border-radius:6px;font-size:12px;font-weight:600;z-index:9998;pointer-events:none";
|
|
4148
|
+
document.body.appendChild(el);
|
|
4149
|
+
}
|
|
4150
|
+
el.textContent = "⏳ 处理中...";
|
|
4151
|
+
el.style.display = "block";
|
|
4152
|
+
}
|
|
4153
|
+
function wdHideSaving() {
|
|
4154
|
+
_wdSaving = false;
|
|
4155
|
+
var el = document.getElementById("wdSavingTip");
|
|
4156
|
+
if (el) el.style.display = "none";
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4080
4159
|
async function wdSaveClientCmds(userId, commands) {
|
|
4160
|
+
wdShowSaving();
|
|
4081
4161
|
try {
|
|
4082
4162
|
await fetch("/api/watchdog/client-commands", {
|
|
4083
4163
|
method: "POST",
|
|
4084
4164
|
headers: { "Content-Type": "application/json" },
|
|
4085
4165
|
body: JSON.stringify({ userId: userId, commands: commands }),
|
|
4086
4166
|
});
|
|
4167
|
+
wdHideSaving();
|
|
4087
4168
|
fetchWatchdogData();
|
|
4088
4169
|
} catch (e) {
|
|
4170
|
+
wdHideSaving();
|
|
4089
4171
|
alert("保存失败: " + e.message);
|
|
4090
|
-
// 失败时立即刷新恢复按钮状态
|
|
4091
4172
|
fetchWatchdogData();
|
|
4092
4173
|
}
|
|
4093
4174
|
}
|
|
@@ -4097,9 +4178,23 @@ function wdDeleteClient(userId) {
|
|
|
4097
4178
|
!confirm(
|
|
4098
4179
|
"确定删除 " +
|
|
4099
4180
|
userId +
|
|
4100
|
-
"
|
|
4181
|
+
" 及其所有命令配置?\n\n删除后该客户端将从列表中移除。",
|
|
4101
4182
|
)
|
|
4102
4183
|
)
|
|
4103
4184
|
return;
|
|
4104
|
-
|
|
4185
|
+
wdShowSaving();
|
|
4186
|
+
fetch("/api/watchdog/delete-client", {
|
|
4187
|
+
method: "POST",
|
|
4188
|
+
headers: { "Content-Type": "application/json" },
|
|
4189
|
+
body: JSON.stringify({ userId: userId }),
|
|
4190
|
+
})
|
|
4191
|
+
.then(function () {
|
|
4192
|
+
wdHideSaving();
|
|
4193
|
+
fetchWatchdogData();
|
|
4194
|
+
})
|
|
4195
|
+
.catch(function (e) {
|
|
4196
|
+
wdHideSaving();
|
|
4197
|
+
alert("删除失败: " + e.message);
|
|
4198
|
+
fetchWatchdogData();
|
|
4199
|
+
});
|
|
4105
4200
|
}
|
|
@@ -577,9 +577,9 @@
|
|
|
577
577
|
placeholder="默认">
|
|
578
578
|
</div>
|
|
579
579
|
<div>
|
|
580
|
-
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px"
|
|
581
|
-
<input id="wdCfgUserId"
|
|
582
|
-
style="width:100%;padding:6px;background:#
|
|
580
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">用户编号(不可修改)</label>
|
|
581
|
+
<input id="wdCfgUserId" readonly
|
|
582
|
+
style="width:100%;padding:6px;background:#1a1a2a;border:1px solid #333;color:#666;border-radius:4px;font-size:11px;box-sizing:border-box;cursor:not-allowed"
|
|
583
583
|
placeholder="自动生成">
|
|
584
584
|
</div>
|
|
585
585
|
<div>
|
|
@@ -84,12 +84,14 @@ function getOnlineClients() {
|
|
|
84
84
|
function normalizeCmd(c) {
|
|
85
85
|
if (typeof c === "string")
|
|
86
86
|
return {
|
|
87
|
+
id: "",
|
|
87
88
|
cmd: c.replace(/^[▶\n\r\s]+/, "").trim(),
|
|
88
89
|
enabled: true,
|
|
89
90
|
maxUptimeSeconds: 0,
|
|
90
91
|
maxMemoryMB: 0,
|
|
91
92
|
};
|
|
92
93
|
return {
|
|
94
|
+
id: c.id || "",
|
|
93
95
|
cmd: (c.cmd || "").replace(/^[▶\n\r\s]+/, "").trim(),
|
|
94
96
|
enabled: c.enabled !== false,
|
|
95
97
|
maxUptimeSeconds: c.maxUptimeSeconds || 0,
|
|
@@ -105,6 +107,7 @@ function getClientCommands(userId) {
|
|
|
105
107
|
.map(normalizeCmd)
|
|
106
108
|
.filter((c) => c.enabled && c.cmd)
|
|
107
109
|
.map((c) => ({
|
|
110
|
+
id: c.id,
|
|
108
111
|
cmd: c.cmd,
|
|
109
112
|
maxUptimeSeconds: c.maxUptimeSeconds,
|
|
110
113
|
maxMemoryMB: c.maxMemoryMB,
|
|
@@ -113,14 +116,35 @@ function getClientCommands(userId) {
|
|
|
113
116
|
|
|
114
117
|
function setClientCommands(userId, commands) {
|
|
115
118
|
if (!Array.isArray(commands)) return;
|
|
116
|
-
|
|
117
|
-
const cleaned = commands.map((c) => {
|
|
118
|
-
if (typeof c === "string")
|
|
119
|
-
|
|
119
|
+
let nextId = 1;
|
|
120
|
+
const cleaned = commands.map((c, idx) => {
|
|
121
|
+
if (typeof c === "string") {
|
|
122
|
+
const id = String(Date.now() + idx);
|
|
123
|
+
return {
|
|
124
|
+
id,
|
|
125
|
+
cmd: c.replace(/^[▶\n\r\s]+/, "").trim(),
|
|
126
|
+
enabled: true,
|
|
127
|
+
maxUptimeSeconds: 0,
|
|
128
|
+
maxMemoryMB: 0,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const obj = { ...c, cmd: (c.cmd || "").replace(/^[▶\n\r\s]+/, "").trim() };
|
|
132
|
+
if (!obj.id) obj.id = String(Date.now() + nextId++);
|
|
133
|
+
return obj;
|
|
120
134
|
});
|
|
121
135
|
const cfg = loadConfig();
|
|
122
136
|
if (!cfg.clientCommands) cfg.clientCommands = {};
|
|
123
|
-
|
|
137
|
+
if (cleaned.length === 0) {
|
|
138
|
+
delete cfg.clientCommands[userId];
|
|
139
|
+
console.error(
|
|
140
|
+
`[watchdog ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}] ${userId} 命令已清空,卡片已删除`,
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
cfg.clientCommands[userId] = cleaned;
|
|
144
|
+
console.error(
|
|
145
|
+
`[watchdog ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}] ${userId} 命令已更新: ${cleaned.length} 条`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
124
148
|
saveConfig(cfg);
|
|
125
149
|
}
|
|
126
150
|
|
|
@@ -265,8 +289,33 @@ async function handleAnalyzeLogs(params, body, res) {
|
|
|
265
289
|
sendJSON(res, 200, { llmAnalysis });
|
|
266
290
|
} catch (e) {
|
|
267
291
|
console.error(`[watchdog] LLM 分析失败: ${e.message}`);
|
|
268
|
-
sendJSON(res,
|
|
292
|
+
sendJSON(res, 200, { llmAnalysis: [] });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function handleDeleteClient(params, body, res) {
|
|
297
|
+
const userId = body.userId;
|
|
298
|
+
if (!userId) {
|
|
299
|
+
sendJSON(res, 400, { error: "缺少 userId" });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
// 1. 删除命令配置
|
|
303
|
+
const cfg = loadConfig();
|
|
304
|
+
if (cfg.clientCommands) delete cfg.clientCommands[userId];
|
|
305
|
+
if (cfg.clientConfigs) delete cfg.clientConfigs[userId];
|
|
306
|
+
saveConfig(cfg);
|
|
307
|
+
// 2. 从内存客户端列表中移除所有匹配的客户端
|
|
308
|
+
let removed = 0;
|
|
309
|
+
for (const [clientId, info] of clients) {
|
|
310
|
+
if ((info.config && info.config.userId) === userId) {
|
|
311
|
+
clients.delete(clientId);
|
|
312
|
+
removed++;
|
|
313
|
+
}
|
|
269
314
|
}
|
|
315
|
+
console.error(
|
|
316
|
+
`[watchdog] ${userId} 已删除(${removed}个客户端,命令+配置已清除)`,
|
|
317
|
+
);
|
|
318
|
+
sendJSON(res, 200, { ok: true, removed });
|
|
270
319
|
}
|
|
271
320
|
|
|
272
321
|
// ── 公开 ──
|
|
@@ -274,6 +323,29 @@ async function handleAnalyzeLogs(params, body, res) {
|
|
|
274
323
|
export function registerWatchdogRoutes(httpServer) {
|
|
275
324
|
console.error("[watchdog] 看门狗服务端模块已加载");
|
|
276
325
|
console.error(`[watchdog] 配置文件: ${WATCHDOG_CONFIG_PATH}`);
|
|
326
|
+
// 自动迁移:为已有命令补充唯一 ID
|
|
327
|
+
migrateCommandIds();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 为现有命令中未分配 ID 的自动补充 ID
|
|
331
|
+
function migrateCommandIds() {
|
|
332
|
+
const cfg = loadConfig();
|
|
333
|
+
if (!cfg.clientCommands) return;
|
|
334
|
+
let migrated = 0;
|
|
335
|
+
for (const userId of Object.keys(cfg.clientCommands)) {
|
|
336
|
+
const cmds = cfg.clientCommands[userId];
|
|
337
|
+
if (!Array.isArray(cmds)) continue;
|
|
338
|
+
for (const c of cmds) {
|
|
339
|
+
if (typeof c === "object" && !c.id) {
|
|
340
|
+
c.id = String(Date.now() + migrated);
|
|
341
|
+
migrated++;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (migrated > 0) {
|
|
346
|
+
saveConfig(cfg);
|
|
347
|
+
console.error(`[watchdog] 已为 ${migrated} 条旧命令补充 ID`);
|
|
348
|
+
}
|
|
277
349
|
}
|
|
278
350
|
|
|
279
351
|
export async function tryHandleWatchdogRoute(
|
|
@@ -303,5 +375,9 @@ export async function tryHandleWatchdogRoute(
|
|
|
303
375
|
await handleAnalyzeLogs(params, body, res);
|
|
304
376
|
return true;
|
|
305
377
|
}
|
|
378
|
+
if (method === "POST" && routePath === "/api/watchdog/delete-client") {
|
|
379
|
+
await handleDeleteClient(params, body, res);
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
306
382
|
return false;
|
|
307
383
|
}
|
package/src/watchdog/agent.js
CHANGED
|
@@ -42,9 +42,21 @@ function applyClientConfig(newCfg) {
|
|
|
42
42
|
];
|
|
43
43
|
let changed = false;
|
|
44
44
|
for (const f of fields) {
|
|
45
|
-
if (newCfg[f] !== undefined
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
if (newCfg[f] !== undefined) {
|
|
46
|
+
var newVal = newCfg[f],
|
|
47
|
+
oldVal = cfg[f];
|
|
48
|
+
// 忽略无意义的零值/空值(避免每次心跳都写文件)
|
|
49
|
+
if (
|
|
50
|
+
(typeof newVal === "number" &&
|
|
51
|
+
newVal === 0 &&
|
|
52
|
+
oldVal === undefined) ||
|
|
53
|
+
(typeof newVal === "string" && newVal === "" && oldVal === undefined)
|
|
54
|
+
)
|
|
55
|
+
continue;
|
|
56
|
+
if (newVal !== oldVal) {
|
|
57
|
+
cfg[f] = newVal;
|
|
58
|
+
changed = true;
|
|
59
|
+
}
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
62
|
if (changed) {
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LLM 日志分析模块 — 供服务端调用(传入 llmFetch),或客户端独立使用(读环境变量)
|
|
3
|
-
*
|
|
4
|
-
* 分析结果通过心跳上报到 watch 服务端展示。
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
// ── 分析入口 ──
|
|
8
6
|
|
|
9
|
-
/**
|
|
10
|
-
* 对所有进程的日志调用 LLM 分析
|
|
11
|
-
* @param {Array} processLogs — [{cmdId, logs[], uptimeSeconds, restartCount}]
|
|
12
|
-
* @param {Function} [llmFetch] — 可选,服务端传入的 llmFetch;不传则读环境变量
|
|
13
|
-
* @returns {Promise<Array<{cmdId, severity, summary, suggestions}>>}
|
|
14
|
-
*/
|
|
15
7
|
export async function analyzeProcessLogs(processLogs, llmFetch) {
|
|
16
8
|
if (!processLogs || processLogs.length === 0) return [];
|
|
17
9
|
|
|
18
|
-
// 如果没有传入 llmFetch,使用内置的轻量调用(客户端模式)
|
|
19
10
|
if (!llmFetch) {
|
|
20
11
|
llmFetch = await createStandaloneFetch();
|
|
21
12
|
if (!llmFetch) return [];
|
|
@@ -41,7 +32,7 @@ export async function analyzeProcessLogs(processLogs, llmFetch) {
|
|
|
41
32
|
return results;
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
// ──
|
|
35
|
+
// ── 客户端独立模式 ──
|
|
45
36
|
|
|
46
37
|
async function createStandaloneFetch() {
|
|
47
38
|
try {
|
|
@@ -59,8 +50,6 @@ async function createStandaloneFetch() {
|
|
|
59
50
|
const model = process.env.LLM_MODEL || fileCfg.llmModel || "auto";
|
|
60
51
|
if (!apiKey) { console.error("[llm-analyzer] 未配置 LLM_API_KEY"); return null; }
|
|
61
52
|
console.error(`[llm-analyzer] 客户端模式: ${model} @ ${url}`);
|
|
62
|
-
|
|
63
|
-
// 返回兼容 llmFetch 签名的函数
|
|
64
53
|
return async ({ messages, max_tokens = 256, temperature = 0.1 }) => {
|
|
65
54
|
const resp = await fetch(url, {
|
|
66
55
|
method: "POST",
|
|
@@ -79,34 +68,13 @@ async function createStandaloneFetch() {
|
|
|
79
68
|
return null;
|
|
80
69
|
}
|
|
81
70
|
}
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
256,
|
|
85
|
-
);
|
|
86
|
-
const parsed = parseResponse(content);
|
|
87
|
-
console.error(
|
|
88
|
-
`[llm-analyzer] ${proc.cmdId.substring(0, 30)} → ${parsed.severity}: ${parsed.summary}`,
|
|
89
|
-
);
|
|
90
|
-
results.push({ cmdId: proc.cmdId, ...parsed });
|
|
91
|
-
} catch (e) {
|
|
92
|
-
console.error(
|
|
93
|
-
`[llm-analyzer] ${proc.cmdId.substring(0, 30)} 失败: ${e.message}`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return results;
|
|
98
|
-
} catch (e) {
|
|
99
|
-
console.error(`[llm-analyzer] 异常: ${e.message}`);
|
|
100
|
-
return [];
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
71
|
|
|
104
|
-
|
|
105
|
-
const uptime =
|
|
106
|
-
uptimeSeconds >= 3600
|
|
107
|
-
? Math.round(uptimeSeconds / 3600) + "h"
|
|
108
|
-
: uptimeSeconds + "s";
|
|
72
|
+
// ── Prompt ──
|
|
109
73
|
|
|
74
|
+
function buildPrompt(cmdId, logs, uptimeSeconds, restartCount) {
|
|
75
|
+
const uptime = uptimeSeconds >= 3600
|
|
76
|
+
? Math.round(uptimeSeconds / 3600) + "h"
|
|
77
|
+
: uptimeSeconds + "s";
|
|
110
78
|
return `你是 TikTok 数据采集工具的运维助手。以下是进程 "${cmdId}" 的最近 10 分钟日志,请分析是否有异常。
|
|
111
79
|
|
|
112
80
|
进程信息:
|
|
@@ -127,38 +95,20 @@ ${logs || "(无日志)"}
|
|
|
127
95
|
- 每个 suggestions 是简洁可操作的建议,不超过 3 条`;
|
|
128
96
|
}
|
|
129
97
|
|
|
130
|
-
// ──
|
|
98
|
+
// ── 解析 ──
|
|
131
99
|
|
|
132
100
|
function parseResponse(content) {
|
|
133
101
|
try {
|
|
134
102
|
const parsed = JSON.parse(content);
|
|
135
|
-
return {
|
|
136
|
-
severity: parsed.severity || "ok",
|
|
137
|
-
summary: parsed.summary || "",
|
|
138
|
-
suggestions: Array.isArray(parsed.suggestions)
|
|
139
|
-
? parsed.suggestions.slice(0, 3)
|
|
140
|
-
: [],
|
|
141
|
-
};
|
|
103
|
+
return { severity: parsed.severity || "ok", summary: parsed.summary || "", suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions.slice(0, 3) : [] };
|
|
142
104
|
} catch {
|
|
143
|
-
// 尝试提取 JSON 块
|
|
144
105
|
const match = content.match(/\{[\s\S]*\}/);
|
|
145
106
|
if (match) {
|
|
146
107
|
try {
|
|
147
108
|
const parsed = JSON.parse(match[0]);
|
|
148
|
-
return {
|
|
149
|
-
severity: parsed.severity || "ok",
|
|
150
|
-
summary: parsed.summary || "",
|
|
151
|
-
suggestions: Array.isArray(parsed.suggestions)
|
|
152
|
-
? parsed.suggestions.slice(0, 3)
|
|
153
|
-
: [],
|
|
154
|
-
};
|
|
109
|
+
return { severity: parsed.severity || "ok", summary: parsed.summary || "", suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions.slice(0, 3) : [] };
|
|
155
110
|
} catch {}
|
|
156
111
|
}
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
severity: "warn",
|
|
160
|
-
summary: content.substring(0, 100),
|
|
161
|
-
suggestions: [],
|
|
162
|
-
};
|
|
112
|
+
return { severity: "warn", summary: content.substring(0, 100), suggestions: [] };
|
|
163
113
|
}
|
|
164
114
|
}
|
|
@@ -114,6 +114,19 @@ function getProcessStats(pid) {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// Windows: 从 cmd.exe 往下找真正的 node.exe PID(shell:true 时 cmd.exe 是中间层)
|
|
118
|
+
function getChildPid(parentPid) {
|
|
119
|
+
try {
|
|
120
|
+
const out = execSync(
|
|
121
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter 'ParentProcessId=${parentPid}' | Where-Object { $_.Name -notlike 'conhost*' } | Select-Object -First 1 -ExpandProperty ProcessId"`,
|
|
122
|
+
{ encoding: "utf-8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"] },
|
|
123
|
+
).trim();
|
|
124
|
+
return parseInt(out, 10) || 0;
|
|
125
|
+
} catch {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
117
130
|
export class ProcessManager extends EventEmitter {
|
|
118
131
|
constructor() {
|
|
119
132
|
super();
|
|
@@ -158,6 +171,19 @@ export class ProcessManager extends EventEmitter {
|
|
|
158
171
|
stdio: ["ignore", "pipe", "pipe"],
|
|
159
172
|
detached: false,
|
|
160
173
|
});
|
|
174
|
+
|
|
175
|
+
// Windows: shell:true → child.pid 是 cmd.exe,延迟获取真实 node.exe PID(用于内存读取)
|
|
176
|
+
if (IS_WIN) {
|
|
177
|
+
setTimeout(() => {
|
|
178
|
+
const realPid = getChildPid(child.pid);
|
|
179
|
+
if (realPid > 0) {
|
|
180
|
+
const info = this.processes.get(id);
|
|
181
|
+
if (info && info.child === child) {
|
|
182
|
+
info._realPid = realPid;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}, 3000);
|
|
186
|
+
}
|
|
161
187
|
} catch (e) {
|
|
162
188
|
console.error(`[watchdog] 启动命令 ${id} 失败: ${e.message}`);
|
|
163
189
|
this.processes.set(id, {
|
|
@@ -295,7 +321,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
295
321
|
const result = [];
|
|
296
322
|
for (const [cmdId, info] of this.processes) {
|
|
297
323
|
if (info.stopped) continue;
|
|
298
|
-
|
|
324
|
+
// Windows: 壳是 cmd.exe,真实业务进程是 node.exe(_realPid)
|
|
325
|
+
const pid = IS_WIN
|
|
326
|
+
? info._realPid || info.child?.pid || 0
|
|
327
|
+
: info.child?.pid || 0;
|
|
299
328
|
const stats = pid ? getProcessStats(pid) : null;
|
|
300
329
|
const uptimeSeconds = Math.round((Date.now() - info.startTime) / 1000);
|
|
301
330
|
|
|
@@ -361,16 +390,16 @@ export class ProcessManager extends EventEmitter {
|
|
|
361
390
|
syncCommands(expectedCmds) {
|
|
362
391
|
if (!Array.isArray(expectedCmds)) return;
|
|
363
392
|
|
|
364
|
-
//
|
|
393
|
+
// 标准化:使用服务端分配的 id,无 id 时用命令行截断兜底
|
|
365
394
|
const normalized = expectedCmds
|
|
366
395
|
.map((c) => {
|
|
367
396
|
if (typeof c === "string")
|
|
368
397
|
c = { cmd: c, maxUptimeSeconds: 0, maxMemoryMB: 0 };
|
|
369
|
-
// 清洗:去除可能混入的控制字符、状态图标等
|
|
370
398
|
const cmdLine = (c.cmd || "").replace(/^[▶\n\r\s]+/, "").trim();
|
|
371
399
|
if (!cmdLine) return null;
|
|
372
400
|
const id =
|
|
373
|
-
|
|
401
|
+
c.id ||
|
|
402
|
+
(cmdLine.length > 60 ? cmdLine.substring(0, 60) + "..." : cmdLine);
|
|
374
403
|
return {
|
|
375
404
|
id,
|
|
376
405
|
name: id,
|
|
@@ -422,7 +451,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
422
451
|
if (info.stopped || !info.child) continue;
|
|
423
452
|
const thresholds = info.thresholds || {};
|
|
424
453
|
const uptimeSeconds = Math.round((now - info.startTime) / 1000);
|
|
425
|
-
|
|
454
|
+
// Windows: 用真实 node.exe PID 读内存
|
|
455
|
+
const pid = IS_WIN ? info._realPid || info.child.pid : info.child.pid;
|
|
426
456
|
|
|
427
457
|
let reason = "";
|
|
428
458
|
// 检查运行时长
|