tt-help-cli-ycl 1.4.75 → 1.4.76
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
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// === 2026-08-07 变更 ===
|
|
2
|
+
// 1. renderWatchdogDashboard: 客户端按名称排序(在线优先,离线排最后)
|
|
3
|
+
// 2. renderClientCard / renderOfflineCard: 离线客户端红色标题+红色边框
|
|
4
|
+
// 3. 整体字体调大一号(12px→14px, 14px→16px, 10px→12px 等)
|
|
5
|
+
// 4. 新增 wdClearLlm() + 🧹 按钮(清除指定客户端 LLM 分析提示)
|
|
6
|
+
// 5. renderCmdRows 新增 llmAnalysis 参数,显示 🤖 AI 分析行
|
|
1
7
|
const COLORS = [
|
|
2
8
|
"#fe2c55",
|
|
3
9
|
"#60a5fa",
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 独立模块。每条命令是完整命令行字符串(如 "tt-help explore --location DE"),
|
|
5
5
|
* 按 userId(~/.tt-help.json 的 userId 字段)分组存储。
|
|
6
|
+
*
|
|
7
|
+
* === 2026-08-07 变更 ===
|
|
8
|
+
* 1. 新增 llmCache 内存缓存,handleAnalyzeLogs 分析完成后写入缓存
|
|
9
|
+
* 2. getOnlineClients() 优先用缓存填充 llmAnalysis(不再依赖 agent 转发)
|
|
10
|
+
* 3. 新增 POST /api/watchdog/clear-llm 端点,手动清除指定客户端的 LLM 提示
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
package/src/watchdog/agent.js
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
* 3. 处理信号优雅退出
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getSystemInfo,
|
|
12
|
+
getClientConfig,
|
|
13
|
+
getLocalIP,
|
|
14
|
+
getClashHealth,
|
|
15
|
+
} from "./reporter.js";
|
|
11
16
|
import { ProcessManager } from "./process-manager.js";
|
|
12
17
|
import {
|
|
13
18
|
getVersion,
|
|
@@ -16,12 +21,46 @@ import {
|
|
|
16
21
|
startUpgradeChecker,
|
|
17
22
|
} from "./upgrader.js";
|
|
18
23
|
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
24
|
+
import { spawn } from "child_process";
|
|
25
|
+
import { join, dirname } from "path";
|
|
26
|
+
import { homedir, tmpdir } from "os";
|
|
27
|
+
import { fileURLToPath } from "url";
|
|
21
28
|
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
30
|
const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
|
|
23
31
|
const TT_HELP_CONFIG = join(homeDir, ".tt-help.json");
|
|
24
32
|
|
|
33
|
+
/** 自动发现 daemon.js 路径 */
|
|
34
|
+
function findDaemonPath() {
|
|
35
|
+
// 1. 从 ~/.tt-help.json 读取配置
|
|
36
|
+
try {
|
|
37
|
+
if (existsSync(TT_HELP_CONFIG)) {
|
|
38
|
+
const cfg = JSON.parse(readFileSync(TT_HELP_CONFIG, "utf-8"));
|
|
39
|
+
if (cfg.clashDaemonPath && existsSync(cfg.clashDaemonPath)) {
|
|
40
|
+
return cfg.clashDaemonPath;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
|
|
45
|
+
// 2. 相对于 tiktok-cli 同级目录
|
|
46
|
+
const candidates = [
|
|
47
|
+
join(__dirname, "..", "..", "..", "clash-verge-explore", "daemon.js"),
|
|
48
|
+
join(__dirname, "..", "..", "..", "..", "clash-verge-explore", "daemon.js"),
|
|
49
|
+
join(process.cwd(), "..", "clash-verge-explore", "daemon.js"),
|
|
50
|
+
];
|
|
51
|
+
for (const p of candidates) {
|
|
52
|
+
if (existsSync(p)) return p;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// daemon 路径(启动时解析一次)
|
|
58
|
+
let _daemonPath = null;
|
|
59
|
+
function getDaemonPath() {
|
|
60
|
+
if (_daemonPath === null) _daemonPath = findDaemonPath();
|
|
61
|
+
return _daemonPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
/** 将服务端下发的配置合并写回 ~/.tt-help.json */
|
|
26
65
|
function applyClientConfig(newCfg) {
|
|
27
66
|
try {
|
|
@@ -92,6 +131,11 @@ export class WatchdogAgent {
|
|
|
92
131
|
this._heartbeatSeq = 0; // 心跳序号
|
|
93
132
|
this._lastProcCount = -1; // 上次进程数
|
|
94
133
|
this._lastCmdCount = -1; // 上次命令数
|
|
134
|
+
// clash daemon 集成
|
|
135
|
+
this.clashDaemon = null; // daemon 子进程引用
|
|
136
|
+
this.gracePeriodMs = 120_000; // 启动宽限期 2 分钟
|
|
137
|
+
this.startTime = 0; // 启动时间戳
|
|
138
|
+
this._graceEnded = false; // 宽限期是否已结束
|
|
95
139
|
}
|
|
96
140
|
|
|
97
141
|
/**
|
|
@@ -100,6 +144,7 @@ export class WatchdogAgent {
|
|
|
100
144
|
async start() {
|
|
101
145
|
if (this.running) return;
|
|
102
146
|
this.running = true;
|
|
147
|
+
this.startTime = Date.now();
|
|
103
148
|
|
|
104
149
|
console.error(
|
|
105
150
|
`[watchdog] 启动 — 客户端: ${this.clientId}, 版本: ${this.ttHelpVersion}, 间隔: ${this.intervalMs / 1000}s`,
|
|
@@ -111,6 +156,9 @@ export class WatchdogAgent {
|
|
|
111
156
|
// 注册信号处理
|
|
112
157
|
this._setupSignalHandlers();
|
|
113
158
|
|
|
159
|
+
// 启动 clash daemon(保代理通道)
|
|
160
|
+
this._spawnClashDaemon();
|
|
161
|
+
|
|
114
162
|
// 首次立即发送心跳
|
|
115
163
|
await this._sendHeartbeat();
|
|
116
164
|
|
|
@@ -152,6 +200,8 @@ export class WatchdogAgent {
|
|
|
152
200
|
this.upgradeChecker.stop();
|
|
153
201
|
this.upgradeChecker = null;
|
|
154
202
|
}
|
|
203
|
+
// 停止 clash daemon
|
|
204
|
+
this._killClashDaemon();
|
|
155
205
|
this.processManager.stopAll();
|
|
156
206
|
console.error("[watchdog] 看门狗已停止");
|
|
157
207
|
}
|
|
@@ -178,6 +228,7 @@ export class WatchdogAgent {
|
|
|
178
228
|
config: getClientConfig(),
|
|
179
229
|
processes: this.processManager.getProcessesStatus(),
|
|
180
230
|
llmAnalysis: this._llmResults,
|
|
231
|
+
clashHealth: getClashHealth(), // daemon 写入的代理健康状态
|
|
181
232
|
};
|
|
182
233
|
}
|
|
183
234
|
|
|
@@ -244,6 +295,29 @@ export class WatchdogAgent {
|
|
|
244
295
|
console.error(
|
|
245
296
|
`[watchdog] 心跳失败 (${this.consecutiveFailures}/${this.maxFailures}): ${e.message}`,
|
|
246
297
|
);
|
|
298
|
+
|
|
299
|
+
// 启动宽限期:daemon 可能还在切节点,心跳失败不计入自杀计数
|
|
300
|
+
if (!this._graceEnded) {
|
|
301
|
+
const elapsed = Date.now() - this.startTime;
|
|
302
|
+
if (elapsed < this.gracePeriodMs) {
|
|
303
|
+
const remain = Math.round((this.gracePeriodMs - elapsed) / 1000);
|
|
304
|
+
// daemon 已报告健康 → 提前结束宽限期
|
|
305
|
+
const health = getClashHealth();
|
|
306
|
+
if (health && health.healthy) {
|
|
307
|
+
this._graceEnded = true;
|
|
308
|
+
console.error(`[watchdog] 代理已健康,宽限期提前结束`);
|
|
309
|
+
this.consecutiveFailures = 0;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
console.error(
|
|
313
|
+
`[watchdog] 宽限期内 (剩余${remain}s),等待 daemon 修复代理...`,
|
|
314
|
+
);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this._graceEnded = true;
|
|
318
|
+
console.error(`[watchdog] 宽限期结束,恢复正常心跳失败计数`);
|
|
319
|
+
}
|
|
320
|
+
|
|
247
321
|
if (this.consecutiveFailures >= this.maxFailures) {
|
|
248
322
|
console.error("[watchdog] 连续心跳失败次数过多,看门狗退出");
|
|
249
323
|
await this.stop();
|
|
@@ -315,6 +389,76 @@ export class WatchdogAgent {
|
|
|
315
389
|
});
|
|
316
390
|
}
|
|
317
391
|
|
|
392
|
+
/**
|
|
393
|
+
* 启动 clash daemon 子进程
|
|
394
|
+
* daemon 负责代理节点健康巡检和故障切换,watchdog 不阻塞等待
|
|
395
|
+
*/
|
|
396
|
+
_spawnClashDaemon() {
|
|
397
|
+
const daemonPath = getDaemonPath();
|
|
398
|
+
if (!daemonPath) {
|
|
399
|
+
console.error("[watchdog] 未找到 clash daemon (daemon.js),跳过代理守护");
|
|
400
|
+
console.error(
|
|
401
|
+
"[watchdog] 提示: 在 ~/.tt-help.json 中设置 clashDaemonPath",
|
|
402
|
+
);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
console.error(`[watchdog] 启动代理守护: ${daemonPath}`);
|
|
407
|
+
try {
|
|
408
|
+
const child = spawn(process.execPath, [daemonPath], {
|
|
409
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
410
|
+
detached: false,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
child.stdout?.on("data", (d) => {
|
|
414
|
+
// daemon 日志通过 stderr 输出,stdout 静默
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
child.stderr?.on("data", (d) => {
|
|
418
|
+
const msg = d.toString().trim();
|
|
419
|
+
if (msg) {
|
|
420
|
+
console.error(`[clash-daemon] ${msg.substring(0, 300)}`);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
child.on("exit", (code, signal) => {
|
|
425
|
+
console.error(
|
|
426
|
+
`[watchdog] clash daemon 退出 (code=${code}, signal=${signal})`,
|
|
427
|
+
);
|
|
428
|
+
this.clashDaemon = null;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
child.on("error", (err) => {
|
|
432
|
+
console.error(`[watchdog] clash daemon 启动失败: ${err.message}`);
|
|
433
|
+
this.clashDaemon = null;
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
this.clashDaemon = child;
|
|
437
|
+
} catch (e) {
|
|
438
|
+
console.error(`[watchdog] clash daemon 启动异常: ${e.message}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* 停止 clash daemon 子进程
|
|
444
|
+
*/
|
|
445
|
+
_killClashDaemon() {
|
|
446
|
+
if (!this.clashDaemon) return;
|
|
447
|
+
try {
|
|
448
|
+
this.clashDaemon.kill("SIGTERM");
|
|
449
|
+
setTimeout(() => {
|
|
450
|
+
try {
|
|
451
|
+
if (this.clashDaemon && this.clashDaemon.exitCode === null) {
|
|
452
|
+
this.clashDaemon.kill("SIGKILL");
|
|
453
|
+
}
|
|
454
|
+
} catch {}
|
|
455
|
+
}, 3000);
|
|
456
|
+
} catch (e) {
|
|
457
|
+
console.error(`[watchdog] 停止 clash daemon 失败: ${e.message}`);
|
|
458
|
+
}
|
|
459
|
+
this.clashDaemon = null;
|
|
460
|
+
}
|
|
461
|
+
|
|
318
462
|
/**
|
|
319
463
|
* 信号处理
|
|
320
464
|
*/
|
|
@@ -16,6 +16,8 @@ import { writeFileSync, unlinkSync } from "fs";
|
|
|
16
16
|
import { tmpdir } from "os";
|
|
17
17
|
import { join } from "path";
|
|
18
18
|
|
|
19
|
+
// === 2026-08-07 变更 ===
|
|
20
|
+
// syncCommands 改为 async,批量启动时每个进程间隔 SPAWN_STAGGER_MS(30s),避免抢网络
|
|
19
21
|
const MAX_RESTART = 3;
|
|
20
22
|
const MAX_LOG_LINES = 500; // 每个进程最多保留的日志行数
|
|
21
23
|
const IS_WIN = process.platform === "win32";
|
package/src/watchdog/reporter.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
import os from "os";
|
|
7
7
|
import { existsSync, readFileSync } from "fs";
|
|
8
8
|
import { join } from "path";
|
|
9
|
-
import { homedir } from "os";
|
|
9
|
+
import { homedir, tmpdir } from "os";
|
|
10
10
|
|
|
11
11
|
const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
|
|
12
12
|
const CONFIG_PATH = join(homeDir, ".tt-help.json");
|
|
13
|
+
const CLASH_HEALTH_FILE = join(tmpdir(), "clash-health.json");
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* 获取系统信息
|
|
@@ -67,3 +68,25 @@ export function getLocalIP() {
|
|
|
67
68
|
}
|
|
68
69
|
return "127.0.0.1";
|
|
69
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 读取 clash daemon 写入的健康状态文件
|
|
74
|
+
* @returns {object|null} { node, healthy, httpCode, timeTotal, lastCheck, lastSwitch } 或 null
|
|
75
|
+
*/
|
|
76
|
+
export function getClashHealth() {
|
|
77
|
+
try {
|
|
78
|
+
if (existsSync(CLASH_HEALTH_FILE)) {
|
|
79
|
+
const raw = readFileSync(CLASH_HEALTH_FILE, "utf-8");
|
|
80
|
+
const data = JSON.parse(raw);
|
|
81
|
+
// 检查数据是否过期(超过 2 分钟没更新视为 stale)
|
|
82
|
+
const age = Date.now() - new Date(data.lastCheck || 0).getTime();
|
|
83
|
+
if (age > 120_000) {
|
|
84
|
+
return { ...data, stale: true, ageSeconds: Math.round(age / 1000) };
|
|
85
|
+
}
|
|
86
|
+
return { ...data, stale: false };
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// 文件不存在或解析失败
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|