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 +1 -1
- package/src/cli/comments.js +35 -121
- package/src/cli/explore.js +24 -0
- package/src/cli/tag.js +261 -210
- package/src/cli/webserver.js +6 -0
- package/src/lib/api-interceptor-comment.js +14 -2
- package/src/lib/args.js +24 -0
- package/src/lib/browser/page.js +34 -6
- package/src/lib/browser-memory.js +95 -0
- package/src/lib/client-error-report.js +82 -0
- package/src/lib/constants.js +4 -0
- package/src/lib/tiktok-scraper.mjs +42 -0
- package/src/webserver/server.mjs +133 -1
package/package.json
CHANGED
package/src/cli/comments.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
// 手动模式: tt-help comments <URL> [N] [--save] 单视频评论提取
|
|
6
6
|
// ────────────────────────────────────────
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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:
|
|
70
|
+
/** 内存回收 — 回收时是否切换端口(默认关闭:只重启浏览器不换端口) */
|
|
71
|
+
switchPortOnRecycle: false,
|
|
71
72
|
/** 连续任务处理错误阈值(达到后上报服务端) */
|
|
72
73
|
consecutiveTaskErrorsMax: 20,
|
|
73
|
-
/** 单端口最大运行时间(ms)
|
|
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 || "
|
|
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
|
-
() =>
|
|
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}
|
|
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
|
-
|
|
855
|
+
await reportConsecutiveTaskErrors({
|
|
856
|
+
post: apiPost,
|
|
857
|
+
baseUrl: serverUrl,
|
|
940
858
|
userId,
|
|
941
|
-
|
|
942
|
-
errorType: "consecutive_task_errors",
|
|
943
|
-
errorMessage: `连续 ${consecutiveTaskErrors} 个任务处理失败 (port=${session.port})`,
|
|
859
|
+
message: `连续 ${consecutiveTaskErrors} 个任务处理失败 (port=${session.port})`,
|
|
944
860
|
stage: "comments",
|
|
945
|
-
|
|
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
|
-
|
|
885
|
+
await reportConsecutiveTaskErrors({
|
|
886
|
+
post: apiPost,
|
|
887
|
+
baseUrl: serverUrl,
|
|
971
888
|
userId,
|
|
972
|
-
|
|
973
|
-
errorType: "consecutive_task_errors",
|
|
974
|
-
errorMessage: `连续 ${consecutiveTaskErrors} 个轮次处理失败 (port=${session.port})`,
|
|
889
|
+
message: `连续 ${consecutiveTaskErrors} 个轮次处理失败 (port=${session.port})`,
|
|
975
890
|
stage: "comments",
|
|
976
|
-
|
|
977
|
-
}).catch(() => {});
|
|
891
|
+
});
|
|
978
892
|
consecutiveTaskErrors = 0;
|
|
979
893
|
}
|
|
980
894
|
}
|
package/src/cli/explore.js
CHANGED
|
@@ -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}`, {
|