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.
@@ -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
+ }
@@ -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, 503, {
290
+ return sendJSON(res, 200, {
267
291
  error: `TikTok statusCode=${result.statusCode}`,
268
292
  retryable,
269
- tiktokStatusCode: result.statusCode,
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
- // 自动恢复:连续错误超过阈值时重启 scraper
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 (e.name === "CDNBlockedError") {
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) {