tt-help-cli-ycl 1.4.140 → 1.4.144
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/sync-raw-jobs.mjs +13 -7
- package/scripts/test-api-interceptor-429.mjs +122 -0
- 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/comments.js +5 -5
- package/src/cli/precheck.js +2 -2
- package/src/cli/refresh-mini.js +1 -1
- package/src/cli/tag.js +32 -41
- package/src/lib/api-client.js +11 -5
- package/src/lib/api-interceptor.js +26 -8
- package/src/lib/app-config.js +119 -0
- package/src/lib/args.js +3 -1
- package/src/lib/client-error-report.js +3 -0
- package/src/lib/constants.js +3 -1
- package/src/lib/target-locations.js +1 -0
- package/src/scraper/core.js +2 -2
- package/src/scraper/explore-core.js +2 -2
- package/src/watch/data-store.js +9 -8
- package/src/watch/public/app.js +362 -34
- package/src/watch/public/index.html +62 -2
- package/src/watch/public/style.css +16 -0
- package/src/watch/server.js +85 -40
- 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
|
@@ -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");
|