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.
- package/package.json +1 -1
- package/scripts/test-webserver-error-classify.mjs +379 -0
- package/src/clash/checker.js +14 -2
- package/src/clash/daemon.js +7 -1
- package/src/cli/comments.js +40 -125
- 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/parse-ssr.mjs +13 -1
- package/src/lib/port.js +72 -0
- package/src/lib/tiktok-scraper.mjs +104 -4
- package/src/watch/public/app.js +67 -23
- package/src/watch/public/style.css +18 -0
- package/src/webserver/server.mjs +354 -39
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
|
// ────────────────────────────────────────
|
|
@@ -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
|
-
|
|
377
|
-
|
|
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
|
-
() =>
|
|
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}
|
|
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
|
-
|
|
856
|
+
await reportConsecutiveTaskErrors({
|
|
857
|
+
post: apiPost,
|
|
858
|
+
baseUrl: serverUrl,
|
|
940
859
|
userId,
|
|
941
|
-
|
|
942
|
-
errorType: "consecutive_task_errors",
|
|
943
|
-
errorMessage: `连续 ${consecutiveTaskErrors} 个任务处理失败 (port=${session.port})`,
|
|
860
|
+
message: `连续 ${consecutiveTaskErrors} 个任务处理失败 (port=${session.port})`,
|
|
944
861
|
stage: "comments",
|
|
945
|
-
|
|
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
|
-
|
|
886
|
+
await reportConsecutiveTaskErrors({
|
|
887
|
+
post: apiPost,
|
|
888
|
+
baseUrl: serverUrl,
|
|
971
889
|
userId,
|
|
972
|
-
|
|
973
|
-
errorType: "consecutive_task_errors",
|
|
974
|
-
errorMessage: `连续 ${consecutiveTaskErrors} 个轮次处理失败 (port=${session.port})`,
|
|
890
|
+
message: `连续 ${consecutiveTaskErrors} 个轮次处理失败 (port=${session.port})`,
|
|
975
891
|
stage: "comments",
|
|
976
|
-
|
|
977
|
-
}).catch(() => {});
|
|
892
|
+
});
|
|
978
893
|
consecutiveTaskErrors = 0;
|
|
979
894
|
}
|
|
980
895
|
}
|
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}`, {
|