tt-help-cli-ycl 1.4.153 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.153",
3
+ "version": "1.4.154",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,8 +16,13 @@ function check(name, ok, detail = "") {
16
16
  }
17
17
 
18
18
  // 429 + 页面有验证码容器
19
- function make429CaptchaPage(username) {
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 用 () => {...}(fn.length===0,通过 captchaVisible 开关控制返回值)
37
- _captchaVisible: true,
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
- page._captchaVisible &&
41
- fn.toString().includes("captcha-verify-container")
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 + 验证码可见 → 立即抛 "验证码:",只 goto 1 次(无重试)
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:429 + 验证码不可见 → 仍走正常被封重试,最终抛 "被封:"
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
- // 用例3:429 + detectCaptcha 本身抛异常(页面上下文销毁)→ 忽略,按被封重试
157
+ // 用例4:429 + waitForFunction/检测本身抛异常(上下文销毁)→ 忽略,按被封重试
112
158
  {
113
- const page = make429CaptchaPage("fumolbbf08o");
114
- page.evaluate = async (fn) => {
115
- if (fn.toString().includes("captcha-verify-container")) {
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, () => {});
@@ -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
- const lateCaptcha = await detectCaptcha(page);
582
+ // 带等待检测:验证码容器晚于页面加载渲染,瞬时检测常漏(尤其被判场景)
583
+ const lateCaptcha = await detectCaptchaWithWait(page, 5000);
582
584
  if (lateCaptcha && lateCaptcha.visible) {
583
585
  console.error(
584
586
  ` [验证码] 兜底识别到验证码 @${username}${wasBlocked ? "(被判后弹出)" : ""}`,
@@ -1,6 +1,9 @@
1
1
  import { delay } from "./delay.js";
2
2
  import { assertPageUrl } from "./browser/page.js";
3
- import { detectCaptcha } from "../scraper/modules/captcha-handler.js";
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
- try {
397
- const captcha = await detectCaptcha(page);
398
- if (captcha && captcha.visible) {
399
- log(
400
- ` [API拦截] item_list ${reason},页面已弹出验证码,转验证码处理(不再重试)`,
401
- );
402
- throw new Error(`验证码: ${username}`);
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
- } catch (e) {
405
- if (e.message && e.message.startsWith("验证码:")) throw e;
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');