tt-help-cli-ycl 1.4.114 → 1.4.117
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/precheck-monitor.sh +37 -0
- package/scripts/test-clash-discover.mjs +126 -0
- package/scripts/test-clash-multichannel.mjs +120 -0
- package/src/clash/api.js +29 -3
- package/src/clash/benchmark/lib/clash-api.js +3 -232
- package/src/clash/checker.js +63 -30
- package/src/clash/daemon.js +285 -108
- package/src/clash/discover.js +451 -0
- package/src/clash/ipc.js +65 -20
- package/src/cli/attach.js +57 -10
- package/src/cli/clash-daemon.js +10 -1
- package/src/cli/precheck.js +42 -14
- package/src/cli/refresh-mini.js +36 -24
- package/src/lib/args.js +17 -0
- package/src/lib/constants.js +6 -1
- package/src/watch/public/app.js +58 -17
- package/src/watch/watchdog-server.js +14 -4
- package/src/watchdog/agent.js +16 -6
- package/src/watchdog/reporter.js +32 -1
- package/src/webserver/server.mjs +40 -7
package/src/watchdog/reporter.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import os from "os";
|
|
7
|
-
import { existsSync, readFileSync } from "fs";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { homedir, tmpdir } from "os";
|
|
10
10
|
|
|
@@ -91,3 +91,34 @@ export function getClashHealth() {
|
|
|
91
91
|
}
|
|
92
92
|
return null;
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 读取多控制端健康状态(daemon 多通道模式写入的 clash-health-{port}.json)
|
|
97
|
+
* @returns {Array<{port:number, stale:boolean, ageSeconds:number, ...health}>}
|
|
98
|
+
*/
|
|
99
|
+
export function getClashHealthChannels() {
|
|
100
|
+
const out = [];
|
|
101
|
+
try {
|
|
102
|
+
for (const name of readdirSync(tmpdir())) {
|
|
103
|
+
const m = /^clash-health-(\d+)\.json$/.exec(name);
|
|
104
|
+
if (!m) continue;
|
|
105
|
+
const port = parseInt(m[1], 10);
|
|
106
|
+
if (!port) continue;
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(readFileSync(join(tmpdir(), name), "utf-8"));
|
|
109
|
+
const age = Date.now() - new Date(data.lastCheck || 0).getTime();
|
|
110
|
+
out.push({
|
|
111
|
+
...data,
|
|
112
|
+
port,
|
|
113
|
+
stale: age > 120_000,
|
|
114
|
+
ageSeconds: Math.round(age / 1000),
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
/* 单个文件解析失败跳过 */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
/* 目录不可读 */
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
package/src/webserver/server.mjs
CHANGED
|
@@ -80,6 +80,28 @@ function apiLog(msg) {
|
|
|
80
80
|
if (SCRAPER_CONFIG.enableLog) console.error(msg);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// 限频错误日志:同类错误在窗口内只打首条,其余计数汇总,避免 CDN 限流等高频错误刷屏
|
|
84
|
+
const ERROR_LOG_THROTTLE_MS = 10000;
|
|
85
|
+
let lastThrottledLogTime = 0;
|
|
86
|
+
let throttledLogCount = 0;
|
|
87
|
+
let throttledLogLabel = "";
|
|
88
|
+
function logErrorThrottled(label, msg) {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
if (now - lastThrottledLogTime >= ERROR_LOG_THROTTLE_MS) {
|
|
91
|
+
if (throttledLogCount > 0) {
|
|
92
|
+
console.error(
|
|
93
|
+
`[webserver] ${throttledLogLabel} | 窗口内共 ${throttledLogCount} 条同类错误(已限频)`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
console.error(`[webserver] ${label} | ${msg}`);
|
|
97
|
+
lastThrottledLogTime = now;
|
|
98
|
+
throttledLogCount = 0;
|
|
99
|
+
throttledLogLabel = label;
|
|
100
|
+
} else {
|
|
101
|
+
throttledLogCount++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
83
105
|
let scraperInstance = null;
|
|
84
106
|
let scraperClosing = false;
|
|
85
107
|
let scraperInitPromise = null;
|
|
@@ -257,16 +279,19 @@ async function handleTiktokUser(req, res) {
|
|
|
257
279
|
|
|
258
280
|
if (result.error && result.statusCode) {
|
|
259
281
|
// TikTok 明确返回的状态码(209002 验证码、10202 被封、10221 不存在等):
|
|
260
|
-
// 属业务结果而非 webserver 故障,不计入连续错误、不触发 scraper
|
|
282
|
+
// 属业务结果而非 webserver 故障,不计入连续错误、不触发 scraper 重启;
|
|
283
|
+
// 返回 200 而非 503:避免 nginx 将其当作节点故障去 failover/熔断
|
|
284
|
+
//(账号/IP 级风控,切换后端无意义,只会放大对 TikTok 的访问)
|
|
261
285
|
const retryable = TIKTOK_RETRYABLE_NORMAL_CODES.has(result.statusCode);
|
|
262
286
|
apiLog(
|
|
263
287
|
`[webserver] user business | ${clientIP} | @${body.uniqueId} | ${elapsed}ms | statusCode=${result.statusCode}${retryable ? " (可重试)" : ""}`,
|
|
264
288
|
);
|
|
265
289
|
recordRequest(true);
|
|
266
|
-
return sendJSON(res,
|
|
290
|
+
return sendJSON(res, 200, {
|
|
267
291
|
error: `TikTok statusCode=${result.statusCode}`,
|
|
268
292
|
retryable,
|
|
269
|
-
|
|
293
|
+
statusCode: result.statusCode, // 后端 data-store / attach 外层读取此字段
|
|
294
|
+
tiktokStatusCode: result.statusCode, // 兼容保留
|
|
270
295
|
});
|
|
271
296
|
}
|
|
272
297
|
if (result.error) {
|
|
@@ -560,22 +585,26 @@ export async function startWebserver(
|
|
|
560
585
|
try {
|
|
561
586
|
return await route.handler(req, res);
|
|
562
587
|
} catch (e) {
|
|
563
|
-
//
|
|
564
|
-
consecutiveErrors
|
|
588
|
+
// 错误分类:CDN 限流是环境问题(代理/IP 被 TikTok CDN 拦截),重启 scraper 无法解决,
|
|
589
|
+
// 故不累计 consecutiveErrors、不触发重启;但仍计入 recentErrors(/health 展示错误率)。
|
|
590
|
+
const isCdnBlocked = e.name === "CDNBlockedError";
|
|
591
|
+
if (!isCdnBlocked) consecutiveErrors++;
|
|
565
592
|
recordRequest(false);
|
|
566
593
|
|
|
567
594
|
// 区分错误类型,返回正确的 HTTP 状态码供 nginx 判断
|
|
568
595
|
let status = 500;
|
|
569
596
|
let retryable = false;
|
|
570
597
|
|
|
571
|
-
if (
|
|
572
|
-
// CDN 限流(Akamai Access Denied)→ 503
|
|
598
|
+
if (isCdnBlocked) {
|
|
599
|
+
// CDN 限流(Akamai Access Denied)→ 503 临时不可用(节点级,nginx 可 failover 到其他节点)
|
|
573
600
|
status = 503;
|
|
574
601
|
retryable = true;
|
|
602
|
+
logErrorThrottled("request CDN限流", e.message);
|
|
575
603
|
} else if (e.message?.includes("Server is shutting down")) {
|
|
576
604
|
// 服务正在关闭 → 503
|
|
577
605
|
status = 503;
|
|
578
606
|
retryable = true;
|
|
607
|
+
console.error(`[webserver] request 服务关闭中: ${e.message}`);
|
|
579
608
|
} else if (
|
|
580
609
|
e.message?.includes("Timeout") ||
|
|
581
610
|
e.message?.includes("timeout") ||
|
|
@@ -584,6 +613,7 @@ export async function startWebserver(
|
|
|
584
613
|
// 超时 → 504
|
|
585
614
|
status = 504;
|
|
586
615
|
retryable = true;
|
|
616
|
+
console.error(`[webserver] request 超时: ${e.message}`);
|
|
587
617
|
} else if (
|
|
588
618
|
e.message?.includes("Protocol error") ||
|
|
589
619
|
e.message?.includes("Target closed") ||
|
|
@@ -593,6 +623,9 @@ export async function startWebserver(
|
|
|
593
623
|
// 浏览器/页面崩溃 → 503
|
|
594
624
|
status = 503;
|
|
595
625
|
retryable = true;
|
|
626
|
+
console.error(`[webserver] request 浏览器异常: ${e.message}`);
|
|
627
|
+
} else {
|
|
628
|
+
console.error(`[webserver] request 未知错误: ${e.message}`);
|
|
596
629
|
}
|
|
597
630
|
|
|
598
631
|
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|