tt-help-cli-ycl 1.3.34 → 1.3.36

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.
Files changed (62) hide show
  1. package/README.md +33 -17
  2. package/cli.js +9 -9
  3. package/package.json +49 -47
  4. package/scripts/run-explore copy.bat +101 -101
  5. package/scripts/run-explore.bat +132 -132
  6. package/scripts/run-explore.ps1 +157 -157
  7. package/scripts/run-explore.sh +119 -119
  8. package/scripts/test-captcha-lib.mjs +68 -0
  9. package/scripts/test-captcha.mjs +81 -0
  10. package/scripts/test-incognito-lib.mjs +36 -0
  11. package/scripts/test-login-state.mjs +128 -0
  12. package/scripts/test-safe-click.mjs +45 -0
  13. package/scripts/test-watch-db-smoke.mjs +246 -0
  14. package/src/cli/attach.js +240 -180
  15. package/src/cli/auto.js +265 -240
  16. package/src/cli/comments.js +301 -210
  17. package/src/cli/config.js +170 -152
  18. package/src/cli/db-import.js +51 -0
  19. package/src/cli/explore.js +519 -488
  20. package/src/cli/info.js +88 -88
  21. package/src/cli/open.js +111 -111
  22. package/src/cli/progress.js +111 -111
  23. package/src/cli/refresh.js +288 -216
  24. package/src/cli/scrape.js +47 -47
  25. package/src/cli/utils.js +18 -18
  26. package/src/cli/videos.js +41 -41
  27. package/src/cli/videostats.js +140 -25
  28. package/src/cli/watch.js +30 -31
  29. package/src/lib/args.js +766 -722
  30. package/src/lib/browser/anti-detect.js +23 -23
  31. package/src/lib/browser/cdp.js +261 -261
  32. package/src/lib/browser/health-checker.js +114 -114
  33. package/src/lib/browser/launch.js +43 -43
  34. package/src/lib/browser/page.js +183 -183
  35. package/src/lib/constants.js +238 -216
  36. package/src/lib/delay.js +54 -54
  37. package/src/lib/explore-fetch.js +118 -118
  38. package/src/lib/fetcher.js +45 -45
  39. package/src/lib/filter.js +66 -66
  40. package/src/lib/io.js +54 -54
  41. package/src/lib/output.js +80 -80
  42. package/src/lib/page-error-detector.js +105 -105
  43. package/src/lib/parse-ssr.mjs +69 -69
  44. package/src/lib/parser.js +47 -47
  45. package/src/lib/retry.js +45 -45
  46. package/src/lib/scrape.js +89 -89
  47. package/src/lib/tiktok-scraper.mjs +232 -194
  48. package/src/lib/url.js +52 -52
  49. package/src/main.js +70 -48
  50. package/src/results/user-videos-bar.lar.lar.moeta.json +37 -0
  51. package/src/scraper/auto-core.js +203 -203
  52. package/src/scraper/core.js +211 -211
  53. package/src/scraper/explore-core.js +177 -167
  54. package/src/scraper/modules/captcha-handler.js +114 -114
  55. package/src/scraper/modules/follow-extractor.js +194 -194
  56. package/src/scraper/modules/guess-extractor.js +51 -51
  57. package/src/scraper/modules/page-helpers.js +48 -48
  58. package/src/scraper/refresh-core.js +179 -179
  59. package/src/videos/core.js +125 -125
  60. package/src/watch/data-store.js +2364 -1030
  61. package/src/watch/public/index.html +1494 -753
  62. package/src/watch/server.js +628 -933
@@ -1,167 +1,177 @@
1
- import { ensureBrowserReady, delay } from "./modules/page-helpers.js";
2
- import { detectCaptcha } from "./modules/captcha-handler.js";
3
- export { ensureBrowserReady };
4
- import { getUserInfo, collectVideos } from "../videos/core.js";
5
- import { extractFollowAndFollowers } from "./modules/follow-extractor.js";
6
- import { extractVideoLocation } from "../lib/scrape.js";
7
- import {
8
- maxFollowing as globalMaxFollowing,
9
- maxFollowers as globalMaxFollowers,
10
- } from "../lib/constants.js";
11
-
12
- async function processExplore(page, username, options, log) {
13
- const {
14
- maxVideos = 16,
15
- enableFollow = true,
16
- loggedIn = false, // 由外部传入登录状态,避免每次调用 isLoggedIn(page)
17
- maxFollowing = 50,
18
- maxFollowers = 50,
19
- location = "PL,NL,BE,DE,FR,IT,ES,IE",
20
- } = options;
21
-
22
- const result = {
23
- userInfo: null,
24
- discoveredVideoAuthors: [],
25
- discoveredCommentAuthors: [],
26
- discoveredGuessAuthors: [],
27
- discoveredFollowing: [],
28
- discoveredFollowers: [],
29
- collectedVideos: 0,
30
- processed: false,
31
- hasFollowData: false,
32
- keepFollow: false,
33
- locationCreated: null,
34
- noVideo: false,
35
- error: null,
36
- };
37
-
38
- try {
39
- log(` 访问 @${username} 主页...`);
40
- const videoList = await collectVideos(page, username, maxVideos, log);
41
-
42
- log(" 获取用户信息...");
43
- const info = await getUserInfo(page);
44
- if (info) {
45
- result.userInfo = info;
46
- log(
47
- ` 用户: ${info.nickname || username} | 粉丝: ${info.followerCount || "-"} | 视频: ${info.videoCount || "-"}`,
48
- );
49
- }
50
-
51
- const captcha = await detectCaptcha(page);
52
- if (captcha && captcha.visible) {
53
- log(`[验证码] @${username} 页面出现验证码`);
54
- result.captchaDetected = true;
55
- result.captchaStage = result.captchaStage || "video-page";
56
- result.captchaMessage = result.captchaMessage || "视频页出现验证码";
57
- }
58
- const videoArray = videoList ? [...videoList.values()] : [];
59
- result.collectedVideos = videoArray.length;
60
-
61
- if (videoArray.length <= 0) {
62
- // 视频为空:可能是页面受限或用户真的没有视频
63
- result.processed = true;
64
- result.noVideo = true;
65
- log(` @${username} 没有视频,标记已处理`);
66
- return result;
67
- }
68
-
69
- // 从第一个视频获取 locationCreated
70
- let locationCreated = null;
71
- if (videoArray.length > 0) {
72
- const firstVideo = videoArray[0];
73
- const firstVideoUrl = firstVideo.href.startsWith("http")
74
- ? firstVideo.href
75
- : `https://www.tiktok.com${firstVideo.href}`;
76
-
77
- locationCreated = await extractVideoLocation(firstVideoUrl);
78
- }
79
-
80
- result.locationCreated = locationCreated || null;
81
- log(` 国家: ${result.locationCreated || "未知"}`);
82
-
83
- // 国家筛选
84
- const locationList = (location || "ES")
85
- .split(",")
86
- .map((s) => s.trim().toUpperCase());
87
- const isTargetLocation = locationList.includes(
88
- result.locationCreated?.toUpperCase?.() || result.locationCreated,
89
- );
90
-
91
- if (isTargetLocation) {
92
- result.keepFollow = true;
93
- log(` 国家匹配,获取关注/粉丝...`);
94
-
95
- // 提取关注/粉丝
96
- if (enableFollow) {
97
- await delay(100, 1000);
98
- if (!loggedIn) {
99
- log(" [跳过] 获取关注/粉丝:未登录,请先登录 TikTok");
100
- result.hasFollowData = false;
101
- result.discoveredFollowing = [];
102
- result.discoveredFollowers = [];
103
- } else {
104
- try {
105
- const isSeller = result.userInfo?.ttSeller === true;
106
- const effectiveMaxFollowing = isSeller
107
- ? globalMaxFollowing
108
- : maxFollowing;
109
- const effectiveMaxFollowers = isSeller
110
- ? globalMaxFollowers
111
- : maxFollowers;
112
- if (isSeller)
113
- log(
114
- ` 商家用户,关注采集: ${effectiveMaxFollowing}, 粉丝采集: ${effectiveMaxFollowers}`,
115
- );
116
- const { following, followers } = await extractFollowAndFollowers(
117
- page,
118
- {
119
- maxFollowing: effectiveMaxFollowing,
120
- maxFollowers: effectiveMaxFollowers,
121
- log,
122
- },
123
- );
124
- result.discoveredFollowing = following || [];
125
- result.discoveredFollowers = followers || [];
126
- result.hasFollowData = true;
127
- log(
128
- ` 关注: ${result.discoveredFollowing.length}, 粉丝: ${result.discoveredFollowers.length}`,
129
- );
130
- } catch (e) {
131
- log(` 关注/粉丝提取失败: ${e.message}`);
132
- result.hasFollowData = false;
133
- result.discoveredFollowing = [];
134
- result.discoveredFollowers = [];
135
- }
136
- }
137
- }
138
-
139
- // 携带视频列表供登记
140
- result.videoList = videoArray;
141
- } else {
142
- // 国家不匹配
143
- result.keepFollow = false;
144
- result.discoveredFollowing = [];
145
- result.discoveredFollowers = [];
146
- result.hasFollowData = false;
147
- log(` 国家不匹配,跳过`);
148
- }
149
-
150
- result.processed = true;
151
- } catch (e) {
152
- result.error = e.message;
153
- result.errorStack = e.stack || "";
154
- log(` [错误] ${e.message}`);
155
-
156
- // 被封会抛出 "被封: username" 异常
157
- if (e.message.startsWith("被封:")) {
158
- result.processed = false;
159
- result.noVideo = false;
160
- log(` @${username} 认定为被封`);
161
- }
162
- }
163
-
164
- return result;
165
- }
166
-
167
- export { processExplore };
1
+ import { ensureBrowserReady, delay } from "./modules/page-helpers.js";
2
+ import { detectCaptcha } from "./modules/captcha-handler.js";
3
+ export { ensureBrowserReady };
4
+ import { getUserInfo, collectVideos } from "../videos/core.js";
5
+ import { extractFollowAndFollowers } from "./modules/follow-extractor.js";
6
+ import { extractVideoLocation } from "../lib/scrape.js";
7
+ import {
8
+ maxFollowing as globalMaxFollowing,
9
+ maxFollowers as globalMaxFollowers,
10
+ } from "../lib/constants.js";
11
+
12
+ async function processExplore(page, username, options, log) {
13
+ const {
14
+ maxVideos = 16,
15
+ enableFollow = true,
16
+ loggedIn = false, // 由外部传入登录状态,避免每次调用 isLoggedIn(page)
17
+ maxFollowing = 50,
18
+ maxFollowers = 50,
19
+ location = "PL,NL,BE,DE,FR,IT,ES,IE",
20
+ } = options;
21
+
22
+ const result = {
23
+ userInfo: null,
24
+ discoveredVideoAuthors: [],
25
+ discoveredCommentAuthors: [],
26
+ discoveredGuessAuthors: [],
27
+ discoveredFollowing: [],
28
+ discoveredFollowers: [],
29
+ collectedVideos: 0,
30
+ processed: false,
31
+ hasFollowData: false,
32
+ keepFollow: false,
33
+ locationCreated: null,
34
+ noVideo: false,
35
+ error: null,
36
+ };
37
+
38
+ try {
39
+ log(` 访问 @${username} 主页...`);
40
+ const videoList = await collectVideos(page, username, maxVideos, log);
41
+
42
+ log(" 获取用户信息...");
43
+ const info = await getUserInfo(page);
44
+ if (info) {
45
+ result.userInfo = info;
46
+ log(
47
+ ` 用户: ${info.nickname || username} | 粉丝: ${info.followerCount || "-"} | 视频: ${info.videoCount || "-"}`,
48
+ );
49
+ }
50
+
51
+ const captcha = await detectCaptcha(page);
52
+ if (captcha && captcha.visible) {
53
+ log(`[验证码] @${username} 页面出现验证码`);
54
+ result.captchaDetected = true;
55
+ result.captchaStage = result.captchaStage || "video-page";
56
+ result.captchaMessage = result.captchaMessage || "视频页出现验证码";
57
+ }
58
+ const videoArray = videoList ? [...videoList.values()] : [];
59
+ result.collectedVideos = videoArray.length;
60
+
61
+ if (videoArray.length <= 0) {
62
+ // 视频为空:可能是页面受限或用户真的没有视频
63
+ result.processed = true;
64
+ result.noVideo = true;
65
+ log(` @${username} 没有视频,标记已处理`);
66
+ return result;
67
+ }
68
+
69
+ // 从最多 5 个视频并发获取 locationCreated,取众数
70
+ const SAMPLE_SIZE = 5;
71
+ let locationCreated = null;
72
+ const sampleVideos = videoArray.slice(0, SAMPLE_SIZE);
73
+ if (sampleVideos.length > 0) {
74
+ const sampleUrls = sampleVideos.map(v =>
75
+ v.href.startsWith("http") ? v.href : `https://www.tiktok.com${v.href}`
76
+ );
77
+ const locations = await Promise.all(sampleUrls.map(url => extractVideoLocation(url)));
78
+ log(` 国家采样(${locations.length}个): [${locations.filter(Boolean).join(", ") || "无数据"}]`);
79
+ const freq = {};
80
+ for (const loc of locations) {
81
+ if (loc) {
82
+ const key = loc.toUpperCase();
83
+ freq[key] = (freq[key] || 0) + 1;
84
+ }
85
+ }
86
+ const entries = Object.entries(freq).sort((a, b) => b[1] - a[1]);
87
+ locationCreated = entries.length > 0 ? entries[0][0] : null;
88
+ }
89
+
90
+ result.locationCreated = locationCreated || null;
91
+ log(` 国家: ${result.locationCreated || "未知"} (众数)`);
92
+
93
+ // 国家筛选
94
+ const locationList = (location || "ES")
95
+ .split(",")
96
+ .map((s) => s.trim().toUpperCase());
97
+ const isTargetLocation = locationList.includes(
98
+ result.locationCreated?.toUpperCase?.() || result.locationCreated,
99
+ );
100
+
101
+ if (isTargetLocation) {
102
+ result.keepFollow = true;
103
+ log(` 国家匹配,获取关注/粉丝...`);
104
+
105
+ // 提取关注/粉丝
106
+ if (enableFollow) {
107
+ await delay(100, 1000);
108
+ if (!loggedIn) {
109
+ log(" [跳过] 获取关注/粉丝:未登录,请先登录 TikTok");
110
+ result.hasFollowData = false;
111
+ result.discoveredFollowing = [];
112
+ result.discoveredFollowers = [];
113
+ } else {
114
+ try {
115
+ const isSeller = result.userInfo?.ttSeller === true;
116
+ const effectiveMaxFollowing = isSeller
117
+ ? globalMaxFollowing
118
+ : maxFollowing;
119
+ const effectiveMaxFollowers = isSeller
120
+ ? globalMaxFollowers
121
+ : maxFollowers;
122
+ if (isSeller)
123
+ log(
124
+ ` 商家用户,关注采集: ${effectiveMaxFollowing}, 粉丝采集: ${effectiveMaxFollowers}`,
125
+ );
126
+ const { following, followers } = await extractFollowAndFollowers(
127
+ page,
128
+ {
129
+ maxFollowing: effectiveMaxFollowing,
130
+ maxFollowers: effectiveMaxFollowers,
131
+ log,
132
+ },
133
+ );
134
+ result.discoveredFollowing = following || [];
135
+ result.discoveredFollowers = followers || [];
136
+ result.hasFollowData = true;
137
+ log(
138
+ ` 关注: ${result.discoveredFollowing.length}, 粉丝: ${result.discoveredFollowers.length}`,
139
+ );
140
+ } catch (e) {
141
+ log(` 关注/粉丝提取失败: ${e.message}`);
142
+ result.hasFollowData = false;
143
+ result.discoveredFollowing = [];
144
+ result.discoveredFollowers = [];
145
+ }
146
+ }
147
+ }
148
+
149
+ // 携带视频列表供登记
150
+ result.videoList = videoArray;
151
+ } else {
152
+ // 国家不匹配
153
+ result.keepFollow = false;
154
+ result.discoveredFollowing = [];
155
+ result.discoveredFollowers = [];
156
+ result.hasFollowData = false;
157
+ log(` 国家不匹配,跳过`);
158
+ }
159
+
160
+ result.processed = true;
161
+ } catch (e) {
162
+ result.error = e.message;
163
+ result.errorStack = e.stack || "";
164
+ log(` [错误] ${e.message}`);
165
+
166
+ // 被封会抛出 "被封: username" 异常
167
+ if (e.message.startsWith("被封:")) {
168
+ result.processed = false;
169
+ result.noVideo = false;
170
+ log(` @${username} 认定为被封`);
171
+ }
172
+ }
173
+
174
+ return result;
175
+ }
176
+
177
+ export { processExplore };
@@ -1,114 +1,114 @@
1
- export async function detectCaptcha(page) {
2
- return page.evaluate(() => {
3
- const container = document.querySelector('.captcha-verify-container');
4
- if (!container) return null;
5
-
6
- const r = container.getBoundingClientRect();
7
- return {
8
- exists: true,
9
- visible: container.offsetParent !== null,
10
- rect: {
11
- x: Math.round(r.x),
12
- y: Math.round(r.y),
13
- w: Math.round(r.width),
14
- h: Math.round(r.height),
15
- },
16
- };
17
- });
18
- }
19
-
20
- export async function closeCaptcha(page) {
21
- return page.evaluate(() => {
22
- const closeBtn = document.getElementById('captcha_close_button');
23
- if (!closeBtn) return { success: false, reason: 'close button not found' };
24
-
25
- closeBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
26
- closeBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
27
- closeBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
28
-
29
- return { success: true };
30
- });
31
- }
32
-
33
- export async function handleCaptcha(page, options = {}) {
34
- const { waitMs = 2000 } = options;
35
-
36
- const captcha = await detectCaptcha(page);
37
- if (!captcha) return { detected: false, closed: false };
38
-
39
- await new Promise(r => setTimeout(r, waitMs));
40
-
41
- const result = await closeCaptcha(page);
42
- if (!result.success) return { detected: true, closed: false, reason: result.reason };
43
-
44
- await new Promise(r => setTimeout(r, 1000));
45
-
46
- const stillThere = await detectCaptcha(page);
47
- return { detected: true, closed: !stillThere };
48
- }
49
-
50
- export async function getIncognitoPage(browser, url, options = {}) {
51
- const { waitMs = 3000 } = options;
52
- const context = await browser.newContext();
53
- const page = await context.newPage();
54
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
55
- await new Promise(r => setTimeout(r, waitMs));
56
- return { page, context };
57
- }
58
-
59
- export async function waitAndGetCaptcha(page, options = {}) {
60
- const { waitMs = 180000, pollInterval = 5000, log } = options;
61
-
62
- const captcha = await detectCaptcha(page);
63
- if (!captcha) return { detected: false, resolved: false, waited: 0 };
64
-
65
- if (log) log(' 检测到验证码,等待用户手动输入...');
66
-
67
- const startTime = Date.now();
68
- const deadline = startTime + waitMs;
69
-
70
- while (Date.now() < deadline) {
71
- await new Promise(r => setTimeout(r, pollInterval));
72
- const remaining = await detectCaptcha(page);
73
- if (!remaining) {
74
- const waited = Math.round((Date.now() - startTime) / 1000);
75
- if (log) log(` 验证码已解决(等待 ${waited}s)`);
76
- return { detected: true, resolved: true, waited };
77
- }
78
- }
79
-
80
- const waited = Math.round(waitMs / 1000);
81
- if (log) log(` 验证码等待超时(${waited}s),继续执行`);
82
-
83
- // 超时后尝试关闭验证码弹窗
84
- await closeCaptcha(page);
85
- await new Promise(r => setTimeout(r, 1000));
86
-
87
- return { detected: true, resolved: false, waited };
88
- }
89
-
90
- export async function safeClickComment(page, options = {}) {
91
- const { waitMs = 3000 } = options;
92
-
93
- // 点击评论
94
- await page.evaluate(() => {
95
- const all = document.querySelectorAll('button');
96
- for (const el of all) {
97
- if (/^评论$/.test(el.textContent?.trim()) && el.offsetParent !== null && el.getBoundingClientRect().width > 0) {
98
- el.click();
99
- break;
100
- }
101
- }
102
- });
103
-
104
- await new Promise(r => setTimeout(r, waitMs));
105
-
106
- // 检测并关闭验证码
107
- const captcha = await detectCaptcha(page);
108
- if (captcha) {
109
- const result = await handleCaptcha(page);
110
- return { clicked: true, captchaDetected: true, captchaClosed: result.closed };
111
- }
112
-
113
- return { clicked: true, captchaDetected: false, captchaClosed: false };
114
- }
1
+ export async function detectCaptcha(page) {
2
+ return page.evaluate(() => {
3
+ const container = document.querySelector('.captcha-verify-container');
4
+ if (!container) return null;
5
+
6
+ const r = container.getBoundingClientRect();
7
+ return {
8
+ exists: true,
9
+ visible: container.offsetParent !== null,
10
+ rect: {
11
+ x: Math.round(r.x),
12
+ y: Math.round(r.y),
13
+ w: Math.round(r.width),
14
+ h: Math.round(r.height),
15
+ },
16
+ };
17
+ });
18
+ }
19
+
20
+ export async function closeCaptcha(page) {
21
+ return page.evaluate(() => {
22
+ const closeBtn = document.getElementById('captcha_close_button');
23
+ if (!closeBtn) return { success: false, reason: 'close button not found' };
24
+
25
+ closeBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
26
+ closeBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
27
+ closeBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
28
+
29
+ return { success: true };
30
+ });
31
+ }
32
+
33
+ export async function handleCaptcha(page, options = {}) {
34
+ const { waitMs = 2000 } = options;
35
+
36
+ const captcha = await detectCaptcha(page);
37
+ if (!captcha) return { detected: false, closed: false };
38
+
39
+ await new Promise(r => setTimeout(r, waitMs));
40
+
41
+ const result = await closeCaptcha(page);
42
+ if (!result.success) return { detected: true, closed: false, reason: result.reason };
43
+
44
+ await new Promise(r => setTimeout(r, 1000));
45
+
46
+ const stillThere = await detectCaptcha(page);
47
+ return { detected: true, closed: !stillThere };
48
+ }
49
+
50
+ export async function getIncognitoPage(browser, url, options = {}) {
51
+ const { waitMs = 3000 } = options;
52
+ const context = await browser.newContext();
53
+ const page = await context.newPage();
54
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
55
+ await new Promise(r => setTimeout(r, waitMs));
56
+ return { page, context };
57
+ }
58
+
59
+ export async function waitAndGetCaptcha(page, options = {}) {
60
+ const { waitMs = 180000, pollInterval = 5000, log } = options;
61
+
62
+ const captcha = await detectCaptcha(page);
63
+ if (!captcha) return { detected: false, resolved: false, waited: 0 };
64
+
65
+ if (log) log(' 检测到验证码,等待用户手动输入...');
66
+
67
+ const startTime = Date.now();
68
+ const deadline = startTime + waitMs;
69
+
70
+ while (Date.now() < deadline) {
71
+ await new Promise(r => setTimeout(r, pollInterval));
72
+ const remaining = await detectCaptcha(page);
73
+ if (!remaining) {
74
+ const waited = Math.round((Date.now() - startTime) / 1000);
75
+ if (log) log(` 验证码已解决(等待 ${waited}s)`);
76
+ return { detected: true, resolved: true, waited };
77
+ }
78
+ }
79
+
80
+ const waited = Math.round(waitMs / 1000);
81
+ if (log) log(` 验证码等待超时(${waited}s),继续执行`);
82
+
83
+ // 超时后尝试关闭验证码弹窗
84
+ await closeCaptcha(page);
85
+ await new Promise(r => setTimeout(r, 1000));
86
+
87
+ return { detected: true, resolved: false, waited };
88
+ }
89
+
90
+ export async function safeClickComment(page, options = {}) {
91
+ const { waitMs = 3000 } = options;
92
+
93
+ // 点击评论
94
+ await page.evaluate(() => {
95
+ const all = document.querySelectorAll('button');
96
+ for (const el of all) {
97
+ if (/^评论$/.test(el.textContent?.trim()) && el.offsetParent !== null && el.getBoundingClientRect().width > 0) {
98
+ el.click();
99
+ break;
100
+ }
101
+ }
102
+ });
103
+
104
+ await new Promise(r => setTimeout(r, waitMs));
105
+
106
+ // 检测并关闭验证码
107
+ const captcha = await detectCaptcha(page);
108
+ if (captcha) {
109
+ const result = await handleCaptcha(page);
110
+ return { clicked: true, captchaDetected: true, captchaClosed: result.closed };
111
+ }
112
+
113
+ return { clicked: true, captchaDetected: false, captchaClosed: false };
114
+ }