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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.75",
3
+ "version": "1.4.76",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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";
@@ -7,7 +7,12 @@
7
7
  * 3. 处理信号优雅退出
8
8
  */
9
9
 
10
- import { getSystemInfo, getClientConfig, getLocalIP } from "./reporter.js";
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 { join } from "path";
20
- import { homedir } from "os";
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";
@@ -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
+ }