tt-help-cli-ycl 1.4.159 → 1.4.160

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.159",
3
+ "version": "1.4.160",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,4 +48,4 @@
48
48
  "devDependencies": {
49
49
  "better-sqlite3": "^12.10.0"
50
50
  }
51
- }
51
+ }
@@ -296,6 +296,7 @@ export class WatchdogAgent {
296
296
  this._switchRestartEnabled = false; // 代理切换重启默认不启用(后端下发开启才生效)
297
297
  this._switchRestartConfigKey = null; // 上次配置指纹(幂等打印用)
298
298
  this._autoUpgradeEnabled = true; // 自动升级默认开启(后端下发关闭才停用)
299
+ this._loadedVersion = getVersion(); // 启动时包版本(磁盘版本变化 = 被手动升级,需自动重启)
299
300
  this._errorTriggerEnabled = false; // 默认不启用(后端下发开启才生效)
300
301
  this._errorTriggerCount = ERROR_TRIGGER_COUNT;
301
302
  this._errorTriggerMinMs = ERROR_MIN_MS;
@@ -1068,6 +1069,7 @@ export class WatchdogAgent {
1068
1069
  _startUpgradeChecker() {
1069
1070
  if (!isGlobalInstall()) return;
1070
1071
  if (!this._autoUpgradeEnabled) return;
1072
+ this._startSelfVersionWatcher();
1071
1073
 
1072
1074
  this.upgradeChecker = startUpgradeChecker({
1073
1075
  onUpdateAvailable: async ({ current, latest }) => {
@@ -1082,6 +1084,31 @@ export class WatchdogAgent {
1082
1084
  });
1083
1085
  }
1084
1086
 
1087
+ /**
1088
+ * 磁盘版本自检(每 60s):发现磁盘包版本 ≠ 本进程加载版本(被手动 npm i -g 升/降级)时,
1089
+ * 自动走 restart-only 重启(杀旧 → 启新 → 校验),无需人工重启。
1090
+ * 只比不等:手动降级同样重启,保证运行代码与磁盘一致。
1091
+ */
1092
+ _startSelfVersionWatcher() {
1093
+ if (this._selfVersionTimer) return;
1094
+ this._selfVersionTimer = setInterval(() => {
1095
+ try {
1096
+ const disk = getVersion();
1097
+ if (disk && disk !== this._loadedVersion) {
1098
+ console.error(
1099
+ `[upgrade] 检测到包版本变化 ${this._loadedVersion} → ${disk}(手动升级),自动重启 watchdog...`,
1100
+ );
1101
+ clearInterval(this._selfVersionTimer);
1102
+ this._selfVersionTimer = null;
1103
+ spawnUpgrader({ ...this._buildRestartParams(), restartOnly: true });
1104
+ }
1105
+ } catch {
1106
+ /* 读版本失败,下轮重试 */
1107
+ }
1108
+ }, 60_000);
1109
+ if (this._selfVersionTimer.unref) this._selfVersionTimer.unref();
1110
+ }
1111
+
1085
1112
  /** 构建新版 watchdog 重启所需参数(透传给独立升级器) */
1086
1113
  _buildRestartParams() {
1087
1114
  return {
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { exec, spawn, execFile } from "child_process";
10
10
  import { promisify } from "util";
11
- import { readFileSync, openSync, unlinkSync } from "fs";
11
+ import { readFileSync, writeFileSync, openSync, unlinkSync } from "fs";
12
12
  import { fileURLToPath } from "url";
13
13
  import { dirname, join } from "path";
14
14
  import { homedir, tmpdir } from "os";
@@ -549,19 +549,38 @@ async function listWatchdogPids() {
549
549
  }
550
550
 
551
551
  /**
552
- * 启动新版 watchdog:
553
- * - Windows:`cmd.exe /c start cmd /k <node> <cli.js> watchdog ...`
554
- * start 弹出【独立交互 cmd 窗口】(与 U 完全解耦),watchdog 日志直接显示在窗口里,
555
- * /k 保持窗口打开(watchdog 退出后提示符仍在,便于排障);中间 cmd 隐藏不闪窗。
556
- * - 其他平台:直接 detached spawn node,日志落盘 logFd。
552
+ * 启动新版 watchdog(Windows 方式 2026-09-25 重写 — 214 实测教训):
553
+ *
554
+ * 旧实现(node spawn 直接嵌 start 命令,windowsHide:true)起的 watchdog 会「起而复死」:
555
+ * 外层 cmd 带 CREATE_NO_WINDOW 隐藏启动,其内 start 弹出的新控制台随外层生命周期
556
+ * 被系统回收,watchdog 写完存活标记后数秒内消失(1.4.159 自动升级后「看门狗没拉起来」实因)。
557
+ *
558
+ * 新实现:写临时 bat,由 cmd /c 执行;bat 内用 start 弹独立交互窗口运行新版 ——
559
+ * 与运维手动双击/命令行启动完全同构(213/214 实测该方式进程持久不灭)。
560
+ * 窗口标题含 WINDOW_TITLE_TAG,供下次升级 killOldWatchdogWindow 关窗匹配。
561
+ *
562
+ * 其他平台:直接 detached spawn node,日志落盘 logFd。
557
563
  * @returns {import("child_process").ChildProcess}
558
564
  */
559
565
  function spawnNewWatchdog(cliPath, args, logFd) {
560
566
  if (process.platform === "win32") {
567
+ // start 直接执行 node(不经 cmd /k 中转):cmd /k 后带多个引号段会触发 cmd 的
568
+ // 引号剥离规则(剥整行首尾引号 → 带空格的 node 路径被截断成 C:\Program → 启动失败)
569
+ const cmdLine = [cliPath, ...args].map((a) => `"${a}"`).join(" ");
570
+ const batPath = join(
571
+ tmpdir(),
572
+ `tt-help-watchdog-start-${Date.now()}.bat`,
573
+ );
574
+ const bat = [
575
+ "@echo off",
576
+ `start "${WINDOW_TITLE_TAG}" "${process.execPath}" ${cmdLine}`,
577
+ "exit",
578
+ ].join("\r\n");
579
+ writeFileSync(batPath, bat + "\r\n", "utf-8");
561
580
  const child = spawn(
562
581
  "cmd.exe",
563
- ["/c", "start", "cmd", "/k", process.execPath, cliPath, ...args],
564
- // cwd 固定到用户主目录:防止新版 watchdog 继承包目录 CWD 锁死包目录(npm install EBUSY)
582
+ ["/c", batPath],
583
+ // 外层 cmd 隐藏且一闪即退;新窗口由 start 创建,独立持久
565
584
  { detached: true, stdio: "ignore", windowsHide: true, cwd: homeDir },
566
585
  );
567
586
  child.unref();
@@ -583,12 +602,94 @@ function buildWatchdogArgs({ serverUrl, clientId, intervalSec, noDaemon }) {
583
602
  return args;
584
603
  }
585
604
 
605
+ /**
606
+ * 仅重启模式(--restart-only):由运行中的 watchdog 在检测到磁盘包版本变化时调用。
607
+ * 复用完整升级模式的 杀旧→启新→存活校验(第 3/4/5 步),跳过下载/验证。
608
+ */
609
+ async function runRestartOnlyMode(argv) {
610
+ const get = (f) => (argv.includes(f) ? argv[argv.indexOf(f) + 1] : undefined);
611
+ const serverUrl = get("--server");
612
+ const clientId = get("--client-id");
613
+ const intervalSec = parseInt(get("--interval"), 10) || 15;
614
+ const noDaemon = argv.includes("--no-daemon");
615
+ const oldPid =
616
+ (argv.includes("--old-pid") ? parseInt(get("--old-pid"), 10) : 0) ||
617
+ process.ppid;
618
+ if (!oldPid) {
619
+ console.error(`[restart-only] 无旧 pid,退出`);
620
+ process.exit(1);
621
+ }
622
+ console.error(`[restart-only] 重启 watchdog (旧 pid=${oldPid})...`);
623
+
624
+ // 杀旧:与完整模式一致(Ctrl+C 优雅 → 强杀 → 清树 → 孤儿 → 关窗)
625
+ const procSnapshot = await snapshotAllProcesses();
626
+ const descPids = listDescendantPids(procSnapshot, oldPid, process.pid);
627
+ let exited = false;
628
+ const gracefulSent =
629
+ process.platform === "win32" && (await sendConsoleCtrlC(oldPid));
630
+ if (gracefulSent) {
631
+ console.error(`[restart-only] 已发送 Ctrl+C,等待旧 watchdog 优雅停止业务...`);
632
+ exited = await waitProcessExit(oldPid, 8000);
633
+ }
634
+ if (!exited) {
635
+ if (gracefulSent)
636
+ console.error(`[restart-only] 旧 watchdog 未在 8s 内优雅退出,强制终止`);
637
+ exited = await terminateProcess(oldPid, 5000);
638
+ await new Promise((r) => setTimeout(r, 1000));
639
+ }
640
+ await killPidList(descPids, "[restart-only] 已清理旧 watchdog 进程树");
641
+ const orphanPids = listOrphanBusinessPids(await snapshotAllProcesses());
642
+ await killPidList(orphanPids, "[restart-only] 已清理孤儿业务进程");
643
+ await killOldWatchdogWindow();
644
+
645
+ // 启新(与完整模式一致:Windows 弹独立 cmd /k 窗口)
646
+ console.error(`[restart-only] 启动新版 watchdog ...`);
647
+ const cliPath = join(__dirname, "..", "..", "cli.js");
648
+ const args = buildWatchdogArgs({ serverUrl, clientId, intervalSec, noDaemon });
649
+ const logFd =
650
+ process.platform === "win32"
651
+ ? undefined
652
+ : openLogFile("tt-help-watchdog", "restart-only", "新版 watchdog 日志");
653
+ if (process.platform === "win32") deleteStartedFile();
654
+ spawnNewWatchdog(cliPath, args, logFd);
655
+
656
+ // 存活校验(与完整模式一致)
657
+ await new Promise((r) => setTimeout(r, 3000));
658
+ if (process.platform === "win32") {
659
+ const deadline = Date.now() + 15_000;
660
+ let st = null;
661
+ while (Date.now() < deadline && !st) {
662
+ const cur = readStartedFile();
663
+ if (cur && cur.pid > 0 && (await isProcessAlive(cur.pid))) st = cur;
664
+ if (!st) await new Promise((r) => setTimeout(r, 1000));
665
+ }
666
+ if (st) {
667
+ console.error(`[restart-only] 新版 watchdog 存活确认 (pid=${st.pid}),重启完成`);
668
+ } else {
669
+ console.error(`[restart-only] 新版 watchdog 启动后未存活,请查看窗口/日志排障`);
670
+ process.exit(1);
671
+ }
672
+ } else {
673
+ const newPids = (await listWatchdogPids()).filter((p) => p !== oldPid);
674
+ if (newPids.length > 0) {
675
+ console.error(`[restart-only] 新版 watchdog 存活确认 (pid=${newPids.join(",")}),重启完成`);
676
+ } else {
677
+ console.error(`[restart-only] 新版 watchdog 启动后未存活,请查看窗口/日志排障`);
678
+ process.exit(1);
679
+ }
680
+ }
681
+ process.exit(0);
682
+ }
683
+
586
684
  /**
587
685
  * 独立升级器主流程(由旧 watchdog 以子进程方式启动)
588
686
  * 顺序:下载 → 验证版本 → 杀旧 → 启新。任一步失败都不杀旧,业务不断。
589
687
  * @param {string[]} argv - process.argv.slice(2)
590
688
  */
591
689
  export async function runUpgradeMode(argv) {
690
+ // 仅重启模式:磁盘包被手动升级(npm i -g)后由运行中的 watchdog 调用,
691
+ // 跳过下载与版本验证(此时磁盘与线上一致,会误判"未升级"),直接杀旧→启新→校验
692
+ if (argv.includes("--restart-only")) return runRestartOnlyMode(argv);
592
693
  const get = (f) => (argv.includes(f) ? argv[argv.indexOf(f) + 1] : undefined);
593
694
  const serverUrl = get("--server");
594
695
  const clientId = get("--client-id");
@@ -759,9 +860,11 @@ export function spawnUpgrader({
759
860
  intervalSec = 15,
760
861
  spawnDaemon = true,
761
862
  oldPid = 0,
863
+ restartOnly = false,
762
864
  } = {}) {
763
865
  const upgraderPath = fileURLToPath(import.meta.url);
764
866
  const args = ["--upgrade", "--old-pid", String(oldPid || process.ppid)];
867
+ if (restartOnly) args.push("--restart-only");
765
868
  if (serverUrl) args.push("--server", serverUrl);
766
869
  if (clientId) args.push("--client-id", clientId);
767
870
  args.push("--interval", String(intervalSec));