tt-help-cli-ycl 1.4.140 → 1.4.142
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/scripts/test-client-error-monitor.mjs +176 -0
- package/scripts/test-daemon-failover.mjs +13 -0
- package/scripts/test-proxy-switch-restart.mjs +198 -0
- package/src/clash/daemon.js +26 -10
- package/src/cli/tag.js +26 -17
- package/src/lib/api-client.js +11 -5
- package/src/lib/client-error-report.js +3 -0
- package/src/watch/data-store.js +3 -0
- package/src/watch/public/app.js +182 -8
- package/src/watch/public/index.html +41 -1
- package/src/watch/server.js +1 -0
- package/src/watch/watchdog-server.js +100 -0
- package/src/watchdog/agent.js +157 -1
- package/src/watchdog/client-error-monitor.js +131 -0
- package/src/watchdog/process-manager.js +49 -0
- package/src/webserver/server.mjs +1 -0
package/src/watchdog/agent.js
CHANGED
|
@@ -24,9 +24,10 @@ import {
|
|
|
24
24
|
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
25
25
|
import { spawn } from "child_process";
|
|
26
26
|
import { join, dirname } from "path";
|
|
27
|
-
import { homedir, tmpdir } from "os";
|
|
27
|
+
import { homedir, tmpdir, hostname as osHostname } from "os";
|
|
28
28
|
import { fileURLToPath } from "url";
|
|
29
29
|
import { pushSelfLog, getSelfLogs } from "./self-log-buffer.js";
|
|
30
|
+
import { ClientErrorMonitor } from "./client-error-monitor.js";
|
|
30
31
|
|
|
31
32
|
// ── 看门狗自身日志入环(供后台页面按需拉取,经 report-logs 回传落盘) ──
|
|
32
33
|
// 包装 console.error:本进程所有 [watchdog]/[llm-analyzer]/[upgrade] 等日志
|
|
@@ -111,6 +112,7 @@ const NETWORK_ERROR_PATTERNS = [
|
|
|
111
112
|
];
|
|
112
113
|
const NETWORK_ERROR_RE = new RegExp(NETWORK_ERROR_PATTERNS.join("|"), "i");
|
|
113
114
|
const ERROR_SCAN_INTERVAL_MS = 30_000; // 日志扫描间隔
|
|
115
|
+
const SWITCH_SCAN_INTERVAL_MS = 15_000; // 代理切换感知检查间隔(daemon restartSeq 变化 → 重启全部命令)
|
|
114
116
|
const ERROR_TRIGGER_COUNT = 10; // 连续错误阈值
|
|
115
117
|
const ERROR_MIN_MS = 5 * 60 * 1000; // 最小持续时间(时间只做最小值判断)
|
|
116
118
|
const ERROR_BREAK_MS = 120_000; // 两次错误间隔超过则视为连续中断,重置计数
|
|
@@ -262,6 +264,7 @@ export class WatchdogAgent {
|
|
|
262
264
|
this.thresholdTimer = null;
|
|
263
265
|
this.llmAnalyzerTimer = null;
|
|
264
266
|
this.processManager = new ProcessManager();
|
|
267
|
+
this.clientErrorMonitor = null; // 客户端异常监控(watch 异常列表 → 本机命中强切 + 清理)
|
|
265
268
|
this.consecutiveFailures = 0;
|
|
266
269
|
this.maxFailures = 10;
|
|
267
270
|
this.upgradeChecker = null;
|
|
@@ -287,6 +290,11 @@ export class WatchdogAgent {
|
|
|
287
290
|
this._errorStates.delete(cmdId);
|
|
288
291
|
});
|
|
289
292
|
this._errorTriggerTimer = null;
|
|
293
|
+
this._switchCheckTimer = null; // 代理切换感知定时器
|
|
294
|
+
this._restartSeqBaseline = null; // 已消费的切换序号基线 Map<通道,seq>(首次读取初始化)
|
|
295
|
+
this._switchRestartEnabled = false; // 代理切换重启默认不启用(后端下发开启才生效)
|
|
296
|
+
this._switchRestartConfigKey = null; // 上次配置指纹(幂等打印用)
|
|
297
|
+
this._autoUpgradeEnabled = true; // 自动升级默认开启(后端下发关闭才停用)
|
|
290
298
|
this._errorTriggerEnabled = false; // 默认不启用(后端下发开启才生效)
|
|
291
299
|
this._errorTriggerCount = ERROR_TRIGGER_COUNT;
|
|
292
300
|
this._errorTriggerMinMs = ERROR_MIN_MS;
|
|
@@ -345,6 +353,11 @@ export class WatchdogAgent {
|
|
|
345
353
|
this._checkProcessErrors();
|
|
346
354
|
}, ERROR_SCAN_INTERVAL_MS);
|
|
347
355
|
|
|
356
|
+
// 代理切换感知:daemon 每次实际切换 restartSeq+1,变化即强制重启全部命令
|
|
357
|
+
this._switchCheckTimer = setInterval(() => {
|
|
358
|
+
this._checkProxySwitch();
|
|
359
|
+
}, SWITCH_SCAN_INTERVAL_MS);
|
|
360
|
+
|
|
348
361
|
// 登录检测调度:每 60s 检查一次是否到点(首次 60s 后检测,之后每 loginCheckIntervalMs)
|
|
349
362
|
this.loginCheckTimer = setInterval(() => {
|
|
350
363
|
this._maybeRunLoginCheck();
|
|
@@ -358,6 +371,9 @@ export class WatchdogAgent {
|
|
|
358
371
|
|
|
359
372
|
// 自动升级检测(仅全局安装模式)
|
|
360
373
|
this._startUpgradeChecker();
|
|
374
|
+
|
|
375
|
+
// 客户端异常监控(watch 服务端异常列表 → 本机命中强切 + 清理)
|
|
376
|
+
this._startClientErrorMonitor();
|
|
361
377
|
}
|
|
362
378
|
|
|
363
379
|
/**
|
|
@@ -381,6 +397,10 @@ export class WatchdogAgent {
|
|
|
381
397
|
clearInterval(this._errorTriggerTimer);
|
|
382
398
|
this._errorTriggerTimer = null;
|
|
383
399
|
}
|
|
400
|
+
if (this._switchCheckTimer) {
|
|
401
|
+
clearInterval(this._switchCheckTimer);
|
|
402
|
+
this._switchCheckTimer = null;
|
|
403
|
+
}
|
|
384
404
|
if (this.loginCheckTimer) {
|
|
385
405
|
clearInterval(this.loginCheckTimer);
|
|
386
406
|
this.loginCheckTimer = null;
|
|
@@ -393,6 +413,10 @@ export class WatchdogAgent {
|
|
|
393
413
|
this.upgradeChecker.stop();
|
|
394
414
|
this.upgradeChecker = null;
|
|
395
415
|
}
|
|
416
|
+
if (this.clientErrorMonitor) {
|
|
417
|
+
this.clientErrorMonitor.stop();
|
|
418
|
+
this.clientErrorMonitor = null;
|
|
419
|
+
}
|
|
396
420
|
// 停止 clash daemon(标记主动关闭,exit 时不再自动重启)
|
|
397
421
|
this._daemonStopping = true;
|
|
398
422
|
if (this._daemonRestartTimer) {
|
|
@@ -404,6 +428,80 @@ export class WatchdogAgent {
|
|
|
404
428
|
console.error("[watchdog] 看门狗已停止");
|
|
405
429
|
}
|
|
406
430
|
|
|
431
|
+
/**
|
|
432
|
+
* 代理切换感知:读 daemon health 的 restartSeq,任一通道变大即发生过切换,
|
|
433
|
+
* 强制重启全部被监控命令。默认不启用(后端开关开启才重启);
|
|
434
|
+
* 基线始终跟随 seq(无论开关状态):开启后只响应"新切换",不补触发历史切换。
|
|
435
|
+
* 变小 = daemon 重启序号归零,只更新基线不触发(防 watchdog/daemon 重启误伤)。
|
|
436
|
+
*/
|
|
437
|
+
async _checkProxySwitch() {
|
|
438
|
+
try {
|
|
439
|
+
const health = getClashHealth();
|
|
440
|
+
if (!health) return; // daemon 未就绪,暂不初始化基线
|
|
441
|
+
const seqMap = new Map();
|
|
442
|
+
seqMap.set("main", Number(health.restartSeq) || 0);
|
|
443
|
+
for (const ch of getClashHealthChannels()) {
|
|
444
|
+
seqMap.set(`ch${ch.port}`, Number(ch.restartSeq) || 0);
|
|
445
|
+
}
|
|
446
|
+
if (this._restartSeqBaseline === null) {
|
|
447
|
+
this._restartSeqBaseline = seqMap; // 首次读取建基线(watchdog 重启不误触发)
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
let switched = false;
|
|
451
|
+
for (const [key, seq] of seqMap) {
|
|
452
|
+
if (seq > (this._restartSeqBaseline.get(key) || 0)) {
|
|
453
|
+
switched = true;
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
this._restartSeqBaseline = seqMap;
|
|
458
|
+
if (!switched) return;
|
|
459
|
+
if (!this._switchRestartEnabled) {
|
|
460
|
+
console.error(
|
|
461
|
+
`[watchdog] 检测到代理切换但重启功能未启用(后台可开启),跳过重启`,
|
|
462
|
+
);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const reason = health.lastSwitch
|
|
466
|
+
? `代理切换(${health.lastSwitch})`
|
|
467
|
+
: "代理切换";
|
|
468
|
+
console.error(`[watchdog] 检测到${reason},强制重启全部命令...`);
|
|
469
|
+
await this.processManager.restartAll(reason);
|
|
470
|
+
} catch (e) {
|
|
471
|
+
console.error(`[watchdog] 代理切换感知检查失败: ${e.message}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** 应用服务端下发的代理切换重启配置(默认不启用,后端下发开启才生效) */
|
|
476
|
+
_applySwitchRestartConfig(cfg) {
|
|
477
|
+
this._switchRestartEnabled = !!cfg?.enabled;
|
|
478
|
+
const key = `${this._switchRestartEnabled}`;
|
|
479
|
+
if (key === this._switchRestartConfigKey) return;
|
|
480
|
+
this._switchRestartConfigKey = key;
|
|
481
|
+
console.error(
|
|
482
|
+
`[watchdog] 代理切换重启: ${this._switchRestartEnabled ? "已启用" : "已禁用"}(切换后${this._switchRestartEnabled ? "" : "不"}强制重启命令)`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* 应用服务端下发的自动升级开关
|
|
488
|
+
* 未配置(null/undefined)= 默认开启;后端显式 {enabled:false} 才停用升级检测器
|
|
489
|
+
*/
|
|
490
|
+
_applyAutoUpgradeConfig(cfg) {
|
|
491
|
+
const enabled = cfg ? !!cfg.enabled : true;
|
|
492
|
+
if (enabled === this._autoUpgradeEnabled) return; // 幂等,避免每次心跳重复启停
|
|
493
|
+
this._autoUpgradeEnabled = enabled;
|
|
494
|
+
console.error(
|
|
495
|
+
`[watchdog] 自动升级: ${enabled ? "已开启" : "已关闭"}${cfg ? "" : "(未配置,默认开启)"}`,
|
|
496
|
+
);
|
|
497
|
+
if (enabled) {
|
|
498
|
+
if (!this.upgradeChecker) this._startUpgradeChecker();
|
|
499
|
+
} else if (this.upgradeChecker) {
|
|
500
|
+
this.upgradeChecker.stop();
|
|
501
|
+
this.upgradeChecker = null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
407
505
|
/** 应用服务端下发的错误触发切换配置(控制启用与参数,默认不启用) */
|
|
408
506
|
_applyErrorTriggerConfig(cfg) {
|
|
409
507
|
this._errorTriggerEnabled = !!cfg?.enabled;
|
|
@@ -511,6 +609,55 @@ export class WatchdogAgent {
|
|
|
511
609
|
}
|
|
512
610
|
}
|
|
513
611
|
|
|
612
|
+
/** 下发强切 failover(force: 无视 daemon 冷却期,直接轮换候选) */
|
|
613
|
+
async _forceFailover(reason) {
|
|
614
|
+
try {
|
|
615
|
+
const ipc = await import("../clash/ipc.js");
|
|
616
|
+
ipc.writeCommand({
|
|
617
|
+
action: "failover",
|
|
618
|
+
force: true,
|
|
619
|
+
reason: reason || "客户端异常强切",
|
|
620
|
+
});
|
|
621
|
+
console.error(
|
|
622
|
+
`[watchdog] 已下发强切命令 (原因: ${reason || "客户端异常"})`,
|
|
623
|
+
);
|
|
624
|
+
} catch (e) {
|
|
625
|
+
console.error(`[watchdog] 下发强切失败: ${e.message}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* 启动客户端异常监控:定时拉取 watch 服务端异常列表,
|
|
631
|
+
* 命中本机条目(reportCount > 8 且最近 2 分钟内)→ 强切代理 + 删除全部本机条目。
|
|
632
|
+
*/
|
|
633
|
+
_startClientErrorMonitor() {
|
|
634
|
+
const cfg = getClientConfig();
|
|
635
|
+
const server = cfg.server || "";
|
|
636
|
+
if (!server) {
|
|
637
|
+
console.error(
|
|
638
|
+
"[watchdog] 未配置 watch 服务端地址 (server),客户端异常监控跳过",
|
|
639
|
+
);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
this.clientErrorMonitor = new ClientErrorMonitor({
|
|
643
|
+
serverUrl: server,
|
|
644
|
+
hostname: osHostname(),
|
|
645
|
+
intervalMs: 60_000,
|
|
646
|
+
onAlert: async (mine) => {
|
|
647
|
+
const ids = mine.map((e) => e.userId).join(",");
|
|
648
|
+
console.error(
|
|
649
|
+
`[watchdog] 客户端异常监控命中 ${mine.length} 条本机异常 (${ids}),强制切换代理并清理`,
|
|
650
|
+
);
|
|
651
|
+
await this._forceFailover("client-errors");
|
|
652
|
+
const removed = await this.clientErrorMonitor.removeErrors(mine);
|
|
653
|
+
console.error(
|
|
654
|
+
`[watchdog] 已清理本机异常条目 ${removed}/${mine.length}`,
|
|
655
|
+
);
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
this.clientErrorMonitor.start();
|
|
659
|
+
}
|
|
660
|
+
|
|
514
661
|
/** 构建心跳请求体 */
|
|
515
662
|
_buildHeartbeatBody() {
|
|
516
663
|
const sys = getSystemInfo();
|
|
@@ -609,6 +756,14 @@ export class WatchdogAgent {
|
|
|
609
756
|
if (data.errorTrigger) {
|
|
610
757
|
this._applyErrorTriggerConfig(data.errorTrigger);
|
|
611
758
|
}
|
|
759
|
+
// 服务端下发的代理切换重启配置(默认不启用,后端下发开启才生效)
|
|
760
|
+
if (data.switchRestart) {
|
|
761
|
+
this._applySwitchRestartConfig(data.switchRestart);
|
|
762
|
+
}
|
|
763
|
+
// 服务端下发的自动升级开关(未配置默认开启;后端显式关闭才停用)
|
|
764
|
+
if (data.autoUpgrade !== undefined) {
|
|
765
|
+
this._applyAutoUpgradeConfig(data.autoUpgrade);
|
|
766
|
+
}
|
|
612
767
|
// 服务端下发的登录检测端口列表
|
|
613
768
|
if (Array.isArray(data.loginPorts)) {
|
|
614
769
|
const key = data.loginPorts.join(",");
|
|
@@ -873,6 +1028,7 @@ export class WatchdogAgent {
|
|
|
873
1028
|
*/
|
|
874
1029
|
_startUpgradeChecker() {
|
|
875
1030
|
if (!isGlobalInstall()) return;
|
|
1031
|
+
if (!this._autoUpgradeEnabled) return;
|
|
876
1032
|
|
|
877
1033
|
this.upgradeChecker = startUpgradeChecker({
|
|
878
1034
|
onUpdateAvailable: async ({ current, latest }) => {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 客户端异常监控模块 — 看门狗侧
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 定时拉取 watch 服务端 /api/client-errors
|
|
6
|
+
* 2. 按 hostname 过滤出本机条目(异常条目由业务进程上报时携带 hostname)
|
|
7
|
+
* 3. 逐条过独立检测函数,命中 → onAlert(本机全部条目, 命中条目)
|
|
8
|
+
* 4. 提供 removeErrors 删除接口(处理完成后的清理)
|
|
9
|
+
*
|
|
10
|
+
* 检测函数第一版:reportCount > 8 且最近一次上报在 2 分钟内。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import os from "os";
|
|
14
|
+
|
|
15
|
+
/** 检测阈值:累计上报次数 > 8 */
|
|
16
|
+
export const DETECT_REPORT_COUNT_THRESHOLD = 8;
|
|
17
|
+
/** 检测窗口:最近一次上报距今 < 2 分钟 */
|
|
18
|
+
export const DETECT_FRESH_MS = 2 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 独立检测函数(第一版)
|
|
22
|
+
* @param {object} err - /api/client-errors 返回的单条异常
|
|
23
|
+
* @param {number} [now] - 当前时间戳(测试注入用)
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
export function detectClientError(err, now = Date.now()) {
|
|
27
|
+
if (!err || typeof err.reportCount !== "number") return false;
|
|
28
|
+
const fresh = now - err.timestamp < DETECT_FRESH_MS;
|
|
29
|
+
return err.reportCount > DETECT_REPORT_COUNT_THRESHOLD && fresh;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 客户端异常监控器
|
|
34
|
+
*/
|
|
35
|
+
export class ClientErrorMonitor {
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} opts
|
|
38
|
+
* @param {string} opts.serverUrl - watch 服务端地址(~/.tt-help.json 的 server)
|
|
39
|
+
* @param {string} [opts.hostname] - 本机 hostname(默认 os.hostname())
|
|
40
|
+
* @param {number} [opts.intervalMs] - 拉取周期,默认 60s
|
|
41
|
+
* @param {(err: object, now: number) => boolean} [opts.detect] - 检测函数,默认 detectClientError
|
|
42
|
+
* @param {(mine: Array, hit: Array) => Promise|void} [opts.onAlert] - 命中回调
|
|
43
|
+
*/
|
|
44
|
+
constructor({ serverUrl, hostname, intervalMs = 60_000, detect, onAlert }) {
|
|
45
|
+
this.serverUrl = serverUrl ? String(serverUrl).replace(/\/+$/, "") : "";
|
|
46
|
+
this.hostname = hostname || os.hostname();
|
|
47
|
+
this.intervalMs = intervalMs;
|
|
48
|
+
this.detect = detect || detectClientError;
|
|
49
|
+
this.onAlert = onAlert || null;
|
|
50
|
+
this.timer = null;
|
|
51
|
+
this.running = false;
|
|
52
|
+
this._polling = false; // 防重入
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 启动轮询(立即拉一次,之后按周期) */
|
|
56
|
+
start() {
|
|
57
|
+
if (this.running || !this.serverUrl) return;
|
|
58
|
+
this.running = true;
|
|
59
|
+
this.timer = setInterval(
|
|
60
|
+
() => this._poll().catch(() => {}),
|
|
61
|
+
this.intervalMs,
|
|
62
|
+
);
|
|
63
|
+
this._poll().catch(() => {});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
stop() {
|
|
67
|
+
this.running = false;
|
|
68
|
+
if (this.timer) {
|
|
69
|
+
clearInterval(this.timer);
|
|
70
|
+
this.timer = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 拉取一次并评估
|
|
76
|
+
* @returns {Promise<Array|null>} 本机命中的条目;无可评估内容时返回 null
|
|
77
|
+
*/
|
|
78
|
+
async _poll() {
|
|
79
|
+
if (this._polling) return null;
|
|
80
|
+
this._polling = true;
|
|
81
|
+
try {
|
|
82
|
+
const resp = await fetch(`${this.serverUrl}/api/client-errors`, {
|
|
83
|
+
signal: AbortSignal.timeout(10000),
|
|
84
|
+
});
|
|
85
|
+
if (!resp.ok) return null;
|
|
86
|
+
const data = await resp.json();
|
|
87
|
+
const clients = Array.isArray(data.clients) ? data.clients : [];
|
|
88
|
+
// hostname 为空的旧条目(升级前上报)不参与本机关联
|
|
89
|
+
const mine = clients.filter(
|
|
90
|
+
(c) => c.hostname && c.hostname === this.hostname,
|
|
91
|
+
);
|
|
92
|
+
if (mine.length === 0) return null;
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const hit = mine.filter((c) => this.detect(c, now));
|
|
95
|
+
if (hit.length > 0 && this.onAlert) {
|
|
96
|
+
try {
|
|
97
|
+
await this.onAlert(mine, hit);
|
|
98
|
+
} catch {
|
|
99
|
+
/* 回调异常不中断轮询 */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return hit;
|
|
103
|
+
} catch {
|
|
104
|
+
return null; // 网络/解析失败下轮重试
|
|
105
|
+
} finally {
|
|
106
|
+
this._polling = false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 删除指定异常条目(处理完成后的清理)
|
|
112
|
+
* @param {Array<{userId:string}>} errors
|
|
113
|
+
* @returns {Promise<number>} 成功删除条数
|
|
114
|
+
*/
|
|
115
|
+
async removeErrors(errors) {
|
|
116
|
+
let removed = 0;
|
|
117
|
+
for (const e of errors || []) {
|
|
118
|
+
if (!e || !e.userId) continue;
|
|
119
|
+
try {
|
|
120
|
+
const resp = await fetch(
|
|
121
|
+
`${this.serverUrl}/api/client-error/${encodeURIComponent(e.userId)}`,
|
|
122
|
+
{ method: "DELETE", signal: AbortSignal.timeout(10000) },
|
|
123
|
+
);
|
|
124
|
+
if (resp.ok) removed++;
|
|
125
|
+
} catch {
|
|
126
|
+
/* 单条失败不影响其余 */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return removed;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -23,6 +23,7 @@ const MAX_RESTART = 3;
|
|
|
23
23
|
const MAX_LOG_LINES = 500; // 每个进程最多保留的日志行数
|
|
24
24
|
const IS_WIN = process.platform === "win32";
|
|
25
25
|
const SPAWN_STAGGER_MS = 30000; // 批量启动时每个进程间隔 30 秒,避免抢网络
|
|
26
|
+
const STOP_STAGGER_MS = 10000; // 强制重启时每个命令停止间隔 10 秒(杀 Edge/进程树会瞬时占资源,错峰释放)
|
|
26
27
|
const STALE_LOG_TIMEOUT_MS = 15 * 60 * 1000; // 15 分钟无日志输出视为假死
|
|
27
28
|
|
|
28
29
|
// === 控制台日志格式化(仅显示层增强,不影响任何功能链路) ===
|
|
@@ -718,10 +719,58 @@ export class ProcessManager extends EventEmitter {
|
|
|
718
719
|
}
|
|
719
720
|
}
|
|
720
721
|
|
|
722
|
+
/**
|
|
723
|
+
* 强制重启所有运行中的命令(先停全部,再错峰逐个重启)
|
|
724
|
+
* 用途:代理切换后强制所有业务命令重建连接、走新出口节点
|
|
725
|
+
* @param {string} [reason] - 重启原因(日志用)
|
|
726
|
+
* @returns {Promise<{restarted:number}>}
|
|
727
|
+
*/
|
|
728
|
+
async restartAll(reason) {
|
|
729
|
+
const toRestart = [];
|
|
730
|
+
for (const [, info] of this.processes) {
|
|
731
|
+
if (!info.stopped && info.child && info.cmdDef) {
|
|
732
|
+
toRestart.push(info.cmdDef);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (!toRestart.length) {
|
|
736
|
+
console.error(`[watchdog] ${reason},无运行中的命令,跳过重启`);
|
|
737
|
+
return { restarted: 0 };
|
|
738
|
+
}
|
|
739
|
+
console.error(
|
|
740
|
+
`[watchdog] ${reason},强制重启全部命令 (${toRestart.length}个)...`,
|
|
741
|
+
);
|
|
742
|
+
// 错峰停止:第一个立即停,之后每个间隔 STOP_STAGGER_MS(平滑释放资源,避免同时杀 N 个浏览器)
|
|
743
|
+
for (let i = 0; i < toRestart.length; i++) {
|
|
744
|
+
if (i > 0) await new Promise((r) => setTimeout(r, STOP_STAGGER_MS));
|
|
745
|
+
if (this.shutdown) {
|
|
746
|
+
console.error("[watchdog] 看门狗已停止,中止停止命令");
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
this.stopCommand(toRestart[i].id);
|
|
750
|
+
console.error(
|
|
751
|
+
`[watchdog] 命令 ${toRestart[i].id} 已停止 (${i + 1}/${toRestart.length})`,
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
// 错峰逐个重启:第一个立即起,之后每个间隔 SPAWN_STAGGER_MS
|
|
755
|
+
for (let i = 0; i < toRestart.length; i++) {
|
|
756
|
+
if (i > 0) await new Promise((r) => setTimeout(r, SPAWN_STAGGER_MS));
|
|
757
|
+
if (this.shutdown) {
|
|
758
|
+
console.error("[watchdog] 看门狗已停止,中止重启命令");
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
this.startCommand(toRestart[i]);
|
|
762
|
+
console.error(
|
|
763
|
+
`[watchdog] 命令 ${toRestart[i].id} 已重启 (${i + 1}/${toRestart.length})`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
return { restarted: toRestart.length };
|
|
767
|
+
}
|
|
768
|
+
|
|
721
769
|
/**
|
|
722
770
|
* 停止所有进程
|
|
723
771
|
*/
|
|
724
772
|
stopAll() {
|
|
773
|
+
this.shutdown = true;
|
|
725
774
|
console.error("[watchdog] 正在停止所有子进程...");
|
|
726
775
|
for (const [cmdId] of this.processes) {
|
|
727
776
|
this.stopCommand(cmdId);
|
package/src/webserver/server.mjs
CHANGED
|
@@ -741,6 +741,7 @@ async function notifyCaptchaToWatch(url, maxRetries = 3) {
|
|
|
741
741
|
"TikTok Shop captcha detected, browser opened - manual intervention required",
|
|
742
742
|
stage: "product_info",
|
|
743
743
|
errorStack: url,
|
|
744
|
+
hostname: os.hostname(),
|
|
744
745
|
}),
|
|
745
746
|
});
|
|
746
747
|
console.error("[webserver] Captcha alert sent to watch server");
|