tt-help-cli-ycl 1.0.8 → 1.1.0

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/src/auto-core.cjs CHANGED
@@ -1,310 +1,367 @@
1
- const {
2
- delay,
3
- ensureBrowserReady,
4
- ensureTikTokPage,
5
- setDelayConfig,
6
- getDelayConfig,
7
- closeCommentPanel,
8
- retryWithBackoff,
9
- } = require('./scraper/modules/page-helpers.cjs');
10
- const {
11
- getUserInfo,
12
- collectVideos,
13
- isPageRestricted,
14
- } = require('./get-user-videos-core.cjs');
15
- const { runScrape } = require('./scraper/core.cjs');
16
-
17
- function mergeUserInfo(existing, incoming, source) {
18
- const merged = { ...existing };
19
- for (const [key, value] of Object.entries(incoming)) {
20
- if (key === '_sources') continue;
21
- if (value === undefined || value === null || value === '') continue;
22
- if (typeof value === 'number' && typeof merged[key] === 'number') {
23
- merged[key] = Math.max(merged[key], value);
24
- } else if (merged[key] === undefined || merged[key] === null || merged[key] === '') {
25
- merged[key] = value;
26
- }
27
- }
28
- if (source) {
29
- if (!merged._sources) merged._sources = [];
30
- if (!merged._sources.includes(source)) merged._sources.push(source);
31
- }
32
- return merged;
33
- }
34
-
35
- async function runAuto(options) {
36
- const {
37
- username,
38
- collectMax = 1,
39
- scrapeDepth = 50,
40
- maxComments = 200,
41
- maxGuess = 10,
42
- preset = null,
43
- switchMax = null,
44
- commentMax = null,
45
- log = console.error,
46
- } = options;
47
-
48
- if (preset) {
49
- setDelayConfig(preset);
50
- } else if (switchMax || commentMax) {
51
- setDelayConfig({
52
- switchMax: switchMax || 5000,
53
- commentMax: commentMax || 3000,
54
- });
55
- }
56
-
57
- const config = getDelayConfig();
58
- const cleanUsername = username.replace('@', '');
59
-
60
- log(`auto 模式: @${cleanUsername}`);
61
- log(`收集视频数: ${collectMax}, 每个滑动: ${scrapeDepth}次, 每视频评论数: ${maxComments}`);
62
-
63
- const browser = await ensureBrowserReady();
64
- let page;
65
- try {
66
- page = await ensureTikTokPage(browser, `https://www.tiktok.com/@${cleanUsername}`);
67
- } catch (e) {
68
- await browser.close().catch(() => {});
69
- throw e;
70
- }
71
-
72
- // [1/3] 获取种子用户信息
73
- const profileUrl = `https://www.tiktok.com/@${cleanUsername}`;
74
- log(`\n[1/3] 获取 @${cleanUsername} 的用户信息和视频列表...`);
75
- await retryWithBackoff(() => page.goto(profileUrl, { waitUntil: 'load', timeout: 30000 }), { log });
76
- await page.waitForSelector('[class*="DivVideoList"]', { timeout: 10000 }).catch(() => {});
77
- await delay(1000, 2000);
78
-
79
- const seedUserInfo = await getUserInfo(page);
80
- if (!seedUserInfo.uniqueId) {
81
- seedUserInfo.uniqueId = cleanUsername;
82
- }
83
- log(`种子用户: ${seedUserInfo.nickname || seedUserInfo.uniqueId} (粉丝: ${seedUserInfo.followerCount || '-'})`);
84
-
85
- // [2/3] 收集视频列表
86
- const videos = await collectVideos(page, cleanUsername, collectMax, log);
87
- const videoList = Array.from(videos.values()).slice(0, collectMax);
88
- log(`获取到 ${videoList.length} 个视频`);
89
-
90
- if (videoList.length === 0) {
91
- const restricted = await isPageRestricted(page);
92
- if (restricted) {
93
- log('种子用户页面受限(需登录),结束');
94
- } else {
95
- log('没有获取到视频,结束');
96
- }
97
- const output = {
98
- seedUser: { ...seedUserInfo, sources: ['seed'], restricted },
99
- users: [{ ...seedUserInfo, sources: ['seed'], restricted }],
100
- stats: {
101
- totalVideos: 0,
102
- totalUsers: 1,
103
- fromSeed: 1,
104
- fromVideo: 0,
105
- fromComment: 0,
106
- },
107
- };
108
- return { output, browser };
109
- }
110
-
111
- // [3/3] 循环每个视频,执行 runScrape
112
- log(`\n[3/3] 开始循环抓取(${videoList.length} 个视频,每个滑动 ${scrapeDepth} 次)...`);
113
-
114
- const users = new Map();
115
- users.set(seedUserInfo.uniqueId, mergeUserInfo({}, seedUserInfo, 'seed'));
116
-
117
- const restrictedUsers = new Set();
118
- let totalVideosScraped = 0;
119
-
120
- for (let i = 0; i < videoList.length; i++) {
121
- const videoUrl = videoList[i].href.startsWith('http')
122
- ? videoList[i].href
123
- : `https://www.tiktok.com${videoList[i].href}`;
124
-
125
- log(`\n[${i + 1}/${videoList.length}] ${videoUrl}`);
126
-
127
- const { output: scrapeOutput } = await runScrape({
128
- videoUrl,
129
- maxVideos: scrapeDepth,
130
- maxComments,
131
- maxGuess,
132
- preset,
133
- switchMax,
134
- commentMax,
135
- log,
136
- browser,
137
- page,
138
- });
139
-
140
- totalVideosScraped += (scrapeOutput && scrapeOutput.stats) ? scrapeOutput.stats.totalVideos : 0;
141
-
142
- // 合并视频作者信息
143
- for (const vd of scrapeOutput.videoDetails) {
144
- if (restrictedUsers.has(vd.uniqueId)) continue;
145
- const existing = users.get(vd.uniqueId);
146
- users.set(vd.uniqueId, mergeUserInfo(existing || {}, vd, 'video'));
147
- }
148
-
149
- // 添加评论者
150
- for (const cu of scrapeOutput.commentUsers) {
151
- if (restrictedUsers.has(cu)) continue;
152
- if (!users.has(cu)) {
153
- users.set(cu, mergeUserInfo({}, { uniqueId: cu }, 'comment'));
154
- }
155
- }
156
-
157
- // 添加猜你喜欢作者
158
- for (const ga of (scrapeOutput.guessAuthors || [])) {
159
- const gaId = ga.replace(/^@/, '');
160
- if (restrictedUsers.has(gaId)) continue;
161
- if (!users.has(gaId)) {
162
- users.set(gaId, mergeUserInfo({}, { uniqueId: gaId }, 'guess'));
163
- }
164
- }
165
- }
166
-
167
- // 构建输出
168
- const usersList = [...users.values()].map(u => {
169
- const { _sources, ...rest } = u;
170
- return { ...rest, sources: _sources || [] };
171
- });
172
-
173
- usersList.sort((a, b) => {
174
- const aIsSeed = a._sources && a._sources.includes('seed');
175
- const bIsSeed = b._sources && b._sources.includes('seed');
176
- if (aIsSeed && !bIsSeed) return -1;
177
- if (!aIsSeed && bIsSeed) return 1;
178
- const aHasInfo = a.nickname || a.followerCount;
179
- const bHasInfo = b.nickname || b.followerCount;
180
- if (aHasInfo && !bHasInfo) return -1;
181
- if (!aHasInfo && bHasInfo) return 1;
182
- return 0;
183
- });
184
-
185
- const output = usersList;
186
-
187
- log(`\n结果: ${usersList.length} 个用户`);
188
-
189
- return { output, browser };
190
- }
191
-
192
- async function processUser(page, username, options, log) {
193
- const {
194
- collectMax = 1,
195
- scrapeDepth = 50,
196
- maxComments = 200,
197
- maxGuess = 10,
198
- preset = 'fast',
199
- switchMax = null,
200
- commentMax = null,
201
- browser = null,
202
- } = options;
203
-
204
- const result = {
205
- userInfo: null,
206
- collectedVideos: [],
207
- discoveredVideoAuthors: [],
208
- discoveredCommentAuthors: [],
209
- discoveredGuessAuthors: [],
210
- error: null,
211
- };
212
-
213
- try {
214
- log(`\n[processUser] 访问 @${username}...`);
215
- await retryWithBackoff(() => page.goto(`https://www.tiktok.com/@${username}`, {
216
- waitUntil: 'load',
217
- timeout: 30000,
218
- }), { log });
219
- await page.waitForSelector('[class*="DivVideoList"]', { timeout: 10000 }).catch(() => {});
220
- await delay(1000, 2000);
221
-
222
- const info = await getUserInfo(page);
223
- result.userInfo = info;
224
- if (!info.uniqueId) {
225
- info.uniqueId = username;
226
- }
227
- log(` 昵称: ${info.nickname || '-'} | 粉丝: ${info.followerCount || 0}`);
228
-
229
- const videos = await collectVideos(page, username, collectMax, log);
230
- const videoList = Array.from(videos.values()).slice(0, collectMax);
231
- result.collectedVideos = videoList.map(v => ({
232
- videoId: v.id,
233
- videoUrl: v.href,
234
- }));
235
-
236
- if (videoList.length > 0) {
237
- const allVideoAuthors = new Map();
238
- const allCommentAuthors = new Set();
239
- const allGuessAuthors = new Set();
240
-
241
- for (let i = 0; i < videoList.length; i++) {
242
- const video = videoList[i];
243
- const videoUrl = video.href.startsWith('http')
244
- ? video.href
245
- : `https://www.tiktok.com${video.href}`;
246
- log(` [${i + 1}/${videoList.length}] 开始 scrape: ${videoUrl} (深度 ${scrapeDepth})`);
247
-
248
- const scrapeResult = await runScrape({
249
- videoUrl,
250
- maxVideos: scrapeDepth,
251
- maxComments,
252
- maxGuess,
253
- preset,
254
- switchMax,
255
- commentMax,
256
- browser,
257
- page,
258
- log,
259
- });
260
-
261
- const scrapeOutput = scrapeResult.output;
262
-
263
- if (scrapeOutput && scrapeOutput.videoDetails) {
264
- for (const vd of scrapeOutput.videoDetails) {
265
- if (!allVideoAuthors.has(vd.uniqueId)) {
266
- allVideoAuthors.set(vd.uniqueId, {
267
- uniqueId: vd.uniqueId,
268
- nickname: vd.nickname,
269
- locationCreated: vd.locationCreated,
270
- });
271
- }
272
- }
273
- }
274
-
275
- if (scrapeOutput && scrapeOutput.commentUsers) {
276
- for (const cu of scrapeOutput.commentUsers) {
277
- allCommentAuthors.add(cu);
278
- }
279
- }
280
-
281
- if (scrapeOutput && scrapeOutput.guessAuthors) {
282
- for (const ga of scrapeOutput.guessAuthors) {
283
- allGuessAuthors.add(ga);
284
- }
285
- }
286
- }
287
-
288
- result.discoveredVideoAuthors = [...allVideoAuthors.values()];
289
- result.discoveredCommentAuthors = [...allCommentAuthors];
290
- result.discoveredGuessAuthors = [...allGuessAuthors];
291
-
292
- log(` 发现: ${result.discoveredVideoAuthors.length} 个视频作者, ${result.discoveredCommentAuthors.length} 个评论作者, ${result.discoveredGuessAuthors.length} 个猜你喜欢作者`);
293
- } else {
294
- const restricted = await isPageRestricted(page);
295
- result.restricted = restricted;
296
- if (restricted) {
297
- log(` @${username} 页面受限(需登录),标记跳过`);
298
- } else {
299
- log(` @${username} 没有视频,跳过 scrape`);
300
- }
301
- }
302
- } catch (e) {
303
- result.error = e.message;
304
- log(` [错误] ${e.message}`);
305
- }
306
-
307
- return result;
308
- }
309
-
310
- module.exports = { runAuto, processUser };
1
+ const {
2
+ delay,
3
+ ensureBrowserReady,
4
+ ensureTikTokPage,
5
+ setDelayConfig,
6
+ getDelayConfig,
7
+ closeCommentPanel,
8
+ retryWithBackoff,
9
+ } = require('./scraper/modules/page-helpers.cjs');
10
+ const {
11
+ getUserInfo,
12
+ collectVideos,
13
+ isPageRestricted,
14
+ } = require('./get-user-videos-core.cjs');
15
+ const { runScrape } = require('./scraper/core.cjs');
16
+ const { extractFollowAndFollowers } = require('./scraper/modules/follow-extractor.cjs');
17
+
18
+ function mergeUserInfo(existing, incoming, source) {
19
+ const merged = { ...existing };
20
+ for (const [key, value] of Object.entries(incoming)) {
21
+ if (key === '_sources') continue;
22
+ if (value === undefined || value === null || value === '') continue;
23
+ if (typeof value === 'number' && typeof merged[key] === 'number') {
24
+ merged[key] = Math.max(merged[key], value);
25
+ } else if (merged[key] === undefined || merged[key] === null || merged[key] === '') {
26
+ merged[key] = value;
27
+ }
28
+ }
29
+ if (source) {
30
+ if (!merged._sources) merged._sources = [];
31
+ if (!merged._sources.includes(source)) merged._sources.push(source);
32
+ }
33
+ return merged;
34
+ }
35
+
36
+ async function runAuto(options) {
37
+ const {
38
+ username,
39
+ collectMax = 1,
40
+ scrapeDepth = 50,
41
+ maxComments = 200,
42
+ maxGuess = 10,
43
+ preset = null,
44
+ switchMax = null,
45
+ commentMax = null,
46
+ enableFollow = false,
47
+ maxFollowing = 200,
48
+ maxFollowers = 200,
49
+ log = console.error,
50
+ } = options;
51
+
52
+ if (preset) {
53
+ setDelayConfig(preset);
54
+ } else if (switchMax || commentMax) {
55
+ setDelayConfig({
56
+ switchMax: switchMax || 5000,
57
+ commentMax: commentMax || 3000,
58
+ });
59
+ }
60
+
61
+ const config = getDelayConfig();
62
+ const cleanUsername = username.replace('@', '');
63
+
64
+ log(`auto 模式: @${cleanUsername}`);
65
+ log(`收集视频数: ${collectMax}, 每个滑动: ${scrapeDepth}次, 每视频评论数: ${maxComments}`);
66
+
67
+ const browser = await ensureBrowserReady();
68
+ let page;
69
+ try {
70
+ page = await ensureTikTokPage(browser, `https://www.tiktok.com/@${cleanUsername}`);
71
+ } catch (e) {
72
+ await browser.close().catch(() => {});
73
+ throw e;
74
+ }
75
+
76
+ // [1/3] 获取种子用户信息
77
+ const profileUrl = `https://www.tiktok.com/@${cleanUsername}`;
78
+ log(`\n[1/3] 获取 @${cleanUsername} 的用户信息和视频列表...`);
79
+ await retryWithBackoff(() => page.goto(profileUrl, { waitUntil: 'load', timeout: 30000 }), { log });
80
+ await page.waitForSelector('[class*="DivVideoList"]', { timeout: 10000 }).catch(() => {});
81
+ await delay(1000, 2000);
82
+
83
+ const seedUserInfo = await getUserInfo(page);
84
+ if (!seedUserInfo.uniqueId) {
85
+ seedUserInfo.uniqueId = cleanUsername;
86
+ }
87
+ log(`种子用户: ${seedUserInfo.nickname || seedUserInfo.uniqueId} (粉丝: ${seedUserInfo.followerCount || '-'})`);
88
+
89
+ if (options.enableFollow) {
90
+ try {
91
+ log(` 提取关注/粉丝列表...`);
92
+ const { following, followers } = await extractFollowAndFollowers(page, {
93
+ maxFollowing: options.maxFollowing || 200,
94
+ maxFollowers: options.maxFollowers || 200,
95
+ log,
96
+ });
97
+ log(` 关注: ${following.length} | 粉丝: ${followers.length}`);
98
+ following.forEach(([handle, name]) => {
99
+ const uid = handle.replace(/^@/, '');
100
+ users.set(uid, mergeUserInfo(
101
+ users.get(uid) || {},
102
+ { uniqueId: uid, nickname: name },
103
+ 'following'
104
+ ));
105
+ });
106
+ followers.forEach(([handle, name]) => {
107
+ const uid = handle.replace(/^@/, '');
108
+ users.set(uid, mergeUserInfo(
109
+ users.get(uid) || {},
110
+ { uniqueId: uid, nickname: name },
111
+ 'follower'
112
+ ));
113
+ });
114
+ } catch (e) {
115
+ log(` 关注/粉丝提取失败: ${e.message}`);
116
+ }
117
+ }
118
+
119
+ // [2/3] 收集视频列表
120
+ const videos = await collectVideos(page, cleanUsername, collectMax, log);
121
+ const videoList = Array.from(videos.values()).slice(0, collectMax);
122
+ log(`获取到 ${videoList.length} 个视频`);
123
+
124
+ if (videoList.length === 0) {
125
+ const restricted = await isPageRestricted(page);
126
+ if (restricted) {
127
+ log('种子用户页面受限(需登录),结束');
128
+ } else {
129
+ log('没有获取到视频,结束');
130
+ }
131
+ const output = {
132
+ seedUser: { ...seedUserInfo, sources: ['seed'], restricted },
133
+ users: [{ ...seedUserInfo, sources: ['seed'], restricted }],
134
+ stats: {
135
+ totalVideos: 0,
136
+ totalUsers: 1,
137
+ fromSeed: 1,
138
+ fromVideo: 0,
139
+ fromComment: 0,
140
+ },
141
+ };
142
+ return { output, browser };
143
+ }
144
+
145
+ // [3/3] 循环每个视频,执行 runScrape
146
+ log(`\n[3/3] 开始循环抓取(${videoList.length} 个视频,每个滑动 ${scrapeDepth} 次)...`);
147
+
148
+ const users = new Map();
149
+ users.set(seedUserInfo.uniqueId, mergeUserInfo({}, seedUserInfo, 'seed'));
150
+
151
+ const restrictedUsers = new Set();
152
+ let totalVideosScraped = 0;
153
+
154
+ for (let i = 0; i < videoList.length; i++) {
155
+ const videoUrl = videoList[i].href.startsWith('http')
156
+ ? videoList[i].href
157
+ : `https://www.tiktok.com${videoList[i].href}`;
158
+
159
+ log(`\n[${i + 1}/${videoList.length}] ${videoUrl}`);
160
+
161
+ const { output: scrapeOutput } = await runScrape({
162
+ videoUrl,
163
+ maxVideos: scrapeDepth,
164
+ maxComments,
165
+ maxGuess,
166
+ preset,
167
+ switchMax,
168
+ commentMax,
169
+ log,
170
+ browser,
171
+ page,
172
+ });
173
+
174
+ totalVideosScraped += (scrapeOutput && scrapeOutput.stats) ? scrapeOutput.stats.totalVideos : 0;
175
+
176
+ // 合并视频作者信息
177
+ for (const vd of scrapeOutput.videoDetails) {
178
+ if (restrictedUsers.has(vd.uniqueId)) continue;
179
+ const existing = users.get(vd.uniqueId);
180
+ users.set(vd.uniqueId, mergeUserInfo(existing || {}, vd, 'video'));
181
+ }
182
+
183
+ // 添加评论者
184
+ for (const cu of scrapeOutput.commentUsers) {
185
+ if (restrictedUsers.has(cu)) continue;
186
+ if (!users.has(cu)) {
187
+ users.set(cu, mergeUserInfo({}, { uniqueId: cu }, 'comment'));
188
+ }
189
+ }
190
+
191
+ // 添加猜你喜欢作者
192
+ for (const ga of (scrapeOutput.guessAuthors || [])) {
193
+ const gaId = ga.replace(/^@/, '');
194
+ if (restrictedUsers.has(gaId)) continue;
195
+ if (!users.has(gaId)) {
196
+ users.set(gaId, mergeUserInfo({}, { uniqueId: gaId }, 'guess'));
197
+ }
198
+ }
199
+ }
200
+
201
+ // 构建输出
202
+ const usersList = [...users.values()].map(u => {
203
+ const { _sources, ...rest } = u;
204
+ return { ...rest, sources: _sources || [] };
205
+ });
206
+
207
+ usersList.sort((a, b) => {
208
+ const aIsSeed = a._sources && a._sources.includes('seed');
209
+ const bIsSeed = b._sources && b._sources.includes('seed');
210
+ if (aIsSeed && !bIsSeed) return -1;
211
+ if (!aIsSeed && bIsSeed) return 1;
212
+ const aHasInfo = a.nickname || a.followerCount;
213
+ const bHasInfo = b.nickname || b.followerCount;
214
+ if (aHasInfo && !bHasInfo) return -1;
215
+ if (!aHasInfo && bHasInfo) return 1;
216
+ return 0;
217
+ });
218
+
219
+ const output = usersList;
220
+
221
+ log(`\n结果: ${usersList.length} 个用户`);
222
+
223
+ return { output, browser };
224
+ }
225
+
226
+ async function processUser(page, username, options, log) {
227
+ const {
228
+ collectMax = 1,
229
+ scrapeDepth = 50,
230
+ maxComments = 200,
231
+ maxGuess = 10,
232
+ preset = 'fast',
233
+ switchMax = null,
234
+ commentMax = null,
235
+ enableFollow = false,
236
+ maxFollowing = 200,
237
+ maxFollowers = 200,
238
+ browser = null,
239
+ } = options;
240
+
241
+ const result = {
242
+ userInfo: null,
243
+ collectedVideos: [],
244
+ discoveredVideoAuthors: [],
245
+ discoveredCommentAuthors: [],
246
+ discoveredGuessAuthors: [],
247
+ discoveredFollowing: [],
248
+ discoveredFollowers: [],
249
+ error: null,
250
+ };
251
+
252
+ try {
253
+ log(`\n[processUser] 访问 @${username}...`);
254
+ await retryWithBackoff(() => page.goto(`https://www.tiktok.com/@${username}`, {
255
+ waitUntil: 'load',
256
+ timeout: 30000,
257
+ }), { log });
258
+ await page.waitForSelector('[class*="DivVideoList"]', { timeout: 10000 }).catch(() => {});
259
+ await delay(1000, 2000);
260
+
261
+ const info = await getUserInfo(page);
262
+ result.userInfo = info;
263
+ if (!info.uniqueId) {
264
+ info.uniqueId = username;
265
+ }
266
+ log(` 昵称: ${info.nickname || '-'} | 粉丝: ${info.followerCount || 0}`);
267
+
268
+ if (options.enableFollow) {
269
+ try {
270
+ log(` 提取关注/粉丝列表...`);
271
+ const { following, followers } = await extractFollowAndFollowers(page, {
272
+ maxFollowing: options.maxFollowing || 200,
273
+ maxFollowers: options.maxFollowers || 200,
274
+ log,
275
+ });
276
+ result.discoveredFollowing = following;
277
+ result.discoveredFollowers = followers;
278
+ log(` 关注: ${following.length} | 粉丝: ${followers.length}`);
279
+ } catch (e) {
280
+ log(` 关注/粉丝提取失败: ${e.message}`);
281
+ result.discoveredFollowing = [];
282
+ result.discoveredFollowers = [];
283
+ }
284
+ }
285
+
286
+ const videos = await collectVideos(page, username, collectMax, log);
287
+ const videoList = Array.from(videos.values()).slice(0, collectMax);
288
+ result.collectedVideos = videoList.map(v => ({
289
+ videoId: v.id,
290
+ videoUrl: v.href,
291
+ }));
292
+
293
+ if (videoList.length > 0) {
294
+ const allVideoAuthors = new Map();
295
+ const allCommentAuthors = new Set();
296
+ const allGuessAuthors = new Set();
297
+
298
+ for (let i = 0; i < videoList.length; i++) {
299
+ const video = videoList[i];
300
+ const videoUrl = video.href.startsWith('http')
301
+ ? video.href
302
+ : `https://www.tiktok.com${video.href}`;
303
+ log(` [${i + 1}/${videoList.length}] 开始 scrape: ${videoUrl} (深度 ${scrapeDepth})`);
304
+
305
+ const scrapeResult = await runScrape({
306
+ videoUrl,
307
+ maxVideos: scrapeDepth,
308
+ maxComments,
309
+ maxGuess,
310
+ preset,
311
+ switchMax,
312
+ commentMax,
313
+ browser,
314
+ page,
315
+ log,
316
+ });
317
+
318
+ const scrapeOutput = scrapeResult.output;
319
+
320
+ if (scrapeOutput && scrapeOutput.videoDetails) {
321
+ for (const vd of scrapeOutput.videoDetails) {
322
+ if (!allVideoAuthors.has(vd.uniqueId)) {
323
+ allVideoAuthors.set(vd.uniqueId, {
324
+ uniqueId: vd.uniqueId,
325
+ nickname: vd.nickname,
326
+ locationCreated: vd.locationCreated,
327
+ });
328
+ }
329
+ }
330
+ }
331
+
332
+ if (scrapeOutput && scrapeOutput.commentUsers) {
333
+ for (const cu of scrapeOutput.commentUsers) {
334
+ allCommentAuthors.add(cu);
335
+ }
336
+ }
337
+
338
+ if (scrapeOutput && scrapeOutput.guessAuthors) {
339
+ for (const ga of scrapeOutput.guessAuthors) {
340
+ allGuessAuthors.add(ga);
341
+ }
342
+ }
343
+ }
344
+
345
+ result.discoveredVideoAuthors = [...allVideoAuthors.values()];
346
+ result.discoveredCommentAuthors = [...allCommentAuthors];
347
+ result.discoveredGuessAuthors = [...allGuessAuthors];
348
+
349
+ log(` 发现: ${result.discoveredVideoAuthors.length} 个视频作者, ${result.discoveredCommentAuthors.length} 个评论作者, ${result.discoveredGuessAuthors.length} 个猜你喜欢作者`);
350
+ } else {
351
+ const restricted = await isPageRestricted(page);
352
+ result.restricted = restricted;
353
+ if (restricted) {
354
+ log(` @${username} 页面受限(需登录),标记跳过`);
355
+ } else {
356
+ log(` @${username} 没有视频,跳过 scrape`);
357
+ }
358
+ }
359
+ } catch (e) {
360
+ result.error = e.message;
361
+ log(` [错误] ${e.message}`);
362
+ }
363
+
364
+ return result;
365
+ }
366
+
367
+ module.exports = { runAuto, processUser };