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.
@@ -0,0 +1,332 @@
1
+ /**
2
+ * 进程管理器 — 子进程 spawn、监控、生命周期管理
3
+ *
4
+ * 功能:
5
+ * - 根据命令定义启动子进程
6
+ * - 通过 ps 命令获取进程 RSS 内存和运行时长
7
+ * - 停止/重启子进程
8
+ * - 自动重启崩溃进程(最多 3 次)
9
+ * - 与期望命令列表同步(启动缺失的、停止多余的)
10
+ */
11
+
12
+ import { spawn, execSync } from "child_process";
13
+ import { EventEmitter } from "events";
14
+
15
+ const MAX_RESTART = 3;
16
+
17
+ function getProcessStats(pid) {
18
+ try {
19
+ const stdout = execSync(`ps -p ${pid} -o rss=,etime=`, {
20
+ encoding: "utf-8",
21
+ timeout: 3000,
22
+ }).trim();
23
+ if (!stdout) return null;
24
+ const parts = stdout.split(/\s+/);
25
+ const rssKB = parseInt(parts[0], 10);
26
+ const elapsed = parts.slice(1).join(" ") || "0";
27
+ return {
28
+ memoryMB: isNaN(rssKB) ? 0 : Math.round((rssKB / 1024) * 10) / 10,
29
+ elapsed,
30
+ };
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ export class ProcessManager extends EventEmitter {
37
+ constructor() {
38
+ super();
39
+ /** @type {Map<string, {cmdId:string, cmdDef:object, child:ChildProcess, startTime:number, restartCount:number, thresholds:object, stopped:boolean}>} */
40
+ this.processes = new Map();
41
+ }
42
+
43
+ /**
44
+ * 启动一个命令
45
+ * @param {object} cmdDef - { id, name, command, args[], enabled }
46
+ */
47
+ startCommand(cmdDef) {
48
+ let { id, command, args = [] } = cmdDef;
49
+ if (this.processes.has(id)) {
50
+ // 已有进程在运行(或 stopCommand 还未清理),跳过
51
+ const existing = this.processes.get(id);
52
+ if (existing && !existing.stopped) {
53
+ console.error(`[watchdog] 命令 ${id} 已在运行中,跳过`);
54
+ return;
55
+ }
56
+ }
57
+
58
+ console.error(`[watchdog] 启动命令: ${id}`);
59
+
60
+ let child;
61
+ const startTime = Date.now();
62
+ try {
63
+ // 有显式 args 则直接 spawn,否则用 sh -c 传递完整命令行(保留引号语义)
64
+ if (args.length > 0) {
65
+ child = spawn(command, args, {
66
+ stdio: ["ignore", "pipe", "pipe"],
67
+ detached: false,
68
+ });
69
+ } else {
70
+ child = spawn("sh", ["-c", command], {
71
+ stdio: ["ignore", "pipe", "pipe"],
72
+ detached: false,
73
+ });
74
+ }
75
+ } catch (e) {
76
+ console.error(`[watchdog] 启动命令 ${id} 失败: ${e.message}`);
77
+ this.processes.set(id, {
78
+ cmdId: id,
79
+ cmdDef,
80
+ child: null,
81
+ startTime,
82
+ restartCount: 0,
83
+ stopped: true,
84
+ spawnError: e.message,
85
+ });
86
+ return;
87
+ }
88
+
89
+ child.stdout?.on("data", (data) => {
90
+ // 静默处理,避免子进程输出撑爆缓冲区
91
+ });
92
+
93
+ child.stderr?.on("data", (data) => {
94
+ const msg = data.toString().trim();
95
+ if (msg) {
96
+ console.error(`[${id}] ${msg.substring(0, 200)}`);
97
+ }
98
+ });
99
+
100
+ child.on("exit", (code, signal) => {
101
+ const info = this.processes.get(id);
102
+ const uptimeMs = Date.now() - (info?.startTime || startTime);
103
+ console.error(
104
+ `[watchdog] 命令 ${id} 已退出 (code=${code}, signal=${signal}, 运行${Math.round(uptimeMs / 1000)}s)`,
105
+ );
106
+
107
+ // 仅当 info 存在且 child 匹配时才处理(避免竞态:旧进程退出时新进程已启动)
108
+ if (!info || info.child !== child) {
109
+ // 旧进程退出,info 已被替换,无需处理
110
+ return;
111
+ }
112
+
113
+ // 被显式停止的不重启;正常退出不重启;仅异常退出才尝试重启
114
+ const shouldRestart = !info.stopped && code !== 0;
115
+
116
+ if (shouldRestart) {
117
+ const restartCount = (info?.restartCount || 0) + 1;
118
+ if (restartCount <= MAX_RESTART) {
119
+ console.error(
120
+ `[watchdog] 命令 ${id} 异常退出,自动重启 (${restartCount}/${MAX_RESTART})...`,
121
+ );
122
+ this.processes.delete(id);
123
+ setTimeout(() => this.startCommand(cmdDef), 5000);
124
+ } else {
125
+ console.error(
126
+ `[watchdog] 命令 ${id} 已达最大重启次数 (${MAX_RESTART}),停止重启`,
127
+ );
128
+ this.processes.delete(id);
129
+ this.emit("process-crashed", { cmdId: id, cmdDef, code, signal });
130
+ }
131
+ } else {
132
+ this.processes.delete(id);
133
+ }
134
+ });
135
+
136
+ child.on("error", (err) => {
137
+ console.error(`[watchdog] 命令 ${id} 进程错误: ${err.message}`);
138
+ });
139
+
140
+ this.processes.set(id, {
141
+ cmdId: id,
142
+ cmdDef,
143
+ child,
144
+ startTime,
145
+ restartCount: 0,
146
+ stopped: false,
147
+ });
148
+ }
149
+
150
+ /**
151
+ * 停止一个命令(kill 进程组)
152
+ */
153
+ stopCommand(cmdId) {
154
+ const info = this.processes.get(cmdId);
155
+ if (!info) return;
156
+
157
+ console.error(`[watchdog] 停止命令: ${cmdId}`);
158
+ info.stopped = true;
159
+
160
+ if (info.child) {
161
+ try {
162
+ // 尝试优雅终止
163
+ info.child.kill("SIGTERM");
164
+ // 2 秒后强制 kill
165
+ setTimeout(() => {
166
+ try {
167
+ if (info.child && info.child.exitCode === null) {
168
+ info.child.kill("SIGKILL");
169
+ }
170
+ } catch {}
171
+ }, 2000);
172
+ } catch (e) {
173
+ console.error(`[watchdog] 停止命令 ${cmdId} 失败: ${e.message}`);
174
+ }
175
+ }
176
+
177
+ this.processes.delete(cmdId);
178
+ }
179
+
180
+ /**
181
+ * 获取所有运行中进程的状态
182
+ * @returns {Array<{cmdId:string, pid:number, status:string, memoryMB:number, uptimeSeconds:number, restartCount:number}>}
183
+ */
184
+ getProcessesStatus() {
185
+ const result = [];
186
+ for (const [cmdId, info] of this.processes) {
187
+ if (info.stopped) continue;
188
+ const pid = info.child?.pid || 0;
189
+ const stats = pid ? getProcessStats(pid) : null;
190
+ const uptimeSeconds = Math.round((Date.now() - info.startTime) / 1000);
191
+
192
+ result.push({
193
+ cmdId,
194
+ name: info.cmdDef.name || cmdId,
195
+ pid,
196
+ status: info.child
197
+ ? info.child.exitCode === null
198
+ ? "running"
199
+ : "exited"
200
+ : "error",
201
+ memoryMB: stats?.memoryMB ?? 0,
202
+ uptimeSeconds,
203
+ uptimeDisplay: stats?.elapsed || `${uptimeSeconds}s`,
204
+ restartCount: info.restartCount,
205
+ error: info.spawnError || null,
206
+ });
207
+ }
208
+ return result;
209
+ }
210
+
211
+ /**
212
+ * 与期望命令列表同步:
213
+ * - 启动期望列表中已启用但未运行的命令
214
+ * - 停止正在运行但不在期望列表中的命令
215
+ * @param {Array} expectedCmds - 服务端返回的命令列表,格式 [{cmd, maxUptimeSeconds, maxMemoryMB}]
216
+ */
217
+ syncCommands(expectedCmds) {
218
+ if (!Array.isArray(expectedCmds)) return;
219
+
220
+ // 标准化:{cmd, maxUptimeSeconds, maxMemoryMB} → {id, name, command, args, enabled, thresholds}
221
+ const normalized = expectedCmds
222
+ .map((c) => {
223
+ if (typeof c === "string")
224
+ c = { cmd: c, maxUptimeSeconds: 0, maxMemoryMB: 0 };
225
+ const cmdLine = (c.cmd || "").trim();
226
+ if (!cmdLine) return null;
227
+ const id =
228
+ cmdLine.length > 60 ? cmdLine.substring(0, 60) + "..." : cmdLine;
229
+ return {
230
+ id,
231
+ name: id,
232
+ command: cmdLine,
233
+ args: [],
234
+ enabled: true,
235
+ thresholds: {
236
+ maxUptimeSeconds: c.maxUptimeSeconds || 0,
237
+ maxMemoryMB: c.maxMemoryMB || 0,
238
+ },
239
+ };
240
+ })
241
+ .filter(Boolean);
242
+
243
+ const expectedIds = new Set(normalized.map((c) => c.id));
244
+ const runningIds = new Set(
245
+ [...this.processes.entries()]
246
+ .filter(([, info]) => !info.stopped)
247
+ .map(([id]) => id),
248
+ );
249
+
250
+ // 停止不在期望列表中的
251
+ for (const runningId of runningIds) {
252
+ if (!expectedIds.has(runningId)) {
253
+ this.stopCommand(runningId);
254
+ }
255
+ }
256
+
257
+ // 启动缺失的,并更新已有进程的阈值
258
+ for (const cmd of normalized) {
259
+ if (!runningIds.has(cmd.id)) {
260
+ this.startCommand(cmd);
261
+ } else {
262
+ // 更新阈值(可能被管理员修改了)
263
+ const info = this.processes.get(cmd.id);
264
+ if (info) info.thresholds = cmd.thresholds;
265
+ }
266
+ }
267
+ }
268
+
269
+ /**
270
+ * 检查所有运行进程是否超过阈值(最长时间 / 最大内存)
271
+ */
272
+ checkThresholds() {
273
+ const now = Date.now();
274
+ const toRestart = [];
275
+
276
+ for (const [cmdId, info] of this.processes) {
277
+ if (info.stopped || !info.child) continue;
278
+ const thresholds = info.thresholds || {};
279
+ const uptimeSeconds = Math.round((now - info.startTime) / 1000);
280
+ const pid = info.child.pid;
281
+
282
+ let reason = "";
283
+ // 检查运行时长
284
+ if (
285
+ thresholds.maxUptimeSeconds > 0 &&
286
+ uptimeSeconds >= thresholds.maxUptimeSeconds
287
+ ) {
288
+ reason = `运行 ${uptimeSeconds}s 超过阈值 ${thresholds.maxUptimeSeconds}s`;
289
+ }
290
+ // 检查内存
291
+ if (!reason && thresholds.maxMemoryMB > 0 && pid) {
292
+ const stats = pid ? getProcessStats(pid) : null;
293
+ const memMB = stats?.memoryMB ?? 0;
294
+ if (memMB > thresholds.maxMemoryMB) {
295
+ reason = `内存 ${memMB}MB 超过阈值 ${thresholds.maxMemoryMB}MB`;
296
+ }
297
+ }
298
+
299
+ if (reason) {
300
+ console.error(`[watchdog] 命令 ${cmdId} ${reason},强制重启`);
301
+ toRestart.push({ cmdId, cmdDef: info.cmdDef });
302
+ }
303
+ }
304
+
305
+ // 逐个停止 + 重启
306
+ for (const { cmdId, cmdDef } of toRestart) {
307
+ this.stopCommand(cmdId);
308
+ this.startCommand(cmdDef);
309
+ }
310
+ }
311
+
312
+ /**
313
+ * 停止所有进程
314
+ */
315
+ stopAll() {
316
+ console.error("[watchdog] 正在停止所有子进程...");
317
+ for (const [cmdId] of this.processes) {
318
+ this.stopCommand(cmdId);
319
+ }
320
+ }
321
+
322
+ /**
323
+ * 运行中的进程数
324
+ */
325
+ get runningCount() {
326
+ let count = 0;
327
+ for (const [, info] of this.processes) {
328
+ if (!info.stopped) count++;
329
+ }
330
+ return count;
331
+ }
332
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * 系统信息收集模块
3
+ * 收集机器信息 + tt-help 配置,用于上报到看门狗服务端
4
+ */
5
+
6
+ import os from "os";
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+
11
+ const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
12
+ const CONFIG_PATH = join(homeDir, ".tt-help.json");
13
+
14
+ /**
15
+ * 获取系统信息
16
+ */
17
+ export function getSystemInfo() {
18
+ return {
19
+ hostname: os.hostname(),
20
+ platform: os.platform(),
21
+ release: os.release(),
22
+ arch: os.arch(),
23
+ cpus: os.cpus().length,
24
+ totalMemoryMB: Math.round(os.totalmem() / (1024 * 1024)),
25
+ freeMemoryMB: Math.round(os.freemem() / (1024 * 1024)),
26
+ nodeVersion: process.version,
27
+ uptime: Math.round(os.uptime()),
28
+ loadavg: os.loadavg(),
29
+ };
30
+ }
31
+
32
+ /**
33
+ * 读取 ~/.tt-help.json 的关键配置字段
34
+ */
35
+ export function getClientConfig() {
36
+ try {
37
+ if (existsSync(CONFIG_PATH)) {
38
+ const cfg = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
39
+ return {
40
+ server: cfg.server || "",
41
+ proxy: cfg.proxy || "",
42
+ browser: cfg.browser || "",
43
+ userId: cfg.userId || "",
44
+ maxVideos: cfg.maxVideos,
45
+ maxComments: cfg.maxComments,
46
+ maxFollowing: cfg.maxFollowing,
47
+ maxFollowers: cfg.maxFollowers,
48
+ };
49
+ }
50
+ } catch {
51
+ // 无配置文件
52
+ }
53
+ return {};
54
+ }
55
+
56
+ /**
57
+ * 获取本机 IP 地址
58
+ */
59
+ export function getLocalIP() {
60
+ const ifaces = os.networkInterfaces();
61
+ for (const name of Object.keys(ifaces)) {
62
+ for (const iface of ifaces[name]) {
63
+ if (iface.family === "IPv4" && !iface.internal) {
64
+ return iface.address;
65
+ }
66
+ }
67
+ }
68
+ return "127.0.0.1";
69
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * 自动升级模块 — 检测 tt-help-cli-ycl 新版本并执行升级
3
+ *
4
+ * 仅全局安装模式启用。每 10 分钟查询 npm registry,
5
+ * 发现新版本 → 回调通知 agent → agent 负责停进程 → 升级 → 重启。
6
+ */
7
+
8
+ import { exec } from "child_process";
9
+ import { readFileSync } from "fs";
10
+ import { fileURLToPath } from "url";
11
+ import { dirname, join } from "path";
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const PKG_PATH = join(__dirname, "..", "..", "package.json");
15
+ const PACKAGE = "tt-help-cli-ycl";
16
+ const REGISTRY = "https://registry.npmjs.org";
17
+
18
+ /** 从 package.json 读取当前版本 */
19
+ export function getVersion() {
20
+ try {
21
+ return JSON.parse(readFileSync(PKG_PATH, "utf-8")).version || "unknown";
22
+ } catch {
23
+ return "unknown";
24
+ }
25
+ }
26
+
27
+ /** 是否通过全局 npm 安装运行 */
28
+ export function isGlobalInstall() {
29
+ const entry = process.argv[1] || "";
30
+ return (
31
+ entry.includes(`/node_modules/${PACKAGE}/`) || entry.endsWith("/tt-help")
32
+ );
33
+ }
34
+
35
+ /** 查询 npm registry 最新版本 */
36
+ export async function checkForUpdates() {
37
+ const current = getVersion();
38
+ try {
39
+ const resp = await fetch(`${REGISTRY}/${PACKAGE}/latest`, {
40
+ signal: AbortSignal.timeout(15_000),
41
+ });
42
+ if (!resp.ok)
43
+ return {
44
+ current,
45
+ latest: "?",
46
+ hasUpdate: false,
47
+ error: `HTTP ${resp.status}`,
48
+ };
49
+ const latest = (await resp.json()).version || "?";
50
+ return {
51
+ current,
52
+ latest,
53
+ hasUpdate: latest !== "?" && current !== "unknown" && latest !== current,
54
+ };
55
+ } catch (e) {
56
+ return { current, latest: "?", hasUpdate: false, error: e.message };
57
+ }
58
+ }
59
+
60
+ /** 执行全局升级(异步,不阻塞事件循环,升级期间心跳正常发送) */
61
+ export function performUpgrade() {
62
+ return new Promise((resolve) => {
63
+ exec(
64
+ `npm install -g ${PACKAGE}@latest`,
65
+ { timeout: 120_000 },
66
+ (err, stdout, stderr) => {
67
+ if (err) {
68
+ resolve({ success: false, output: stderr || err.message });
69
+ } else {
70
+ resolve({ success: true, output: stdout.trim() });
71
+ }
72
+ },
73
+ );
74
+ });
75
+ }
76
+
77
+ /**
78
+ * 启动定时升级检测
79
+ * @param {{intervalMs?: number, onUpdateAvailable?: function}} opts
80
+ * @returns {{stop: function}}
81
+ */
82
+ export function startUpgradeChecker({
83
+ intervalMs = 10 * 60_000,
84
+ onUpdateAvailable,
85
+ } = {}) {
86
+ if (!onUpdateAvailable) return { stop() {} };
87
+
88
+ let running = true,
89
+ timer = null,
90
+ checking = false;
91
+
92
+ const check = async () => {
93
+ if (!running || checking) return;
94
+ checking = true;
95
+ try {
96
+ const r = await checkForUpdates();
97
+ if (r.hasUpdate) {
98
+ console.error(`[upgrade] ${r.current} → ${r.latest},准备升级...`);
99
+ await onUpdateAvailable(r);
100
+ }
101
+ } catch {
102
+ /* 静默 */
103
+ } finally {
104
+ checking = false;
105
+ }
106
+ };
107
+
108
+ // 首次 30s 后检测,之后按间隔
109
+ timer = setInterval(check, intervalMs);
110
+ setTimeout(check, 30_000);
111
+
112
+ return {
113
+ stop() {
114
+ running = false;
115
+ clearInterval(timer);
116
+ timer = null;
117
+ },
118
+ };
119
+ }