tt-help-cli-ycl 1.4.136 → 1.4.139

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.136",
3
+ "version": "1.4.139",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
  // ────────────────────────────────────────
@@ -384,7 +296,7 @@ class CdpSessionManager {
384
296
  if (now - this._lastMemCheck < 30000) return;
385
297
  this._lastMemCheck = now;
386
298
 
387
- // 定时轮换:单端口运行超时自动切换
299
+ // 定时轮换:单端口运行超时自动重启浏览器
388
300
  const sessionElapsed = now - this.sessionStartTime;
389
301
  if (sessionElapsed >= CFG.maxSessionRunTimeMs) {
390
302
  const mins = Math.round(sessionElapsed / 60000);
@@ -474,10 +386,14 @@ async function prepareVideoPage(page, href) {
474
386
  await page.keyboard.press("Escape");
475
387
  await new Promise((r) => setTimeout(r, 1000));
476
388
 
477
- // 如果评论面板已打开,先关闭
389
+ // 如果右侧面板已打开(评论/猜你喜欢/商品 tab 面板),先安全关闭
390
+ // 同时识别旧版 RightPanelContainer 与新版 VideoDetailContainer
478
391
  const panelOpen = await safeEvaluate(
479
392
  page,
480
- () => !!document.querySelector('[class*="RightPanelContainer"]'),
393
+ () =>
394
+ !!document.querySelector(
395
+ '[class*="RightPanelContainer"], [class*="VideoDetailContainer"]',
396
+ ),
481
397
  );
482
398
  if (panelOpen) {
483
399
  await closeCommentPanel(page);
@@ -849,7 +765,7 @@ async function runAutoMode(options) {
849
765
 
850
766
  if (consecutiveCaptchaCount >= CFG.maxConsecutiveCaptcha) {
851
767
  console.error(
852
- ` ♻️ 连续${CFG.maxConsecutiveCaptcha}次验证码 → 切换端口`,
768
+ ` ♻️ 连续${CFG.maxConsecutiveCaptcha}次验证码 → 重启浏览器`,
853
769
  );
854
770
  await session.recycleSession("连续验证码", {
855
771
  switchPort: true,
@@ -899,7 +815,7 @@ async function runAutoMode(options) {
899
815
  const remainingMins =
900
816
  remainingMs > 0 ? Math.round(remainingMs / 60000) : 0;
901
817
  console.error(
902
- `── #${totalTasks()} ── ${formatMemoryWithBrowser(session.port)} 处理=${processedCount} 跳过=${skippedCount} 错误=${errorCount} 距切换≈${remainingMins}min`,
818
+ `── #${totalTasks()} ── ${formatMemoryWithBrowser(session.port)} 处理=${processedCount} 跳过=${skippedCount} 错误=${errorCount} 距轮换≈${remainingMins}min`,
903
819
  );
904
820
  }
905
821
  } catch (err) {
@@ -918,7 +834,7 @@ async function runAutoMode(options) {
918
834
  ` 403 (${consecutive403}/${CFG.consecutive403Blocked})`,
919
835
  );
920
836
  if (consecutive403 >= CFG.consecutive403Blocked) {
921
- console.error(" 🚫 连续403 → 等2min切换端口");
837
+ console.error(" 🚫 连续403 → 等2min重启浏览器");
922
838
  await new Promise((r) => setTimeout(r, 120000));
923
839
  await session.recycleSession("连续 403 封禁");
924
840
  consecutive403 = 0;
@@ -931,19 +847,18 @@ async function runAutoMode(options) {
931
847
  console.error(` ${err.message}`);
932
848
  }
933
849
 
934
- // 连续任务错误达到阈值,汇报服务端
850
+ // 连续任务错误达到阈值,汇报服务端 + 触发 daemon fallback 换节点
935
851
  if (consecutiveTaskErrors >= CFG.consecutiveTaskErrorsMax) {
936
852
  console.error(
937
- ` ⚠️ 连续任务错误 ${consecutiveTaskErrors}/${CFG.consecutiveTaskErrorsMax} → 汇报服务端`,
853
+ ` ⚠️ 连续任务错误 ${consecutiveTaskErrors}/${CFG.consecutiveTaskErrorsMax} → 汇报服务端 + 触发 fallback`,
938
854
  );
939
- apiPost(`${serverUrl}/api/error-report`, {
855
+ await reportConsecutiveTaskErrors({
856
+ post: apiPost,
857
+ baseUrl: serverUrl,
940
858
  userId,
941
- username: "",
942
- errorType: "consecutive_task_errors",
943
- errorMessage: `连续 ${consecutiveTaskErrors} 个任务处理失败 (port=${session.port})`,
859
+ message: `连续 ${consecutiveTaskErrors} 个任务处理失败 (port=${session.port})`,
944
860
  stage: "comments",
945
- errorStack: "",
946
- }).catch(() => {});
861
+ });
947
862
  consecutiveTaskErrors = 0;
948
863
  }
949
864
  }
@@ -962,19 +877,18 @@ async function runAutoMode(options) {
962
877
  console.error(` × ${err.message}`);
963
878
  }
964
879
 
965
- // 连续轮间错误达到阈值,汇报服务端
880
+ // 连续轮间错误达到阈值,汇报服务端 + 触发 daemon fallback 换节点
966
881
  if (consecutiveTaskErrors >= CFG.consecutiveTaskErrorsMax) {
967
882
  console.error(
968
- ` ⚠️ 连续轮间错误 ${consecutiveTaskErrors}/${CFG.consecutiveTaskErrorsMax} → 汇报服务端`,
883
+ ` ⚠️ 连续轮间错误 ${consecutiveTaskErrors}/${CFG.consecutiveTaskErrorsMax} → 汇报服务端 + 触发 fallback`,
969
884
  );
970
- apiPost(`${serverUrl}/api/error-report`, {
885
+ await reportConsecutiveTaskErrors({
886
+ post: apiPost,
887
+ baseUrl: serverUrl,
971
888
  userId,
972
- username: "",
973
- errorType: "consecutive_task_errors",
974
- errorMessage: `连续 ${consecutiveTaskErrors} 个轮次处理失败 (port=${session.port})`,
889
+ message: `连续 ${consecutiveTaskErrors} 个轮次处理失败 (port=${session.port})`,
975
890
  stage: "comments",
976
- errorStack: "",
977
- }).catch(() => {});
891
+ });
978
892
  consecutiveTaskErrors = 0;
979
893
  }
980
894
  }
@@ -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}`, {