tt-help-cli-ycl 1.3.6 → 1.3.7
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/README.md +17 -17
- package/cli.js +9 -9
- package/package.json +45 -45
- package/src/cli/auto.js +131 -121
- package/src/cli/explore.js +147 -138
- package/src/cli/progress.js +111 -111
- package/src/cli/scrape.js +47 -47
- package/src/cli/utils.js +18 -18
- package/src/cli/videos.js +41 -41
- package/src/cli/watch.js +31 -31
- package/src/lib/args.js +391 -391
- package/src/lib/browser/anti-detect.js +23 -23
- package/src/lib/browser/cdp.js +142 -142
- package/src/lib/browser/launch.js +43 -43
- package/src/lib/browser/page.js +87 -87
- package/src/lib/constants.js +109 -95
- package/src/lib/delay.js +54 -54
- package/src/lib/explore-fetch.js +118 -118
- package/src/lib/fetcher.js +45 -45
- package/src/lib/filter.js +66 -66
- package/src/lib/io.js +54 -54
- package/src/lib/mac-or-uuid.js +82 -0
- package/src/lib/output.js +80 -80
- package/src/lib/parser.js +47 -47
- package/src/lib/retry.js +44 -44
- package/src/lib/scrape.js +40 -40
- package/src/lib/url.js +52 -52
- package/src/main.mjs +221 -221
- package/src/scraper/auto-core.mjs +185 -185
- package/src/scraper/core.mjs +190 -190
- package/src/scraper/explore-core.mjs +162 -162
- package/src/scraper/modules/captcha-handler.mjs +114 -114
- package/src/scraper/modules/comment-extractor.mjs +69 -69
- package/src/scraper/modules/follow-extractor.mjs +121 -121
- package/src/scraper/modules/guess-extractor.mjs +51 -51
- package/src/scraper/modules/page-error-detector.mjs +70 -70
- package/src/scraper/modules/page-helpers.mjs +48 -48
- package/src/scraper/modules/scroll-collector.mjs +189 -189
- package/src/test-auto-follow.cjs +109 -0
- package/src/test-extractors.cjs +75 -0
- package/src/test-follow.cjs +41 -0
- package/src/videos/core.mjs +126 -126
- package/src/watch/data-store.mjs +258 -261
- package/src/watch/public/index.html +466 -465
- package/src/watch/server.mjs +291 -281
- package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
import {
|
|
2
|
-
delay,
|
|
3
|
-
ensureBrowserReady,
|
|
4
|
-
setDelayConfig,
|
|
5
|
-
closeCommentPanel,
|
|
6
|
-
retryWithBackoff,
|
|
7
|
-
detectPageError,
|
|
8
|
-
isLoggedIn,
|
|
9
|
-
assertPageUrl,
|
|
10
|
-
} from './modules/page-helpers.mjs';
|
|
11
|
-
export { ensureBrowserReady };
|
|
12
|
-
import {
|
|
13
|
-
getUserInfo,
|
|
14
|
-
collectVideos,
|
|
15
|
-
} from '../videos/core.mjs';
|
|
16
|
-
import { scrapeSingleVideo } from './core.mjs';
|
|
17
|
-
import { extractFollowAndFollowers } from './modules/follow-extractor.mjs';
|
|
18
|
-
import { extractCommentAuthors } from './modules/comment-extractor.mjs';
|
|
19
|
-
import { extractGuessVideos } from './modules/guess-extractor.mjs';
|
|
20
|
-
|
|
21
|
-
async function processExplore(page, username, options, log) {
|
|
22
|
-
const {
|
|
23
|
-
maxComments = 0,
|
|
24
|
-
maxGuess = 0,
|
|
25
|
-
enableFollow = true,
|
|
26
|
-
maxFollowing = 5,
|
|
27
|
-
maxFollowers = 5,
|
|
28
|
-
location = 'ES',
|
|
29
|
-
} = options;
|
|
30
|
-
|
|
31
|
-
const result = {
|
|
32
|
-
userInfo: null,
|
|
33
|
-
discoveredVideoAuthors: [],
|
|
34
|
-
discoveredCommentAuthors: [],
|
|
35
|
-
discoveredGuessAuthors: [],
|
|
36
|
-
discoveredFollowing: [],
|
|
37
|
-
discoveredFollowers: [],
|
|
38
|
-
collectedVideos: 0,
|
|
39
|
-
processed: false,
|
|
40
|
-
hasFollowData: false,
|
|
41
|
-
keepFollow: false,
|
|
42
|
-
locationCreated: null,
|
|
43
|
-
noVideo: false,
|
|
44
|
-
error: null,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
log(` 访问 @${username} 主页...`);
|
|
49
|
-
const homeUrl = `https://www.tiktok.com/@${username}`;
|
|
50
|
-
await retryWithBackoff(() => page.goto(homeUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }), { log });
|
|
51
|
-
assertPageUrl(page, `@${username}`);
|
|
52
|
-
await page.waitForSelector('[class*="DivVideoList"]', { timeout: 10000 }).catch(() => {});
|
|
53
|
-
await delay(1000, 2000);
|
|
54
|
-
|
|
55
|
-
log(' 获取用户信息...');
|
|
56
|
-
const info = await getUserInfo(page);
|
|
57
|
-
if (info) {
|
|
58
|
-
result.userInfo = info;
|
|
59
|
-
log(` 用户: ${info.nickname || username} | 粉丝: ${info.followerCount || '-'} | 视频: ${info.videoCount || '-'}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const videoList = await collectVideos(page, username, 1, log);
|
|
63
|
-
const videoArray = videoList ? [...videoList.values()] : [];
|
|
64
|
-
result.collectedVideos = videoArray.length;
|
|
65
|
-
|
|
66
|
-
if (videoArray.length <= 0) {
|
|
67
|
-
result.processed = true;
|
|
68
|
-
result.noVideo = true;
|
|
69
|
-
const pageError = await detectPageError(page);
|
|
70
|
-
if (pageError) {
|
|
71
|
-
result.restricted = true;
|
|
72
|
-
log(` @${username} 页面受限(${pageError}),标记跳过`);
|
|
73
|
-
} else {
|
|
74
|
-
log(` @${username} 没有视频,标记已处理`);
|
|
75
|
-
}
|
|
76
|
-
return result;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (enableFollow) {
|
|
80
|
-
const loggedIn = await isLoggedIn(page);
|
|
81
|
-
if (!loggedIn) {
|
|
82
|
-
log(' [跳过] 获取关注/粉丝:未登录,请先登录 TikTok');
|
|
83
|
-
result.hasFollowData = false;
|
|
84
|
-
result.discoveredFollowing = [];
|
|
85
|
-
result.discoveredFollowers = [];
|
|
86
|
-
} else {
|
|
87
|
-
try {
|
|
88
|
-
log(' 获取关注/粉丝...');
|
|
89
|
-
const { following, followers } = await extractFollowAndFollowers(
|
|
90
|
-
page, { maxFollowing, maxFollowers, log }
|
|
91
|
-
);
|
|
92
|
-
result.discoveredFollowing = following || [];
|
|
93
|
-
result.discoveredFollowers = followers || [];
|
|
94
|
-
result.hasFollowData = true;
|
|
95
|
-
log(` 关注: ${result.discoveredFollowing.length}, 粉丝: ${result.discoveredFollowers.length}`);
|
|
96
|
-
} catch (e) {
|
|
97
|
-
log(` 关注/粉丝提取失败: ${e.message}`);
|
|
98
|
-
result.hasFollowData = false;
|
|
99
|
-
result.discoveredFollowing = [];
|
|
100
|
-
result.discoveredFollowers = [];
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const firstVideo = videoArray[0];
|
|
106
|
-
const videoUrl = firstVideo.href.startsWith('http')
|
|
107
|
-
? firstVideo.href
|
|
108
|
-
: `https://www.tiktok.com${firstVideo.href}`;
|
|
109
|
-
|
|
110
|
-
log(` 进入第一个视频: ${videoUrl}`);
|
|
111
|
-
await retryWithBackoff(() => page.goto(videoUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }), { log });
|
|
112
|
-
assertPageUrl(page, videoUrl.split('/video/')[0]);
|
|
113
|
-
await delay(1500, 2500);
|
|
114
|
-
|
|
115
|
-
const videoData = await scrapeSingleVideo(page, 0, 0, log, 'NEVER_MATCH');
|
|
116
|
-
result.locationCreated = videoData.locationCreated || null;
|
|
117
|
-
log(` 视频作者: ${videoData.videoAuthor} | 国家: ${result.locationCreated || '未知'}`);
|
|
118
|
-
|
|
119
|
-
const isTargetLocation = result.locationCreated === location;
|
|
120
|
-
|
|
121
|
-
if (isTargetLocation) {
|
|
122
|
-
result.keepFollow = true;
|
|
123
|
-
log(` 国家匹配 (${location}),获取评论和猜你喜欢...`);
|
|
124
|
-
|
|
125
|
-
if (maxComments > 0) {
|
|
126
|
-
const commentResult = await extractCommentAuthors(page, maxComments);
|
|
127
|
-
result.discoveredCommentAuthors = commentResult || [];
|
|
128
|
-
await closeCommentPanel(page);
|
|
129
|
-
await delay(500, 1000);
|
|
130
|
-
log(` 评论用户: ${result.discoveredCommentAuthors.length}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (maxGuess > 0) {
|
|
134
|
-
const guessResult = await extractGuessVideos(page, maxGuess);
|
|
135
|
-
result.discoveredGuessAuthors = (guessResult || []).map(v => v.author).filter(Boolean);
|
|
136
|
-
await closeCommentPanel(page);
|
|
137
|
-
await delay(500, 1000);
|
|
138
|
-
log(` 猜你喜欢作者: ${result.discoveredGuessAuthors.length}`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
result.discoveredVideoAuthors = [{
|
|
142
|
-
uniqueId: videoData.uniqueId,
|
|
143
|
-
nickname: videoData.nickname,
|
|
144
|
-
locationCreated: videoData.locationCreated,
|
|
145
|
-
}];
|
|
146
|
-
} else {
|
|
147
|
-
result.keepFollow = false;
|
|
148
|
-
log(` 国家不匹配 (${result.locationCreated} !== ${location}),跳过评论/猜你喜欢,丢弃关注/粉丝`);
|
|
149
|
-
result.discoveredFollowing = [];
|
|
150
|
-
result.discoveredFollowers = [];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
result.processed = true;
|
|
154
|
-
} catch (e) {
|
|
155
|
-
result.error = e.message;
|
|
156
|
-
log(` [错误] ${e.message}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export { processExplore };
|
|
1
|
+
import {
|
|
2
|
+
delay,
|
|
3
|
+
ensureBrowserReady,
|
|
4
|
+
setDelayConfig,
|
|
5
|
+
closeCommentPanel,
|
|
6
|
+
retryWithBackoff,
|
|
7
|
+
detectPageError,
|
|
8
|
+
isLoggedIn,
|
|
9
|
+
assertPageUrl,
|
|
10
|
+
} from './modules/page-helpers.mjs';
|
|
11
|
+
export { ensureBrowserReady };
|
|
12
|
+
import {
|
|
13
|
+
getUserInfo,
|
|
14
|
+
collectVideos,
|
|
15
|
+
} from '../videos/core.mjs';
|
|
16
|
+
import { scrapeSingleVideo } from './core.mjs';
|
|
17
|
+
import { extractFollowAndFollowers } from './modules/follow-extractor.mjs';
|
|
18
|
+
import { extractCommentAuthors } from './modules/comment-extractor.mjs';
|
|
19
|
+
import { extractGuessVideos } from './modules/guess-extractor.mjs';
|
|
20
|
+
|
|
21
|
+
async function processExplore(page, username, options, log) {
|
|
22
|
+
const {
|
|
23
|
+
maxComments = 0,
|
|
24
|
+
maxGuess = 0,
|
|
25
|
+
enableFollow = true,
|
|
26
|
+
maxFollowing = 5,
|
|
27
|
+
maxFollowers = 5,
|
|
28
|
+
location = 'ES',
|
|
29
|
+
} = options;
|
|
30
|
+
|
|
31
|
+
const result = {
|
|
32
|
+
userInfo: null,
|
|
33
|
+
discoveredVideoAuthors: [],
|
|
34
|
+
discoveredCommentAuthors: [],
|
|
35
|
+
discoveredGuessAuthors: [],
|
|
36
|
+
discoveredFollowing: [],
|
|
37
|
+
discoveredFollowers: [],
|
|
38
|
+
collectedVideos: 0,
|
|
39
|
+
processed: false,
|
|
40
|
+
hasFollowData: false,
|
|
41
|
+
keepFollow: false,
|
|
42
|
+
locationCreated: null,
|
|
43
|
+
noVideo: false,
|
|
44
|
+
error: null,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
log(` 访问 @${username} 主页...`);
|
|
49
|
+
const homeUrl = `https://www.tiktok.com/@${username}`;
|
|
50
|
+
await retryWithBackoff(() => page.goto(homeUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }), { log });
|
|
51
|
+
assertPageUrl(page, `@${username}`);
|
|
52
|
+
await page.waitForSelector('[class*="DivVideoList"]', { timeout: 10000 }).catch(() => {});
|
|
53
|
+
await delay(1000, 2000);
|
|
54
|
+
|
|
55
|
+
log(' 获取用户信息...');
|
|
56
|
+
const info = await getUserInfo(page);
|
|
57
|
+
if (info) {
|
|
58
|
+
result.userInfo = info;
|
|
59
|
+
log(` 用户: ${info.nickname || username} | 粉丝: ${info.followerCount || '-'} | 视频: ${info.videoCount || '-'}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const videoList = await collectVideos(page, username, 1, log);
|
|
63
|
+
const videoArray = videoList ? [...videoList.values()] : [];
|
|
64
|
+
result.collectedVideos = videoArray.length;
|
|
65
|
+
|
|
66
|
+
if (videoArray.length <= 0) {
|
|
67
|
+
result.processed = true;
|
|
68
|
+
result.noVideo = true;
|
|
69
|
+
const pageError = await detectPageError(page);
|
|
70
|
+
if (pageError) {
|
|
71
|
+
result.restricted = true;
|
|
72
|
+
log(` @${username} 页面受限(${pageError}),标记跳过`);
|
|
73
|
+
} else {
|
|
74
|
+
log(` @${username} 没有视频,标记已处理`);
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (enableFollow) {
|
|
80
|
+
const loggedIn = await isLoggedIn(page);
|
|
81
|
+
if (!loggedIn) {
|
|
82
|
+
log(' [跳过] 获取关注/粉丝:未登录,请先登录 TikTok');
|
|
83
|
+
result.hasFollowData = false;
|
|
84
|
+
result.discoveredFollowing = [];
|
|
85
|
+
result.discoveredFollowers = [];
|
|
86
|
+
} else {
|
|
87
|
+
try {
|
|
88
|
+
log(' 获取关注/粉丝...');
|
|
89
|
+
const { following, followers } = await extractFollowAndFollowers(
|
|
90
|
+
page, { maxFollowing, maxFollowers, log }
|
|
91
|
+
);
|
|
92
|
+
result.discoveredFollowing = following || [];
|
|
93
|
+
result.discoveredFollowers = followers || [];
|
|
94
|
+
result.hasFollowData = true;
|
|
95
|
+
log(` 关注: ${result.discoveredFollowing.length}, 粉丝: ${result.discoveredFollowers.length}`);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
log(` 关注/粉丝提取失败: ${e.message}`);
|
|
98
|
+
result.hasFollowData = false;
|
|
99
|
+
result.discoveredFollowing = [];
|
|
100
|
+
result.discoveredFollowers = [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const firstVideo = videoArray[0];
|
|
106
|
+
const videoUrl = firstVideo.href.startsWith('http')
|
|
107
|
+
? firstVideo.href
|
|
108
|
+
: `https://www.tiktok.com${firstVideo.href}`;
|
|
109
|
+
|
|
110
|
+
log(` 进入第一个视频: ${videoUrl}`);
|
|
111
|
+
await retryWithBackoff(() => page.goto(videoUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }), { log });
|
|
112
|
+
assertPageUrl(page, videoUrl.split('/video/')[0]);
|
|
113
|
+
await delay(1500, 2500);
|
|
114
|
+
|
|
115
|
+
const videoData = await scrapeSingleVideo(page, 0, 0, log, 'NEVER_MATCH');
|
|
116
|
+
result.locationCreated = videoData.locationCreated || null;
|
|
117
|
+
log(` 视频作者: ${videoData.videoAuthor} | 国家: ${result.locationCreated || '未知'}`);
|
|
118
|
+
|
|
119
|
+
const isTargetLocation = result.locationCreated === location;
|
|
120
|
+
|
|
121
|
+
if (isTargetLocation) {
|
|
122
|
+
result.keepFollow = true;
|
|
123
|
+
log(` 国家匹配 (${location}),获取评论和猜你喜欢...`);
|
|
124
|
+
|
|
125
|
+
if (maxComments > 0) {
|
|
126
|
+
const commentResult = await extractCommentAuthors(page, maxComments);
|
|
127
|
+
result.discoveredCommentAuthors = commentResult || [];
|
|
128
|
+
await closeCommentPanel(page);
|
|
129
|
+
await delay(500, 1000);
|
|
130
|
+
log(` 评论用户: ${result.discoveredCommentAuthors.length}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (maxGuess > 0) {
|
|
134
|
+
const guessResult = await extractGuessVideos(page, maxGuess);
|
|
135
|
+
result.discoveredGuessAuthors = (guessResult || []).map(v => v.author).filter(Boolean);
|
|
136
|
+
await closeCommentPanel(page);
|
|
137
|
+
await delay(500, 1000);
|
|
138
|
+
log(` 猜你喜欢作者: ${result.discoveredGuessAuthors.length}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
result.discoveredVideoAuthors = [{
|
|
142
|
+
uniqueId: videoData.uniqueId,
|
|
143
|
+
nickname: videoData.nickname,
|
|
144
|
+
locationCreated: videoData.locationCreated,
|
|
145
|
+
}];
|
|
146
|
+
} else {
|
|
147
|
+
result.keepFollow = false;
|
|
148
|
+
log(` 国家不匹配 (${result.locationCreated} !== ${location}),跳过评论/猜你喜欢,丢弃关注/粉丝`);
|
|
149
|
+
result.discoveredFollowing = [];
|
|
150
|
+
result.discoveredFollowers = [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
result.processed = true;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
result.error = e.message;
|
|
156
|
+
log(` [错误] ${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
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
|
+
}
|