tt-help-cli-ycl 1.4.152 → 1.4.154
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
|
@@ -16,8 +16,13 @@ function check(name, ok, detail = "") {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// 429 + 页面有验证码容器
|
|
19
|
-
|
|
19
|
+
// captchaPhase 控制验证码何时出现:
|
|
20
|
+
// "instant" → 首次检测时已渲染(进分支 5s 检测命中)
|
|
21
|
+
// "delayed" → 首次检测时未渲染,进入重试等待后才出现(sleep 醒来后 3s 检测命中)
|
|
22
|
+
// "never" → 不出现(走正常被封重试)
|
|
23
|
+
function make429CaptchaPage(username, captchaPhase = "instant") {
|
|
20
24
|
let gotoCount = 0;
|
|
25
|
+
let captchaDelayElapsed = false;
|
|
21
26
|
const page = {
|
|
22
27
|
_handlers: {},
|
|
23
28
|
on(event, fn) {
|
|
@@ -33,12 +38,16 @@ function make429CaptchaPage(username) {
|
|
|
33
38
|
gotoCount++;
|
|
34
39
|
},
|
|
35
40
|
// checkCdpHealth 用 () => document.readyState(fn.length===0)→ "complete"
|
|
36
|
-
// detectCaptcha
|
|
37
|
-
_captchaVisible
|
|
41
|
+
// detectCaptcha/detectCaptchaWithWait 通过 captchaDelayElapsed 开关控制
|
|
42
|
+
_captchaVisible() {
|
|
43
|
+
if (captchaPhase === "never") return false;
|
|
44
|
+
if (captchaPhase === "instant") return true;
|
|
45
|
+
return captchaDelayElapsed; // delayed:等待后才可见
|
|
46
|
+
},
|
|
38
47
|
evaluate: async (fn) => {
|
|
39
48
|
if (
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
fn.toString().includes("captcha-verify-container") &&
|
|
50
|
+
page._captchaVisible()
|
|
42
51
|
) {
|
|
43
52
|
return {
|
|
44
53
|
exists: true,
|
|
@@ -50,8 +59,18 @@ function make429CaptchaPage(username) {
|
|
|
50
59
|
if (fn.length === 1) return null;
|
|
51
60
|
return "complete";
|
|
52
61
|
},
|
|
62
|
+
waitForFunction: async (fn, opts) => {
|
|
63
|
+
// 带等待检测:轮询语义——可见即返回 handle,否则等满 timeout 返回 null
|
|
64
|
+
const deadline = Date.now() + (opts?.timeout || 5000);
|
|
65
|
+
while (Date.now() < deadline) {
|
|
66
|
+
if (page._captchaVisible()) {
|
|
67
|
+
return { jsonValue: async () => true };
|
|
68
|
+
}
|
|
69
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
},
|
|
53
73
|
reload: async () => {},
|
|
54
|
-
waitForFunction: async () => null,
|
|
55
74
|
waitForResponse: async (pred) => {
|
|
56
75
|
const resp = {
|
|
57
76
|
url: () => "https://www.tiktok.com/api/post/item_list/?...",
|
|
@@ -68,34 +87,61 @@ function make429CaptchaPage(username) {
|
|
|
68
87
|
get gotoCount() {
|
|
69
88
|
return gotoCount;
|
|
70
89
|
},
|
|
90
|
+
// 测试钩子:标记 delayed 验证码已出现
|
|
91
|
+
_markCaptchaDelayed() {
|
|
92
|
+
captchaDelayElapsed = true;
|
|
93
|
+
},
|
|
71
94
|
};
|
|
72
95
|
return page;
|
|
73
96
|
}
|
|
74
97
|
|
|
75
|
-
// 用例1:429 +
|
|
98
|
+
// 用例1:429 + 验证码立即可见 → 首次检测命中,只 goto 1 次(无重试)
|
|
76
99
|
{
|
|
77
|
-
const page = make429CaptchaPage("fumolbbf08o");
|
|
100
|
+
const page = make429CaptchaPage("fumolbbf08o", "instant");
|
|
78
101
|
try {
|
|
79
102
|
await fetchUserVideosAPI(page, "fumolbbf08o", 5, () => {});
|
|
80
|
-
check("429+验证码 立即转验证码", false, "未抛错");
|
|
103
|
+
check("429+验证码(即时) 立即转验证码", false, "未抛错");
|
|
81
104
|
} catch (e) {
|
|
82
105
|
check(
|
|
83
|
-
"429+验证码 立即转验证码",
|
|
106
|
+
"429+验证码(即时) 立即转验证码",
|
|
84
107
|
e.message === "验证码: fumolbbf08o",
|
|
85
108
|
`抛错 "${e.message}"`,
|
|
86
109
|
);
|
|
87
110
|
}
|
|
88
111
|
check(
|
|
89
|
-
"429+验证码 不再重试(仅 1 次 goto)",
|
|
112
|
+
"429+验证码(即时) 不再重试(仅 1 次 goto)",
|
|
90
113
|
page.gotoCount === 1,
|
|
91
114
|
`goto 次数 = ${page.gotoCount}`,
|
|
92
115
|
);
|
|
93
116
|
}
|
|
94
117
|
|
|
95
|
-
// 用例2
|
|
118
|
+
// 用例2(核心场景):429 首检时验证码未渲染 → 重试等待期间弹出
|
|
119
|
+
// → sleep 醒来后的检测必须抓住它(下次 goto 冲掉前),抛 "验证码:"
|
|
120
|
+
{
|
|
121
|
+
const page = make429CaptchaPage("fumolbbf08o", "delayed");
|
|
122
|
+
// 在 waitForFunction 轮询到 captchaDelayElapsed=false 一轮后标记出现,
|
|
123
|
+
// 模拟"等待期渲染":用 setTimeout 在重试 sleep 期间触发
|
|
124
|
+
setTimeout(() => page._markCaptchaDelayed(), 50);
|
|
125
|
+
try {
|
|
126
|
+
await fetchUserVideosAPI(page, "fumolbbf08o", 5, () => {});
|
|
127
|
+
check("等待期间弹出的验证码 被醒来检测抓住", false, "未抛错");
|
|
128
|
+
} catch (e) {
|
|
129
|
+
check(
|
|
130
|
+
"等待期间弹出的验证码 被醒来检测抓住",
|
|
131
|
+
e.message === "验证码: fumolbbf08o",
|
|
132
|
+
`抛错 "${e.message}",goto ${page.gotoCount} 次`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
check(
|
|
136
|
+
"醒来检测抓住时未再发起下一轮 goto(goto ≤ 2)",
|
|
137
|
+
page.gotoCount <= 2,
|
|
138
|
+
`goto 次数 = ${page.gotoCount}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 用例3:429 + 验证码始终不出现 → 走正常被封重试,最终抛 "被封:"
|
|
96
143
|
{
|
|
97
|
-
const page = make429CaptchaPage("fumolbbf08o");
|
|
98
|
-
page._captchaVisible = false;
|
|
144
|
+
const page = make429CaptchaPage("fumolbbf08o", "never");
|
|
99
145
|
try {
|
|
100
146
|
await fetchUserVideosAPI(page, "fumolbbf08o", 5, () => {});
|
|
101
147
|
check("429无验证码 仍按被封重试", false, "未抛错");
|
|
@@ -108,15 +154,11 @@ function make429CaptchaPage(username) {
|
|
|
108
154
|
}
|
|
109
155
|
}
|
|
110
156
|
|
|
111
|
-
// 用例
|
|
157
|
+
// 用例4:429 + waitForFunction/检测本身抛异常(上下文销毁)→ 忽略,按被封重试
|
|
112
158
|
{
|
|
113
|
-
const page = make429CaptchaPage("fumolbbf08o");
|
|
114
|
-
page.
|
|
115
|
-
|
|
116
|
-
throw new Error("Execution context was destroyed");
|
|
117
|
-
}
|
|
118
|
-
if (fn.length === 1) return null;
|
|
119
|
-
return "complete";
|
|
159
|
+
const page = make429CaptchaPage("fumolbbf08o", "never");
|
|
160
|
+
page.waitForFunction = async () => {
|
|
161
|
+
throw new Error("Execution context was destroyed");
|
|
120
162
|
};
|
|
121
163
|
try {
|
|
122
164
|
await fetchUserVideosAPI(page, "fumolbbf08o", 5, () => {});
|
package/src/cli/explore.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../scraper/modules/page-helpers.js";
|
|
11
11
|
import {
|
|
12
12
|
detectCaptcha,
|
|
13
|
+
detectCaptchaWithWait,
|
|
13
14
|
closeCaptcha,
|
|
14
15
|
} from "../scraper/modules/captcha-handler.js";
|
|
15
16
|
import {
|
|
@@ -578,7 +579,8 @@ export async function handleExplore(options) {
|
|
|
578
579
|
// 先记住原错误类型:兜底转验证码时带上,服务端可同时看到"被判+验证码"
|
|
579
580
|
const wasBlocked = result.error.startsWith("被封:");
|
|
580
581
|
try {
|
|
581
|
-
|
|
582
|
+
// 带等待检测:验证码容器晚于页面加载渲染,瞬时检测常漏(尤其被判场景)
|
|
583
|
+
const lateCaptcha = await detectCaptchaWithWait(page, 5000);
|
|
582
584
|
if (lateCaptcha && lateCaptcha.visible) {
|
|
583
585
|
console.error(
|
|
584
586
|
` [验证码] 兜底识别到验证码 @${username}${wasBlocked ? "(被判后弹出)" : ""}`,
|
package/src/cli/refresh-mini.js
CHANGED
package/src/cli/refresh.js
CHANGED
|
@@ -72,6 +72,8 @@ export async function handleRefresh(options) {
|
|
|
72
72
|
exploreWebserver,
|
|
73
73
|
exploreNoProduct,
|
|
74
74
|
exploreMini,
|
|
75
|
+
exploreSwitchDelay,
|
|
76
|
+
exploreCommentDelay,
|
|
75
77
|
} = options;
|
|
76
78
|
// --mini 模式:只采集视频列表及由视频推导的信息(EC视频、最新发布时间),
|
|
77
79
|
// 跳过用户信息、国家采样、关注/粉丝提取(最大耗时项)和商品详情抓取(由独立商品程序异步补抓),大幅提速。
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { delay } from "./delay.js";
|
|
2
2
|
import { assertPageUrl } from "./browser/page.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
detectCaptcha,
|
|
5
|
+
detectCaptchaWithWait,
|
|
6
|
+
} from "../scraper/modules/captcha-handler.js";
|
|
4
7
|
import {
|
|
5
8
|
detectPageError,
|
|
6
9
|
detectPageErrorWithWait,
|
|
@@ -392,19 +395,25 @@ async function fetchUserVideosAPI(
|
|
|
392
395
|
? "空响应(0 字节)"
|
|
393
396
|
: `限流(HTTP ${httpStatus}, ${bodyText.slice(0, 60)})`;
|
|
394
397
|
// 被封形态(尤其 429/ratelimit)常伴随页面弹出验证码:此时继续重试无意义,
|
|
395
|
-
// 人工解决验证码才是正解。检测到立即抛 "验证码:",跳过剩余 30/40/50s
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
);
|
|
402
|
-
|
|
398
|
+
// 人工解决验证码才是正解。检测到立即抛 "验证码:",跳过剩余 30/40/50s 空转重试。
|
|
399
|
+
// 验证码容器晚于 domcontentloaded 渲染,瞬时检测常因"太早"漏检 → 用带等待检测;
|
|
400
|
+
// 等待期(30/40/50s)内弹出的验证码会被下一轮 goto 冲掉,
|
|
401
|
+
// 所以每轮 sleep 醒来后、下次 goto 前还要再检一次
|
|
402
|
+
const captchaHit = async (waitMs, when) => {
|
|
403
|
+
try {
|
|
404
|
+
const captcha = await detectCaptchaWithWait(page, waitMs);
|
|
405
|
+
if (captcha && captcha.visible) {
|
|
406
|
+
log(
|
|
407
|
+
` [API拦截] item_list ${reason},${when}弹出验证码,转验证码处理(不再重试)`,
|
|
408
|
+
);
|
|
409
|
+
throw new Error(`验证码: ${username}`);
|
|
410
|
+
}
|
|
411
|
+
} catch (e) {
|
|
412
|
+
if (e.message && e.message.startsWith("验证码:")) throw e;
|
|
413
|
+
// 检测本身失败(页面导航中/上下文销毁等):忽略,继续按被封重试
|
|
403
414
|
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// 检测本身失败(页面导航中/上下文销毁等):忽略,继续按被封重试
|
|
407
|
-
}
|
|
415
|
+
};
|
|
416
|
+
await captchaHit(5000, "页面已");
|
|
408
417
|
const retryIndex = attempt - 1;
|
|
409
418
|
if (retryIndex < retryWaits.length) {
|
|
410
419
|
const waitMs = retryWaits[retryIndex];
|
|
@@ -412,8 +421,12 @@ async function fetchUserVideosAPI(
|
|
|
412
421
|
` [API拦截] item_list ${reason},判定为被封,${waitMs / 1000}s 后第 ${attempt}/${retryWaits.length} 次重试...`,
|
|
413
422
|
);
|
|
414
423
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
424
|
+
// 关键时机:等待期间验证码才会真正渲染出来(被 ban 后前端延迟弹窗),
|
|
425
|
+
// 下次 goto 会把它冲掉 —— sleep 醒来后立即检测,抓住它
|
|
426
|
+
await captchaHit(3000, "等待期间");
|
|
415
427
|
continue;
|
|
416
428
|
}
|
|
429
|
+
await captchaHit(5000, "重试耗尽后");
|
|
417
430
|
log(
|
|
418
431
|
` [API拦截] item_list ${reason},重试 ${retryWaits.length} 次仍被封`,
|
|
419
432
|
);
|
|
@@ -17,6 +17,29 @@ export async function detectCaptcha(page) {
|
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* 带等待的验证码检测:轮询等待 .captcha-verify-container 出现且可见。
|
|
22
|
+
* 验证码容器由前端 JS 延迟渲染(晚于 domcontentloaded / item_list 响应),
|
|
23
|
+
* 瞬时 detectCaptcha 常因"检测太早"漏检;超时未出现返回 null。
|
|
24
|
+
*/
|
|
25
|
+
export async function detectCaptchaWithWait(page, timeoutMs = 5000) {
|
|
26
|
+
try {
|
|
27
|
+
const handle = await page.waitForFunction(
|
|
28
|
+
() => {
|
|
29
|
+
const c = document.querySelector('.captcha-verify-container');
|
|
30
|
+
return !!(c && c.offsetParent !== null);
|
|
31
|
+
},
|
|
32
|
+
{ timeout: timeoutMs },
|
|
33
|
+
);
|
|
34
|
+
const appeared = handle ? await handle.jsonValue().catch(() => false) : false;
|
|
35
|
+
if (!appeared) return null;
|
|
36
|
+
// 出现后用原函数拿完整信息(rect 等);异常按未出现处理
|
|
37
|
+
return (await detectCaptcha(page).catch(() => null)) || { exists: true, visible: true };
|
|
38
|
+
} catch {
|
|
39
|
+
return null; // 超时或页面异常:未出现
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
export async function closeCaptcha(page) {
|
|
21
44
|
return page.evaluate(() => {
|
|
22
45
|
const closeBtn = document.getElementById('captcha_close_button');
|