tt-help-cli-ycl 1.4.71 → 1.4.73
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 +24 -29
- package/src/watch/watchdog-server.js +67 -4
- package/src/watchdog/agent.js +22 -34
- package/src/watchdog/llm-analyzer.js +78 -51
- package/src/watchdog/process-manager.js +35 -5
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,
|
|
@@ -3594,43 +3596,32 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3594
3596
|
);
|
|
3595
3597
|
}
|
|
3596
3598
|
|
|
3597
|
-
//
|
|
3599
|
+
// 匹配进程到命令(按唯一 ID 精确匹配)
|
|
3598
3600
|
function matchProcsToCmds(myCmds, procs) {
|
|
3599
3601
|
var matched = {}; // cmdIdx -> proc
|
|
3600
3602
|
var unmatchedProcs = [];
|
|
3603
|
+
// 建 ID → 命令索引 映射
|
|
3604
|
+
var idToIdx = {};
|
|
3605
|
+
myCmds.forEach(function (cmd, i) {
|
|
3606
|
+
var c = cmdObj(cmd);
|
|
3607
|
+
if (c.id) idToIdx[c.id] = i;
|
|
3608
|
+
});
|
|
3601
3609
|
procs.forEach(function (p) {
|
|
3602
3610
|
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
|
-
}
|
|
3611
|
+
if (p.cmdId && idToIdx[p.cmdId] !== undefined) {
|
|
3612
|
+
matched[idToIdx[p.cmdId]] = p;
|
|
3613
|
+
found = true;
|
|
3616
3614
|
}
|
|
3617
3615
|
if (!found) unmatchedProcs.push(p);
|
|
3618
3616
|
});
|
|
3619
3617
|
return { matched: matched, unmatched: unmatchedProcs };
|
|
3620
3618
|
}
|
|
3621
3619
|
|
|
3622
|
-
//
|
|
3623
|
-
function findAnalysis(llmAnalysis,
|
|
3624
|
-
if (!llmAnalysis || !
|
|
3620
|
+
// 按命令 ID 匹配 LLM 分析结果
|
|
3621
|
+
function findAnalysis(llmAnalysis, cmdId) {
|
|
3622
|
+
if (!llmAnalysis || !cmdId) return null;
|
|
3625
3623
|
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
|
-
}
|
|
3624
|
+
if (llmAnalysis[i].cmdId === cmdId) return llmAnalysis[i];
|
|
3634
3625
|
}
|
|
3635
3626
|
return null;
|
|
3636
3627
|
}
|
|
@@ -3848,7 +3839,7 @@ function renderCmdRows(myCmds, userId, procs, isOnline, llmAnalysis) {
|
|
|
3848
3839
|
);
|
|
3849
3840
|
|
|
3850
3841
|
// AI 分析结果
|
|
3851
|
-
var ai = findAnalysis(llmAnalysis, c.
|
|
3842
|
+
var ai = findAnalysis(llmAnalysis, c.id);
|
|
3852
3843
|
if (ai) {
|
|
3853
3844
|
var sevColor =
|
|
3854
3845
|
ai.severity === "error"
|
|
@@ -3956,19 +3947,22 @@ function wdModalSave() {
|
|
|
3956
3947
|
var myCmds = (allCmds[_wdModalUserId] || []).slice();
|
|
3957
3948
|
|
|
3958
3949
|
if (_wdModalEditIdx >= 0) {
|
|
3959
|
-
//
|
|
3950
|
+
// 编辑模式:保留已有 ID
|
|
3960
3951
|
if (_wdModalEditIdx < myCmds.length) {
|
|
3952
|
+
var oldObj = cmdObj(myCmds[_wdModalEditIdx]);
|
|
3961
3953
|
myCmds[_wdModalEditIdx] = {
|
|
3954
|
+
id: oldObj.id,
|
|
3962
3955
|
name: name,
|
|
3963
3956
|
cmd: cmd,
|
|
3964
|
-
enabled:
|
|
3957
|
+
enabled: oldObj.enabled,
|
|
3965
3958
|
maxUptimeSeconds: maxUp,
|
|
3966
3959
|
maxMemoryMB: maxMem,
|
|
3967
3960
|
};
|
|
3968
3961
|
}
|
|
3969
3962
|
} else {
|
|
3970
|
-
//
|
|
3963
|
+
// 添加模式:生成新 ID
|
|
3971
3964
|
myCmds.push({
|
|
3965
|
+
id: String(Date.now()),
|
|
3972
3966
|
name: name,
|
|
3973
3967
|
cmd: cmd,
|
|
3974
3968
|
enabled: true,
|
|
@@ -4059,6 +4053,7 @@ function wdToggleClientCmd(userId, idx) {
|
|
|
4059
4053
|
var cmd = myCmds[idx];
|
|
4060
4054
|
if (typeof cmd === "string") {
|
|
4061
4055
|
myCmds[idx] = {
|
|
4056
|
+
id: String(Date.now()),
|
|
4062
4057
|
cmd: cmd,
|
|
4063
4058
|
enabled: false,
|
|
4064
4059
|
maxUptimeSeconds: 0,
|
|
@@ -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,10 +116,21 @@ 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 = {};
|
|
@@ -196,6 +210,7 @@ async function handleHeartbeat(params, body, res) {
|
|
|
196
210
|
// 按 userId 返回该客户端的专属命令列表
|
|
197
211
|
const userId = (body.config && body.config.userId) || "";
|
|
198
212
|
const commands = getClientCommands(userId);
|
|
213
|
+
|
|
199
214
|
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
200
215
|
console.error(
|
|
201
216
|
`[watchdog ${ts}] 心跳: ${clientId} (${body.hostname || "?"}, user=${userId || "?"}) ${body.processes?.length || 0}进程, 返回${commands.length}条命令`,
|
|
@@ -247,11 +262,55 @@ async function handleClientConfig(params, body, res) {
|
|
|
247
262
|
sendJSON(res, 200, { ok: true });
|
|
248
263
|
}
|
|
249
264
|
|
|
265
|
+
async function handleAnalyzeLogs(params, body, res) {
|
|
266
|
+
const { processLogs } = body;
|
|
267
|
+
if (!processLogs || !Array.isArray(processLogs) || processLogs.length === 0) {
|
|
268
|
+
sendJSON(res, 200, { llmAnalysis: [] });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const { analyzeProcessLogs } = await import("../watchdog/llm-analyzer.js");
|
|
273
|
+
const { llmFetch } = await import("./llm-config.js");
|
|
274
|
+
console.error(`[watchdog] LLM 分析 ${processLogs.length} 个进程日志...`);
|
|
275
|
+
const llmAnalysis = await analyzeProcessLogs(processLogs, llmFetch);
|
|
276
|
+
console.error(
|
|
277
|
+
`[watchdog] LLM 分析完成: ${llmAnalysis.map((r) => r.severity).join(",")}`,
|
|
278
|
+
);
|
|
279
|
+
sendJSON(res, 200, { llmAnalysis });
|
|
280
|
+
} catch (e) {
|
|
281
|
+
console.error(`[watchdog] LLM 分析失败: ${e.message}`);
|
|
282
|
+
sendJSON(res, 500, { error: e.message });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
250
286
|
// ── 公开 ──
|
|
251
287
|
|
|
252
288
|
export function registerWatchdogRoutes(httpServer) {
|
|
253
289
|
console.error("[watchdog] 看门狗服务端模块已加载");
|
|
254
290
|
console.error(`[watchdog] 配置文件: ${WATCHDOG_CONFIG_PATH}`);
|
|
291
|
+
// 自动迁移:为已有命令补充唯一 ID
|
|
292
|
+
migrateCommandIds();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 为现有命令中未分配 ID 的自动补充 ID
|
|
296
|
+
function migrateCommandIds() {
|
|
297
|
+
const cfg = loadConfig();
|
|
298
|
+
if (!cfg.clientCommands) return;
|
|
299
|
+
let migrated = 0;
|
|
300
|
+
for (const userId of Object.keys(cfg.clientCommands)) {
|
|
301
|
+
const cmds = cfg.clientCommands[userId];
|
|
302
|
+
if (!Array.isArray(cmds)) continue;
|
|
303
|
+
for (const c of cmds) {
|
|
304
|
+
if (typeof c === "object" && !c.id) {
|
|
305
|
+
c.id = String(Date.now() + migrated);
|
|
306
|
+
migrated++;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (migrated > 0) {
|
|
311
|
+
saveConfig(cfg);
|
|
312
|
+
console.error(`[watchdog] 已为 ${migrated} 条旧命令补充 ID`);
|
|
313
|
+
}
|
|
255
314
|
}
|
|
256
315
|
|
|
257
316
|
export async function tryHandleWatchdogRoute(
|
|
@@ -277,5 +336,9 @@ export async function tryHandleWatchdogRoute(
|
|
|
277
336
|
await handleClientConfig(params, body, res);
|
|
278
337
|
return true;
|
|
279
338
|
}
|
|
339
|
+
if (method === "POST" && routePath === "/api/watchdog/analyze-logs") {
|
|
340
|
+
await handleAnalyzeLogs(params, body, res);
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
280
343
|
return false;
|
|
281
344
|
}
|
package/src/watchdog/agent.js
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
isGlobalInstall,
|
|
16
16
|
startUpgradeChecker,
|
|
17
17
|
} from "./upgrader.js";
|
|
18
|
-
import { analyzeProcessLogs } from "./llm-analyzer.js";
|
|
19
18
|
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
20
19
|
import { join } from "path";
|
|
21
20
|
import { homedir } from "os";
|
|
@@ -72,12 +71,12 @@ export class WatchdogAgent {
|
|
|
72
71
|
this.heartbeatTimer = null;
|
|
73
72
|
this.thresholdTimer = null;
|
|
74
73
|
this.llmAnalyzerTimer = null;
|
|
75
|
-
this._llmResults = []; // 最近一次 LLM 分析结果
|
|
76
74
|
this.processManager = new ProcessManager();
|
|
77
75
|
this.consecutiveFailures = 0;
|
|
78
76
|
this.maxFailures = 10;
|
|
79
77
|
this.upgradeChecker = null;
|
|
80
78
|
this.ttHelpVersion = getVersion();
|
|
79
|
+
this._llmResults = []; // 服务端返回的 LLM 分析结果
|
|
81
80
|
this._heartbeatSeq = 0; // 心跳序号
|
|
82
81
|
this._lastProcCount = -1; // 上次进程数
|
|
83
82
|
this._lastCmdCount = -1; // 上次命令数
|
|
@@ -113,7 +112,7 @@ export class WatchdogAgent {
|
|
|
113
112
|
this.processManager.checkThresholds();
|
|
114
113
|
}, 5000);
|
|
115
114
|
|
|
116
|
-
// LLM 日志分析(每 10
|
|
115
|
+
// LLM 日志分析(每 10 分钟调服务端接口)
|
|
117
116
|
this._startLlmAnalyzer();
|
|
118
117
|
|
|
119
118
|
// 自动升级检测(仅全局安装模式)
|
|
@@ -242,46 +241,35 @@ export class WatchdogAgent {
|
|
|
242
241
|
}
|
|
243
242
|
|
|
244
243
|
/**
|
|
245
|
-
* 启动 LLM 日志分析定时器(每 10
|
|
244
|
+
* 启动 LLM 日志分析定时器(每 10 分钟调服务端接口)
|
|
246
245
|
*/
|
|
247
246
|
_startLlmAnalyzer() {
|
|
248
|
-
// 首次 60s 后执行(等进程启动稳定),之后每 10 分钟
|
|
249
247
|
const run = async () => {
|
|
250
248
|
try {
|
|
251
|
-
console.error("[llm-analyzer] 定时触发...");
|
|
252
249
|
const logs = this.processManager.getProcessesLogs();
|
|
253
|
-
if (logs.length === 0)
|
|
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;
|
|
254
267
|
console.error(
|
|
255
|
-
|
|
268
|
+
`[llm-analyzer] 分析完成: ${this._llmResults.map((r) => r.severity).join(",")}`,
|
|
256
269
|
);
|
|
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
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
271
|
} catch (e) {
|
|
276
|
-
console.error(`[llm-analyzer]
|
|
277
|
-
this._llmResults = [
|
|
278
|
-
{
|
|
279
|
-
cmdId: "_status",
|
|
280
|
-
severity: "error",
|
|
281
|
-
summary: "LLM 分析异常: " + e.message.substring(0, 80),
|
|
282
|
-
suggestions: [],
|
|
283
|
-
},
|
|
284
|
-
];
|
|
272
|
+
console.error(`[llm-analyzer] 调用失败: ${e.message}`);
|
|
285
273
|
}
|
|
286
274
|
};
|
|
287
275
|
|
|
@@ -1,79 +1,106 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LLM 日志分析模块 —
|
|
2
|
+
* LLM 日志分析模块 — 供服务端调用(传入 llmFetch),或客户端独立使用(读环境变量)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 分析结果通过心跳上报到 watch 服务端展示。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// ── 分析入口 ──
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* 对所有进程的日志调用 LLM 分析
|
|
11
|
-
* @param {Array
|
|
12
|
-
* @
|
|
11
|
+
* @param {Array} processLogs — [{cmdId, logs[], uptimeSeconds, restartCount}]
|
|
12
|
+
* @param {Function} [llmFetch] — 可选,服务端传入的 llmFetch;不传则读环境变量
|
|
13
|
+
* @returns {Promise<Array<{cmdId, severity, summary, suggestions}>>}
|
|
13
14
|
*/
|
|
14
|
-
export async function analyzeProcessLogs(processLogs) {
|
|
15
|
+
export async function analyzeProcessLogs(processLogs, llmFetch) {
|
|
15
16
|
if (!processLogs || processLogs.length === 0) return [];
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// 如果没有传入 llmFetch,使用内置的轻量调用(客户端模式)
|
|
19
|
+
if (!llmFetch) {
|
|
20
|
+
llmFetch = await createStandaloneFetch();
|
|
21
|
+
if (!llmFetch) return [];
|
|
22
|
+
}
|
|
21
23
|
|
|
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);
|
|
24
87
|
console.error(
|
|
25
|
-
`[llm-analyzer]
|
|
88
|
+
`[llm-analyzer] ${proc.cmdId.substring(0, 30)} → ${parsed.severity}: ${parsed.summary}`,
|
|
26
89
|
);
|
|
27
|
-
|
|
28
|
-
if (analysis) {
|
|
29
|
-
console.error(
|
|
30
|
-
`[llm-analyzer] ${proc.cmdId.substring(0, 30)} → ${analysis.severity}: ${analysis.summary}`,
|
|
31
|
-
);
|
|
32
|
-
results.push(analysis);
|
|
33
|
-
}
|
|
90
|
+
results.push({ cmdId: proc.cmdId, ...parsed });
|
|
34
91
|
} catch (e) {
|
|
35
92
|
console.error(
|
|
36
|
-
`[llm-analyzer] ${proc.cmdId.substring(0, 30)}
|
|
93
|
+
`[llm-analyzer] ${proc.cmdId.substring(0, 30)} 失败: ${e.message}`,
|
|
37
94
|
);
|
|
38
95
|
}
|
|
39
96
|
}
|
|
40
|
-
|
|
41
97
|
return results;
|
|
42
98
|
} catch (e) {
|
|
43
|
-
console.error(`[llm-analyzer]
|
|
99
|
+
console.error(`[llm-analyzer] 异常: ${e.message}`);
|
|
44
100
|
return [];
|
|
45
101
|
}
|
|
46
102
|
}
|
|
47
103
|
|
|
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
104
|
function buildPrompt(cmdId, logs, uptimeSeconds, restartCount) {
|
|
78
105
|
const uptime =
|
|
79
106
|
uptimeSeconds >= 3600
|
|
@@ -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
|
// 检查运行时长
|