tt-help-cli-ycl 1.4.56 → 1.4.57
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/cli/watchdog.js +48 -0
- package/src/lib/args.js +38 -0
- package/src/lib/constants.js +10 -0
- package/src/main.js +3 -0
- package/src/npm-main.js +3 -0
- package/src/watch/public/app.js +454 -0
- package/src/watch/public/index.html +21 -0
- package/src/watch/public/style.css +43 -0
- package/src/watch/server.js +22 -0
- package/src/watch/watchdog-server.js +239 -0
- package/src/watchdog/agent.js +219 -0
- package/src/watchdog/process-manager.js +332 -0
- package/src/watchdog/reporter.js +69 -0
- package/src/watchdog/upgrader.js +119 -0
|
@@ -1814,3 +1814,46 @@ td.user-id:hover {
|
|
|
1814
1814
|
.settings-form .btn-submit:hover {
|
|
1815
1815
|
background: #e61944;
|
|
1816
1816
|
}
|
|
1817
|
+
|
|
1818
|
+
/* ── 看门狗页面 ── */
|
|
1819
|
+
|
|
1820
|
+
#watchdogPage {
|
|
1821
|
+
display: none;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
#watchdogPage.active {
|
|
1825
|
+
display: block;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
.watchdog-layout {
|
|
1829
|
+
display: grid;
|
|
1830
|
+
grid-template-columns: 1fr 1fr;
|
|
1831
|
+
gap: 20px;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
@media (max-width: 900px) {
|
|
1835
|
+
.watchdog-layout {
|
|
1836
|
+
grid-template-columns: 1fr;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
.watchdog-clients,
|
|
1841
|
+
.watchdog-commands {
|
|
1842
|
+
background: #1a1a24;
|
|
1843
|
+
border-radius: 12px;
|
|
1844
|
+
padding: 20px;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
.watchdog-clients h3,
|
|
1848
|
+
.watchdog-commands h3 {
|
|
1849
|
+
font-size: 15px;
|
|
1850
|
+
color: #e0e0e0;
|
|
1851
|
+
margin-bottom: 12px;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
.watchdog-clients table th,
|
|
1855
|
+
.watchdog-commands table th {
|
|
1856
|
+
font-size: 11px;
|
|
1857
|
+
color: #888;
|
|
1858
|
+
font-weight: 600;
|
|
1859
|
+
}
|
package/src/watch/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
import { spawn } from "child_process";
|
|
9
9
|
import { createStore } from "./data-store.js";
|
|
10
10
|
import { DEFAULT_TARGET_LOCATIONS } from "../lib/target-locations.js";
|
|
11
|
+
import { tryHandleWatchdogRoute } from "./watchdog-server.js";
|
|
11
12
|
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
|
|
@@ -423,6 +424,27 @@ export function startWatchServer(
|
|
|
423
424
|
store.trackClient(clientId, { ip, port: meta.port, userId, type });
|
|
424
425
|
}
|
|
425
426
|
|
|
427
|
+
// ── 看门狗路由(独立模块,提前拦截) ──
|
|
428
|
+
if (routePath.startsWith("/api/watchdog/")) {
|
|
429
|
+
try {
|
|
430
|
+
let body = {};
|
|
431
|
+
if (req.method === "POST") {
|
|
432
|
+
body = await readBody(req);
|
|
433
|
+
}
|
|
434
|
+
const handled = await tryHandleWatchdogRoute(
|
|
435
|
+
routePath,
|
|
436
|
+
req.method,
|
|
437
|
+
params,
|
|
438
|
+
body,
|
|
439
|
+
res,
|
|
440
|
+
);
|
|
441
|
+
if (handled) return;
|
|
442
|
+
} catch (e) {
|
|
443
|
+
sendJSON(res, 400, { error: e.message });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
426
448
|
if (req.method === "POST" && routePath === "/api/users") {
|
|
427
449
|
try {
|
|
428
450
|
const { usernames, sources, guessedLocation } = await readBody(req);
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 看门狗服务端模块 — 客户端管理 + 每客户端独立命令列表
|
|
3
|
+
*
|
|
4
|
+
* 独立模块。每条命令是完整命令行字符串(如 "tt-help explore --location DE"),
|
|
5
|
+
* 按 userId(~/.tt-help.json 的 userId 字段)分组存储。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
|
|
12
|
+
const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
|
|
13
|
+
const WATCHDOG_CONFIG_PATH = join(homeDir, ".tt-help-watchdog.json");
|
|
14
|
+
|
|
15
|
+
// ── 内存数据结构 ──
|
|
16
|
+
const clients = new Map();
|
|
17
|
+
const CLIENT_TIMEOUT_MS = 60 * 1000;
|
|
18
|
+
const CLIENT_CLEANUP_MS = 24 * 60 * 60 * 1000; // 离线超过 24h 清理
|
|
19
|
+
|
|
20
|
+
// ── 配置文件读写 ──
|
|
21
|
+
|
|
22
|
+
function loadConfig() {
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(WATCHDOG_CONFIG_PATH))
|
|
25
|
+
return JSON.parse(readFileSync(WATCHDOG_CONFIG_PATH, "utf-8"));
|
|
26
|
+
} catch {}
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function saveConfig(cfg) {
|
|
31
|
+
try {
|
|
32
|
+
writeFileSync(WATCHDOG_CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf-8");
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error(`[watchdog] 保存失败: ${e.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── 客户端管理 ──
|
|
39
|
+
|
|
40
|
+
function registerClient(clientId, info) {
|
|
41
|
+
const existing = clients.get(clientId);
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
if (existing) {
|
|
44
|
+
Object.assign(existing, info, { lastHeartbeat: now });
|
|
45
|
+
} else {
|
|
46
|
+
clients.set(clientId, { ...info, lastHeartbeat: now, registeredAt: now });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getOnlineClients() {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
// 清理超过 24h 未心跳的死客户端
|
|
53
|
+
for (const [clientId, info] of clients) {
|
|
54
|
+
if (now - info.lastHeartbeat > CLIENT_CLEANUP_MS) {
|
|
55
|
+
clients.delete(clientId);
|
|
56
|
+
console.error(`[watchdog] 清理离线客户端: ${clientId}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const result = [];
|
|
60
|
+
for (const [clientId, info] of clients) {
|
|
61
|
+
const online = now - info.lastHeartbeat < CLIENT_TIMEOUT_MS;
|
|
62
|
+
result.push({
|
|
63
|
+
clientId,
|
|
64
|
+
hostname: info.hostname || "unknown",
|
|
65
|
+
ip: info.ip || "",
|
|
66
|
+
ttHelpVersion: info.ttHelpVersion || "unknown",
|
|
67
|
+
osInfo: info.osInfo || {},
|
|
68
|
+
config: info.config || {},
|
|
69
|
+
processes: info.processes || [],
|
|
70
|
+
lastHeartbeat: info.lastHeartbeat,
|
|
71
|
+
registeredAt: info.registeredAt,
|
|
72
|
+
online,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// 按 hostname 排序
|
|
76
|
+
result.sort((a, b) => a.hostname.localeCompare(b.hostname));
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── 每客户端命令管理 ──
|
|
81
|
+
// 命令格式:字符串 "tt-help explore" 或对象 {cmd, enabled, maxUptimeSeconds, maxMemoryMB}
|
|
82
|
+
// 心跳返回已启用的命令对象(含阈值)
|
|
83
|
+
|
|
84
|
+
function normalizeCmd(c) {
|
|
85
|
+
if (typeof c === "string")
|
|
86
|
+
return { cmd: c, enabled: true, maxUptimeSeconds: 0, maxMemoryMB: 0 };
|
|
87
|
+
return {
|
|
88
|
+
cmd: c.cmd || "",
|
|
89
|
+
enabled: c.enabled !== false,
|
|
90
|
+
maxUptimeSeconds: c.maxUptimeSeconds || 0,
|
|
91
|
+
maxMemoryMB: c.maxMemoryMB || 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getClientCommands(userId) {
|
|
96
|
+
if (!userId) return [];
|
|
97
|
+
const cfg = loadConfig();
|
|
98
|
+
const raw = (cfg.clientCommands && cfg.clientCommands[userId]) || [];
|
|
99
|
+
return raw
|
|
100
|
+
.map(normalizeCmd)
|
|
101
|
+
.filter((c) => c.enabled && c.cmd)
|
|
102
|
+
.map((c) => ({
|
|
103
|
+
cmd: c.cmd,
|
|
104
|
+
maxUptimeSeconds: c.maxUptimeSeconds,
|
|
105
|
+
maxMemoryMB: c.maxMemoryMB,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function setClientCommands(userId, commands) {
|
|
110
|
+
if (!Array.isArray(commands)) return;
|
|
111
|
+
const cfg = loadConfig();
|
|
112
|
+
if (!cfg.clientCommands) cfg.clientCommands = {};
|
|
113
|
+
cfg.clientCommands[userId] = commands;
|
|
114
|
+
saveConfig(cfg);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 返回原始数据(含 enabled 状态),供仪表盘使用
|
|
118
|
+
function getAllClientCommandsRaw() {
|
|
119
|
+
const cfg = loadConfig();
|
|
120
|
+
return cfg.clientCommands || {};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── 工具函数 ──
|
|
124
|
+
|
|
125
|
+
function readBody(req) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
let body = "";
|
|
128
|
+
req.on("data", (chunk) => (body += chunk));
|
|
129
|
+
req.on("end", () => {
|
|
130
|
+
try {
|
|
131
|
+
resolve(body ? JSON.parse(body) : {});
|
|
132
|
+
} catch (e) {
|
|
133
|
+
reject(e);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
req.on("error", reject);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseQuery(urlString = "/") {
|
|
141
|
+
const url = new URL(urlString, "http://127.0.0.1");
|
|
142
|
+
return {
|
|
143
|
+
path: url.pathname,
|
|
144
|
+
params: Object.fromEntries(url.searchParams.entries()),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sendJSON(res, code, data) {
|
|
149
|
+
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
|
|
150
|
+
res.end(JSON.stringify(data));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── 路由处理器 ──
|
|
154
|
+
|
|
155
|
+
async function handleHeartbeat(params, body, res) {
|
|
156
|
+
const clientId = params.clientId;
|
|
157
|
+
if (!clientId) {
|
|
158
|
+
sendJSON(res, 400, { error: "缺少 clientId" });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
registerClient(clientId, {
|
|
163
|
+
hostname: body.hostname || "unknown",
|
|
164
|
+
ip: body.ip || "",
|
|
165
|
+
ttHelpVersion: body.ttHelpVersion || "unknown",
|
|
166
|
+
osInfo: body.osInfo || {},
|
|
167
|
+
config: body.config || {},
|
|
168
|
+
processes: body.processes || [],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// 按 userId 返回该客户端的专属命令列表
|
|
172
|
+
const userId = (body.config && body.config.userId) || "";
|
|
173
|
+
const commands = getClientCommands(userId);
|
|
174
|
+
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
175
|
+
console.error(
|
|
176
|
+
`[watchdog ${ts}] 心跳: ${clientId} (${body.hostname || "?"}, user=${userId || "?"}) ${body.processes?.length || 0}进程, 返回${commands.length}条命令`,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
sendJSON(res, 200, {
|
|
180
|
+
ok: true,
|
|
181
|
+
commands,
|
|
182
|
+
serverTime: Date.now(),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function handleGetClients(params, body, res) {
|
|
187
|
+
const onlineClients = getOnlineClients();
|
|
188
|
+
const allCmds = getAllClientCommandsRaw();
|
|
189
|
+
sendJSON(res, 200, {
|
|
190
|
+
clients: onlineClients,
|
|
191
|
+
clientCommands: allCmds,
|
|
192
|
+
configPath: WATCHDOG_CONFIG_PATH,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function handleSetClientCommands(params, body, res) {
|
|
197
|
+
const { userId, commands } = body;
|
|
198
|
+
if (!userId) {
|
|
199
|
+
sendJSON(res, 400, { error: "缺少 userId" });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!Array.isArray(commands)) {
|
|
203
|
+
sendJSON(res, 400, { error: "commands 必须是数组" });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
setClientCommands(userId, commands);
|
|
207
|
+
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
208
|
+
console.error(`[watchdog ${ts}] ${userId} 命令已更新: ${commands.length} 条`);
|
|
209
|
+
sendJSON(res, 200, { ok: true, count: commands.length });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── 公开 ──
|
|
213
|
+
|
|
214
|
+
export function registerWatchdogRoutes(httpServer) {
|
|
215
|
+
console.error("[watchdog] 看门狗服务端模块已加载");
|
|
216
|
+
console.error(`[watchdog] 配置文件: ${WATCHDOG_CONFIG_PATH}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function tryHandleWatchdogRoute(
|
|
220
|
+
routePath,
|
|
221
|
+
method,
|
|
222
|
+
params,
|
|
223
|
+
body,
|
|
224
|
+
res,
|
|
225
|
+
) {
|
|
226
|
+
if (method === "POST" && routePath === "/api/watchdog/heartbeat") {
|
|
227
|
+
await handleHeartbeat(params, body, res);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
if (method === "GET" && routePath === "/api/watchdog/clients") {
|
|
231
|
+
await handleGetClients(params, body, res);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (method === "POST" && routePath === "/api/watchdog/client-commands") {
|
|
235
|
+
await handleSetClientCommands(params, body, res);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 看门狗主代理 — 心跳循环 + 命令同步 + 进程编排
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 定时向服务端发送心跳(携带系统信息 + 进程状态)
|
|
6
|
+
* 2. 从服务端拉取命令列表,同步本地进程
|
|
7
|
+
* 3. 处理信号优雅退出
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getSystemInfo, getClientConfig, getLocalIP } from "./reporter.js";
|
|
11
|
+
import { ProcessManager } from "./process-manager.js";
|
|
12
|
+
import {
|
|
13
|
+
getVersion,
|
|
14
|
+
performUpgrade,
|
|
15
|
+
isGlobalInstall,
|
|
16
|
+
startUpgradeChecker,
|
|
17
|
+
} from "./upgrader.js";
|
|
18
|
+
|
|
19
|
+
export class WatchdogAgent {
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} opts
|
|
22
|
+
* @param {string} opts.serverUrl - 服务端地址
|
|
23
|
+
* @param {string} opts.clientId - 客户端标识
|
|
24
|
+
* @param {number} opts.intervalMs - 心跳间隔(毫秒)
|
|
25
|
+
*/
|
|
26
|
+
constructor({ serverUrl, clientId, intervalMs = 15000 }) {
|
|
27
|
+
this.serverUrl = serverUrl.replace(/\/+$/, "");
|
|
28
|
+
this.clientId = clientId;
|
|
29
|
+
this.intervalMs = intervalMs;
|
|
30
|
+
this.running = false;
|
|
31
|
+
this.heartbeatTimer = null;
|
|
32
|
+
this.thresholdTimer = null;
|
|
33
|
+
this.processManager = new ProcessManager();
|
|
34
|
+
this.consecutiveFailures = 0;
|
|
35
|
+
this.maxFailures = 10;
|
|
36
|
+
this.upgradeChecker = null;
|
|
37
|
+
this.ttHelpVersion = getVersion();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 启动看门狗
|
|
42
|
+
*/
|
|
43
|
+
async start() {
|
|
44
|
+
if (this.running) return;
|
|
45
|
+
this.running = true;
|
|
46
|
+
|
|
47
|
+
console.error(
|
|
48
|
+
`[watchdog] 启动 — 客户端: ${this.clientId}, 版本: ${this.ttHelpVersion}, 间隔: ${this.intervalMs / 1000}s`,
|
|
49
|
+
);
|
|
50
|
+
console.error(
|
|
51
|
+
`[watchdog] 升级: ${isGlobalInstall() ? "已启用" : "跳过(源码模式)"}`,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// 注册信号处理
|
|
55
|
+
this._setupSignalHandlers();
|
|
56
|
+
|
|
57
|
+
// 首次立即发送心跳
|
|
58
|
+
await this._sendHeartbeat();
|
|
59
|
+
|
|
60
|
+
// 定时心跳
|
|
61
|
+
this.heartbeatTimer = setInterval(() => {
|
|
62
|
+
this._sendHeartbeat();
|
|
63
|
+
}, this.intervalMs);
|
|
64
|
+
|
|
65
|
+
// 独立阈值检查(每 5s,不与心跳耦合)
|
|
66
|
+
this.thresholdTimer = setInterval(() => {
|
|
67
|
+
this.processManager.checkThresholds();
|
|
68
|
+
}, 5000);
|
|
69
|
+
|
|
70
|
+
// 自动升级检测(仅全局安装模式)
|
|
71
|
+
this._startUpgradeChecker();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 停止看门狗
|
|
76
|
+
*/
|
|
77
|
+
async stop() {
|
|
78
|
+
this.running = false;
|
|
79
|
+
if (this.heartbeatTimer) {
|
|
80
|
+
clearInterval(this.heartbeatTimer);
|
|
81
|
+
this.heartbeatTimer = null;
|
|
82
|
+
}
|
|
83
|
+
if (this.thresholdTimer) {
|
|
84
|
+
clearInterval(this.thresholdTimer);
|
|
85
|
+
this.thresholdTimer = null;
|
|
86
|
+
}
|
|
87
|
+
if (this.upgradeChecker) {
|
|
88
|
+
this.upgradeChecker.stop();
|
|
89
|
+
this.upgradeChecker = null;
|
|
90
|
+
}
|
|
91
|
+
this.processManager.stopAll();
|
|
92
|
+
console.error("[watchdog] 看门狗已停止");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 构建心跳请求体 */
|
|
96
|
+
_buildHeartbeatBody() {
|
|
97
|
+
const sys = getSystemInfo();
|
|
98
|
+
return {
|
|
99
|
+
clientId: this.clientId,
|
|
100
|
+
hostname: sys.hostname,
|
|
101
|
+
ip: getLocalIP(),
|
|
102
|
+
ttHelpVersion: this.ttHelpVersion,
|
|
103
|
+
osInfo: {
|
|
104
|
+
platform: sys.platform,
|
|
105
|
+
release: sys.release,
|
|
106
|
+
arch: sys.arch,
|
|
107
|
+
cpus: sys.cpus,
|
|
108
|
+
totalMemoryMB: sys.totalMemoryMB,
|
|
109
|
+
freeMemoryMB: sys.freeMemoryMB,
|
|
110
|
+
nodeVersion: sys.nodeVersion,
|
|
111
|
+
uptime: sys.uptime,
|
|
112
|
+
loadavg: sys.loadavg,
|
|
113
|
+
},
|
|
114
|
+
config: getClientConfig(),
|
|
115
|
+
processes: this.processManager.getProcessesStatus(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 发送心跳
|
|
121
|
+
*/
|
|
122
|
+
async _sendHeartbeat() {
|
|
123
|
+
if (!this.running) return;
|
|
124
|
+
|
|
125
|
+
const body = this._buildHeartbeatBody();
|
|
126
|
+
const procCount = body.processes.length;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const resp = await fetch(
|
|
130
|
+
`${this.serverUrl}/api/watchdog/heartbeat?clientId=${encodeURIComponent(this.clientId)}`,
|
|
131
|
+
{
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "Content-Type": "application/json" },
|
|
134
|
+
body: JSON.stringify(body),
|
|
135
|
+
// Node 18+ 支持 fetch,超时用 AbortController
|
|
136
|
+
signal: AbortSignal.timeout(10000),
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (!resp.ok) {
|
|
141
|
+
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const data = await resp.json();
|
|
145
|
+
this.consecutiveFailures = 0;
|
|
146
|
+
|
|
147
|
+
// 同步命令
|
|
148
|
+
if (Array.isArray(data.commands)) {
|
|
149
|
+
const prev = this.processManager.runningCount;
|
|
150
|
+
this.processManager.syncCommands(data.commands);
|
|
151
|
+
const now = this.processManager.runningCount;
|
|
152
|
+
if (prev !== now) {
|
|
153
|
+
console.error(
|
|
154
|
+
`[watchdog] 进程 ${prev} → ${now} (${data.commands.length}条命令)`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
160
|
+
console.error(
|
|
161
|
+
`[watchdog ${ts}] 心跳 OK (${procCount}进程, ${data.commands?.length || 0}条)`,
|
|
162
|
+
);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
this.consecutiveFailures++;
|
|
165
|
+
console.error(
|
|
166
|
+
`[watchdog] 心跳失败 (${this.consecutiveFailures}/${this.maxFailures}): ${e.message}`,
|
|
167
|
+
);
|
|
168
|
+
if (this.consecutiveFailures >= this.maxFailures) {
|
|
169
|
+
console.error("[watchdog] 连续心跳失败次数过多,看门狗退出");
|
|
170
|
+
await this.stop();
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 启动自动升级检测(每 10 分钟)
|
|
178
|
+
* 仅全局安装模式下启用
|
|
179
|
+
*/
|
|
180
|
+
_startUpgradeChecker() {
|
|
181
|
+
if (!isGlobalInstall()) return;
|
|
182
|
+
|
|
183
|
+
this.upgradeChecker = startUpgradeChecker({
|
|
184
|
+
onUpdateAvailable: async ({ current, latest }) => {
|
|
185
|
+
console.error(`[upgrade] ${current} → ${latest}`);
|
|
186
|
+
this.processManager.stopAll();
|
|
187
|
+
|
|
188
|
+
const result = await performUpgrade();
|
|
189
|
+
if (result.success) {
|
|
190
|
+
this.ttHelpVersion = latest;
|
|
191
|
+
console.error(`[upgrade] 升级成功`);
|
|
192
|
+
} else {
|
|
193
|
+
console.error(`[upgrade] 失败: ${result.output.substring(0, 200)}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 立即心跳(同步命令,重启进程)
|
|
197
|
+
await this._sendHeartbeat();
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 信号处理
|
|
204
|
+
*/
|
|
205
|
+
_setupSignalHandlers() {
|
|
206
|
+
let shuttingDown = false;
|
|
207
|
+
|
|
208
|
+
const gracefulShutdown = async (signal) => {
|
|
209
|
+
if (shuttingDown) return;
|
|
210
|
+
shuttingDown = true;
|
|
211
|
+
console.error(`\n[watchdog] 收到 ${signal},正在退出...`);
|
|
212
|
+
await this.stop();
|
|
213
|
+
process.exit(0);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
process.once("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
217
|
+
process.once("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
218
|
+
}
|
|
219
|
+
}
|