tt-help-cli-ycl 1.4.136 → 1.4.140

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.
@@ -5,10 +5,11 @@
5
5
  // 手动模式: tt-help comments <URL> [N] [--save] 单视频评论提取
6
6
  // ────────────────────────────────────────
7
7
 
8
- import { writeFileSync, unlinkSync } from "fs";
9
- import os from "os";
10
- import path from "path";
11
- import { execSync } from "child_process";
8
+ import {
9
+ getBrowserMemory,
10
+ formatMemoryWithBrowser,
11
+ } from "../lib/browser-memory.js";
12
+ import { reportConsecutiveTaskErrors } from "../lib/client-error-report.js";
12
13
 
13
14
  import { fetchUserCommentsAPI } from "../lib/api-interceptor-comment.js";
14
15
  import {
@@ -52,9 +53,9 @@ const CFG = {
52
53
  interval: 10,
53
54
  /** 并行数 */
54
55
  parallel: 1,
55
- /** 连续 403 后切换端口 */
56
+ /** 连续 403 后重启浏览器 */
56
57
  consecutive403Blocked: 3,
57
- /** 连续验证码后切换端口 */
58
+ /** 连续验证码后重启浏览器 */
58
59
  maxConsecutiveCaptcha: 3,
59
60
  /** 内存回收 — 触发回收的 RSS(MB) 阈值 */
60
61
  recycleRssMb: 2000,
@@ -66,11 +67,11 @@ const CFG = {
66
67
  recycleCooldownMs: 180000,
67
68
  /** 内存回收 — 每 N 个任务强制回收(0=禁用) */
68
69
  periodicRecycleEvery: 0,
69
- /** 内存回收 — 回收时是否切换端口 */
70
- switchPortOnRecycle: true,
70
+ /** 内存回收 — 回收时是否切换端口(默认关闭:只重启浏览器不换端口) */
71
+ switchPortOnRecycle: false,
71
72
  /** 连续任务处理错误阈值(达到后上报服务端) */
72
73
  consecutiveTaskErrorsMax: 20,
73
- /** 单端口最大运行时间(ms),超时自动切换端口(默认 48 分钟) */
74
+ /** 单端口最大运行时间(ms),超时自动重启浏览器(默认 48 分钟) */
74
75
  maxSessionRunTimeMs: 48 * 60 * 1000,
75
76
  };
76
77
 
@@ -103,7 +104,7 @@ function loadEnvConfig() {
103
104
  int("COMMENTS_PERIODIC_RECYCLE_EVERY", CFG.periodicRecycleEvery),
104
105
  );
105
106
  CFG.switchPortOnRecycle =
106
- String(process.env.COMMENTS_SWITCH_PORT_ON_RECYCLE || "1") !== "0";
107
+ String(process.env.COMMENTS_SWITCH_PORT_ON_RECYCLE || "0") !== "0";
107
108
  CFG.consecutiveTaskErrorsMax = Math.max(
108
109
  1,
109
110
  int("COMMENTS_CONSECUTIVE_TASK_ERRORS_MAX", CFG.consecutiveTaskErrorsMax),
@@ -114,95 +115,6 @@ function loadEnvConfig() {
114
115
  );
115
116
  }
116
117
 
117
- // ────────────────────────────────────────
118
- // Section 2: 内存监控工具
119
- // ────────────────────────────────────────
120
-
121
- function formatMemoryUsage(mem = process.memoryUsage()) {
122
- const mb = (n) => (n / 1024 / 1024).toFixed(0);
123
- return `rss:${mb(mem.rss)}MB heap:${mb(mem.heapUsed)}MB ext:${mb(mem.external)}MB ab:${mb(mem.arrayBuffers)}MB`;
124
- }
125
-
126
- /**
127
- * 获取 Edge 浏览器进程的内存占用(跨平台:macOS/Linux/Windows)
128
- */
129
- function getBrowserMemory(port = null) {
130
- try {
131
- const platform = os.platform();
132
- let cmd;
133
- if (platform === "darwin") {
134
- if (port) {
135
- cmd = `ps aux | grep "[M]icrosoft Edge" | grep "remote-debugging-port=${port}" | awk '{sum += $6} END {printf "%d", sum/1024}'`;
136
- } else {
137
- cmd =
138
- 'ps aux | grep "[M]icrosoft Edge" | awk \'{sum += $6} END {printf "%d", sum/1024}\'';
139
- }
140
- const result = execSync(cmd, { timeout: 5000 }).toString().trim();
141
- const mb = parseInt(result, 10);
142
- return mb > 0 ? mb : null;
143
- } else if (platform === "linux") {
144
- if (port) {
145
- cmd = `ps aux | grep "[m]sedge" | grep "remote-debugging-port=${port}" | awk '{sum += $6} END {printf "%d", sum/1024}'`;
146
- } else {
147
- cmd =
148
- 'ps aux | grep "[m]sedge" | awk \'{sum += $6} END {printf "%d", sum/1024}\'';
149
- }
150
- const result = execSync(cmd, { timeout: 5000 }).toString().trim();
151
- const mb = parseInt(result, 10);
152
- return mb > 0 ? mb : null;
153
- } else if (platform === "win32") {
154
- const ps1Path = path.join(os.tmpdir(), `edge-mem-${Date.now()}.ps1`);
155
- try {
156
- let ps1Content;
157
- if (port) {
158
- ps1Content =
159
- `$procs = Get-CimInstance Win32_Process -Filter "Name LIKE 'msedge%'" -ErrorAction SilentlyContinue; ` +
160
- `$sum = 0; ` +
161
- `foreach ($p in $procs) { ` +
162
- ` if ($p.CommandLine -and $p.CommandLine -like "*--remote-debugging-port=${port}*") { ` +
163
- ` $sum += $p.WorkingSetSize ` +
164
- ` } ` +
165
- `}; ` +
166
- `Write-Output $sum`;
167
- } else {
168
- // 无端口场景:Get-Process(.NET API,不依赖 WMI)统计全部 msedge 工作集,
169
- // 与原 Get-CimInstance 全量求和行为等价;WMI 损坏/禁用的机器也能返回
170
- ps1Content =
171
- `$procs = Get-Process msedge -ErrorAction SilentlyContinue; ` +
172
- `$sum = 0; ` +
173
- `foreach ($p in $procs) { $sum += $p.WorkingSet64 }; ` +
174
- `Write-Output $sum`;
175
- }
176
- writeFileSync(ps1Path, ps1Content);
177
- cmd = `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}"`;
178
- const result = execSync(cmd, { timeout: 5000 }).toString().trim();
179
- try {
180
- unlinkSync(ps1Path);
181
- } catch {}
182
- const bytes = parseInt(result, 10);
183
- const mb = bytes > 0 ? Math.round(bytes / 1048576) : 0;
184
- return mb > 0 ? mb : null;
185
- } catch {
186
- try {
187
- unlinkSync(ps1Path);
188
- } catch {}
189
- return null;
190
- }
191
- }
192
- return null;
193
- } catch {
194
- return null;
195
- }
196
- }
197
-
198
- function formatMemoryWithBrowser(port = null) {
199
- const nodeMem = formatMemoryUsage();
200
- const edgeMB = getBrowserMemory(port);
201
- if (!edgeMB) return nodeMem;
202
- const portLabel = port ? `:${port}` : "";
203
- return `${nodeMem} edge${portLabel}:${edgeMB}MB`;
204
- }
205
-
206
118
  // ────────────────────────────────────────
207
119
  // Section 3: 页面辅助
208
120
  // ────────────────────────────────────────
@@ -372,10 +284,11 @@ class CdpSessionManager {
372
284
  this.browser = await ensureBrowserReady(opts);
373
285
  this.page = await this._ensurePage();
374
286
  this.lastRecycleAt = Date.now();
375
- // 端口切换时重置运行计时
376
- if (oldPort !== undefined && oldPort !== this.port) {
377
- this.sessionStartTime = Date.now();
378
- }
287
+ // 重建会话后重置运行计时(无论是否换端口)。
288
+ // 旧逻辑只在换端口时重置,而默认 switchPortOnRecycle=false,
289
+ // 导致 48min 定时轮换触发后计时不归零,之后每次内存采样(30s)
290
+ // 都再次满足超时条件 → 每 30s 反复重建会话(“一直在切换”)
291
+ this.sessionStartTime = Date.now();
379
292
  }
380
293
 
381
294
  /** 内存检查 + 定时轮换 + 按需回收(每 30s 最多采样一次) */
@@ -384,7 +297,7 @@ class CdpSessionManager {
384
297
  if (now - this._lastMemCheck < 30000) return;
385
298
  this._lastMemCheck = now;
386
299
 
387
- // 定时轮换:单端口运行超时自动切换
300
+ // 定时轮换:单端口运行超时自动重启浏览器
388
301
  const sessionElapsed = now - this.sessionStartTime;
389
302
  if (sessionElapsed >= CFG.maxSessionRunTimeMs) {
390
303
  const mins = Math.round(sessionElapsed / 60000);
@@ -474,10 +387,14 @@ async function prepareVideoPage(page, href) {
474
387
  await page.keyboard.press("Escape");
475
388
  await new Promise((r) => setTimeout(r, 1000));
476
389
 
477
- // 如果评论面板已打开,先关闭
390
+ // 如果右侧面板已打开(评论/猜你喜欢/商品 tab 面板),先安全关闭
391
+ // 同时识别旧版 RightPanelContainer 与新版 VideoDetailContainer
478
392
  const panelOpen = await safeEvaluate(
479
393
  page,
480
- () => !!document.querySelector('[class*="RightPanelContainer"]'),
394
+ () =>
395
+ !!document.querySelector(
396
+ '[class*="RightPanelContainer"], [class*="VideoDetailContainer"]',
397
+ ),
481
398
  );
482
399
  if (panelOpen) {
483
400
  await closeCommentPanel(page);
@@ -849,7 +766,7 @@ async function runAutoMode(options) {
849
766
 
850
767
  if (consecutiveCaptchaCount >= CFG.maxConsecutiveCaptcha) {
851
768
  console.error(
852
- ` ♻️ 连续${CFG.maxConsecutiveCaptcha}次验证码 → 切换端口`,
769
+ ` ♻️ 连续${CFG.maxConsecutiveCaptcha}次验证码 → 重启浏览器`,
853
770
  );
854
771
  await session.recycleSession("连续验证码", {
855
772
  switchPort: true,
@@ -899,7 +816,7 @@ async function runAutoMode(options) {
899
816
  const remainingMins =
900
817
  remainingMs > 0 ? Math.round(remainingMs / 60000) : 0;
901
818
  console.error(
902
- `── #${totalTasks()} ── ${formatMemoryWithBrowser(session.port)} 处理=${processedCount} 跳过=${skippedCount} 错误=${errorCount} 距切换≈${remainingMins}min`,
819
+ `── #${totalTasks()} ── ${formatMemoryWithBrowser(session.port)} 处理=${processedCount} 跳过=${skippedCount} 错误=${errorCount} 距轮换≈${remainingMins}min`,
903
820
  );
904
821
  }
905
822
  } catch (err) {
@@ -918,7 +835,7 @@ async function runAutoMode(options) {
918
835
  ` 403 (${consecutive403}/${CFG.consecutive403Blocked})`,
919
836
  );
920
837
  if (consecutive403 >= CFG.consecutive403Blocked) {
921
- console.error(" 🚫 连续403 → 等2min切换端口");
838
+ console.error(" 🚫 连续403 → 等2min重启浏览器");
922
839
  await new Promise((r) => setTimeout(r, 120000));
923
840
  await session.recycleSession("连续 403 封禁");
924
841
  consecutive403 = 0;
@@ -931,19 +848,18 @@ async function runAutoMode(options) {
931
848
  console.error(` ${err.message}`);
932
849
  }
933
850
 
934
- // 连续任务错误达到阈值,汇报服务端
851
+ // 连续任务错误达到阈值,汇报服务端 + 触发 daemon fallback 换节点
935
852
  if (consecutiveTaskErrors >= CFG.consecutiveTaskErrorsMax) {
936
853
  console.error(
937
- ` ⚠️ 连续任务错误 ${consecutiveTaskErrors}/${CFG.consecutiveTaskErrorsMax} → 汇报服务端`,
854
+ ` ⚠️ 连续任务错误 ${consecutiveTaskErrors}/${CFG.consecutiveTaskErrorsMax} → 汇报服务端 + 触发 fallback`,
938
855
  );
939
- apiPost(`${serverUrl}/api/error-report`, {
856
+ await reportConsecutiveTaskErrors({
857
+ post: apiPost,
858
+ baseUrl: serverUrl,
940
859
  userId,
941
- username: "",
942
- errorType: "consecutive_task_errors",
943
- errorMessage: `连续 ${consecutiveTaskErrors} 个任务处理失败 (port=${session.port})`,
860
+ message: `连续 ${consecutiveTaskErrors} 个任务处理失败 (port=${session.port})`,
944
861
  stage: "comments",
945
- errorStack: "",
946
- }).catch(() => {});
862
+ });
947
863
  consecutiveTaskErrors = 0;
948
864
  }
949
865
  }
@@ -962,19 +878,18 @@ async function runAutoMode(options) {
962
878
  console.error(` × ${err.message}`);
963
879
  }
964
880
 
965
- // 连续轮间错误达到阈值,汇报服务端
881
+ // 连续轮间错误达到阈值,汇报服务端 + 触发 daemon fallback 换节点
966
882
  if (consecutiveTaskErrors >= CFG.consecutiveTaskErrorsMax) {
967
883
  console.error(
968
- ` ⚠️ 连续轮间错误 ${consecutiveTaskErrors}/${CFG.consecutiveTaskErrorsMax} → 汇报服务端`,
884
+ ` ⚠️ 连续轮间错误 ${consecutiveTaskErrors}/${CFG.consecutiveTaskErrorsMax} → 汇报服务端 + 触发 fallback`,
969
885
  );
970
- apiPost(`${serverUrl}/api/error-report`, {
886
+ await reportConsecutiveTaskErrors({
887
+ post: apiPost,
888
+ baseUrl: serverUrl,
971
889
  userId,
972
- username: "",
973
- errorType: "consecutive_task_errors",
974
- errorMessage: `连续 ${consecutiveTaskErrors} 个轮次处理失败 (port=${session.port})`,
890
+ message: `连续 ${consecutiveTaskErrors} 个轮次处理失败 (port=${session.port})`,
975
891
  stage: "comments",
976
- errorStack: "",
977
- }).catch(() => {});
892
+ });
978
893
  consecutiveTaskErrors = 0;
979
894
  }
980
895
  }
@@ -557,6 +557,30 @@ export async function handleExplore(options) {
557
557
  }
558
558
  }
559
559
 
560
+ // 兜底:上报错误前再识别一次验证码
561
+ // 网络错误(如连续请求失败)常伴随验证码/限流页,而 explore-core 的 detectCaptcha
562
+ // 可能在验证码延迟渲染前已执行,或已识别到( captchaDetected=true )但后续流程又抛了异常出现 error。
563
+ // 这里在把错误上报为 network/other 前对当前页面再检测一次,识别到则转走验证码分支。
564
+ if (result.error) {
565
+ try {
566
+ const lateCaptcha = await detectCaptcha(page);
567
+ if (lateCaptcha && lateCaptcha.visible) {
568
+ console.error(` [验证码] 兜底识别到验证码 @${username}`);
569
+ result.captchaDetected = true;
570
+ result.captchaStage = result.captchaStage || "process";
571
+ result.captchaMessage =
572
+ result.captchaMessage || "上报错误前识别到验证码";
573
+ result.error = null;
574
+ result.errorStack = "";
575
+ result.retryable = false;
576
+ consecutiveNetworkErrors = 0;
577
+ errorCount--;
578
+ }
579
+ } catch (e) {
580
+ // 页面可能已销毁(浏览器崩溃等),忽略兜底检测异常
581
+ }
582
+ }
583
+
560
584
  if (result.error) {
561
585
  // 上报错误(重试后仍有错误才上报)
562
586
  await apiPost(`${serverUrl}/api/job/${username}`, {