tt-help-cli-ycl 1.3.26 → 1.3.27

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.3.26",
3
+ "version": "1.3.27",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -125,9 +125,14 @@ export async function handleExplore(options) {
125
125
 
126
126
  let browser = await ensureBrowserReadyCDP(cdpOptions);
127
127
  const { processExplore } = await import("../scraper/explore-core.js");
128
+ const { isLoggedIn } = await import("../lib/browser/page.js");
128
129
 
129
130
  const page = await getOrCreatePage(browser);
130
131
 
132
+ // 检测登录状态(启动时只检测一次)
133
+ let loggedIn = await isLoggedIn(page);
134
+ console.error(`登录状态: ${loggedIn ? "已登录" : "未登录"}`);
135
+
131
136
  // 全局拦截图片资源,减少内存占用和加载时间
132
137
  await page.route("**/*", (route) => {
133
138
  const resourceType = route.request().resourceType();
@@ -169,16 +174,22 @@ export async function handleExplore(options) {
169
174
  userDataDir: nextAccount.userDataDir,
170
175
  });
171
176
  console.error(`[健康检查] 已切换到端口 ${nextAccount.port}`);
177
+ // 切换账户后重新检测登录状态
178
+ loggedIn = await isLoggedIn(page);
179
+ console.error(
180
+ `[健康检查] 新账户登录状态: ${loggedIn ? "已登录" : "未登录"}`,
181
+ );
172
182
  lastFollowSuccessTime = Date.now(); // 切换账户后重置
183
+ captchaCount = 0; // 重置验证码计数
184
+ consecutiveNetworkErrors = 0; // 重置网络错误计数
173
185
  }
174
186
 
175
187
  let processedCount = 0;
176
188
  let errorCount = 0;
177
189
  let consecutiveNetworkErrors = 0;
178
190
  let captchaCount = 0; // 验证码累计计数
179
- let followBlockedCount = 0; // 关注/粉丝封禁计数
180
191
  let lastFollowSuccessTime = Date.now(); // 最近一次成功获取关注/粉丝的时间
181
- const FOLLOW_BLOCK_THRESHOLD = 30 * 60 * 1000; // 30分钟
192
+ const FOLLOW_BLOCK_THRESHOLD = 10 * 60 * 1000; // 10分钟
182
193
 
183
194
  while (true) {
184
195
  const job = await apiGet(
@@ -196,6 +207,20 @@ export async function handleExplore(options) {
196
207
  const checkResult = healthChecker.check();
197
208
  if (checkResult.shouldSwitch) {
198
209
  await handleAccountSwitch(checkResult.reason);
210
+ } else if (checkResult.info && processedCount % 10 === 0) {
211
+ // 关注/粉丝封禁检测状态(仅登录状态)
212
+ let followStatus = "";
213
+ if (loggedIn && exploreEnableFollow) {
214
+ const followElapsed = Math.round(
215
+ (Date.now() - lastFollowSuccessTime) / 60000,
216
+ );
217
+ const followRemaining = Math.max(
218
+ 0,
219
+ Math.round(FOLLOW_BLOCK_THRESHOLD / 60000) - followElapsed,
220
+ );
221
+ followStatus = ` | 关注/粉丝上次成功 ${followElapsed} 分钟前 | 封禁检测 ${followRemaining} 分钟后`;
222
+ }
223
+ console.error(`\n[健康检查] ${checkResult.info}${followStatus}`);
199
224
  }
200
225
 
201
226
  // 切换任务前检测验证码
@@ -235,18 +260,16 @@ export async function handleExplore(options) {
235
260
  }
236
261
  }
237
262
 
238
- // 关注/粉丝功能封禁检测(仅登录状态下)
239
- if (
240
- exploreEnableFollow &&
241
- Date.now() - lastFollowSuccessTime > FOLLOW_BLOCK_THRESHOLD
242
- ) {
243
- followBlockedCount++;
244
- const minutes = Math.round((Date.now() - lastFollowSuccessTime) / 60000);
245
- console.error(
246
- `\n[封禁检测] 关注/粉丝功能已 ${minutes} 分钟未成功获取,切换到下一个账户...`,
247
- );
248
- await handleAccountSwitch(`关注/粉丝功能被封 (${followBlockedCount})`);
249
- lastFollowSuccessTime = Date.now();
263
+ // 关注/粉丝封禁检测移到健康检查中(仅登录状态下)
264
+ if (loggedIn && exploreEnableFollow) {
265
+ const followElapsed = Date.now() - lastFollowSuccessTime;
266
+ if (followElapsed > FOLLOW_BLOCK_THRESHOLD) {
267
+ const minutes = Math.round(followElapsed / 60000);
268
+ console.error(
269
+ `\n[封禁检测] 关注/粉丝功能已 ${minutes} 分钟未成功获取,切换到下一个账户...`,
270
+ );
271
+ await handleAccountSwitch(`关注/粉丝功能被封`);
272
+ }
250
273
  }
251
274
 
252
275
  if (consecutiveNetworkErrors > 0) {
@@ -275,6 +298,7 @@ export async function handleExplore(options) {
275
298
  {
276
299
  maxVideos: exploreMaxVideos,
277
300
  enableFollow: exploreEnableFollow,
301
+ loggedIn, // 传入登录状态,避免每次调用 isLoggedIn(page)
278
302
  maxFollowing: exploreMaxFollowing,
279
303
  maxFollowers: exploreMaxFollowers,
280
304
  location: exploreLocation,
@@ -299,6 +323,7 @@ export async function handleExplore(options) {
299
323
  {
300
324
  maxVideos: exploreMaxVideos,
301
325
  enableFollow: exploreEnableFollow,
326
+ loggedIn, // 传入登录状态
302
327
  maxFollowing: exploreMaxFollowing,
303
328
  maxFollowers: exploreMaxFollowers,
304
329
  location: exploreLocation,
@@ -1,11 +1,23 @@
1
- import { delay } from './delay.js';
2
- import { retryWithBackoff } from './retry.js';
3
- import { assertPageUrl } from './browser/page.js';
1
+ import { delay } from "./delay.js";
2
+ import { retryWithBackoff } from "./retry.js";
3
+ import { assertPageUrl } from "./browser/page.js";
4
+ import {
5
+ detectPageError,
6
+ detectPageErrorWithWait,
7
+ } from "./page-error-detector.js";
4
8
 
5
9
  /**
6
10
  * 处理 API 响应数据:提取首页视频 + 翻页
7
11
  */
8
- async function processAPIResponse(data, username, maxVideos, items, page, apiRequestUrl, log) {
12
+ async function processAPIResponse(
13
+ data,
14
+ username,
15
+ maxVideos,
16
+ items,
17
+ page,
18
+ apiRequestUrl,
19
+ log,
20
+ ) {
9
21
  // 提取首页视频
10
22
  const firstPageItems = data.itemList || [];
11
23
  for (const item of firstPageItems) {
@@ -23,7 +35,7 @@ async function processAPIResponse(data, username, maxVideos, items, page, apiReq
23
35
  while (hasMore && cursor && items.length < maxVideos) {
24
36
  const reqUrl = apiRequestUrl || data.apiRequestUrl;
25
37
  if (!reqUrl) {
26
- log(' [API拦截] 未捕获到 API 请求 URL,无法翻页');
38
+ log(" [API拦截] 未捕获到 API 请求 URL,无法翻页");
27
39
  break;
28
40
  }
29
41
 
@@ -41,7 +53,9 @@ async function processAPIResponse(data, username, maxVideos, items, page, apiReq
41
53
  const href = `https://www.tiktok.com/@${username}/video/${item.id}`;
42
54
  items.push({ id: item.id, href });
43
55
  }
44
- log(` [API拦截] 翻页 cursor=${cursor},获取 ${pageData.itemList.length} 条,累计 ${items.length}`);
56
+ log(
57
+ ` [API拦截] 翻页 cursor=${cursor},获取 ${pageData.itemList.length} 条,累计 ${items.length}`,
58
+ );
45
59
  }
46
60
 
47
61
  cursor = pageData.cursor;
@@ -82,12 +96,14 @@ async function fetchUserVideosAPI(page, username, maxVideos, log) {
82
96
 
83
97
  // 1. 注册 response 拦截器 + request URL 捕获
84
98
  let apiResolve = null;
85
- const apiPromise = new Promise(r => { apiResolve = r; });
99
+ const apiPromise = new Promise((r) => {
100
+ apiResolve = r;
101
+ });
86
102
 
87
103
  let apiRequestUrl = null;
88
104
 
89
105
  const responseHandler = async (response) => {
90
- if (response.url().includes('/api/post/item_list/')) {
106
+ if (response.url().includes("/api/post/item_list/")) {
91
107
  try {
92
108
  apiResolve(await response.json());
93
109
  } catch (e) {
@@ -97,94 +113,69 @@ async function fetchUserVideosAPI(page, username, maxVideos, log) {
97
113
  };
98
114
 
99
115
  const requestHandler = (request) => {
100
- if (request.url().includes('/api/post/item_list/') && !apiRequestUrl) {
116
+ if (request.url().includes("/api/post/item_list/") && !apiRequestUrl) {
101
117
  apiRequestUrl = request.url();
102
118
  }
103
119
  };
104
120
 
105
- page.on('response', responseHandler);
106
- page.on('request', requestHandler);
121
+ page.on("response", responseHandler);
122
+ page.on("request", requestHandler);
107
123
 
108
124
  try {
109
125
  // 2. 导航并等待 API 响应
110
- log(' [API拦截] 导航到用户页,等待 /api/post/item_list/ ...');
126
+ log(" [API拦截] 导航到用户页,等待 /api/post/item_list/ ...");
111
127
  const t0 = Date.now();
112
128
 
113
- await retryWithBackoff(async () => {
114
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
115
- await assertPageUrl(page, `@${username}`);
116
- }, { maxRetries: 3, baseDelay: 3000, log });
129
+ await retryWithBackoff(
130
+ async () => {
131
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
132
+ await assertPageUrl(page, `@${username}`);
133
+ },
134
+ { maxRetries: 3, baseDelay: 3000, log },
135
+ );
117
136
 
118
137
  const data = await Promise.race([
119
138
  apiPromise,
120
- new Promise(r => setTimeout(() => r(null), 8000)),
139
+ new Promise((r) => setTimeout(() => r(null), 8000)),
121
140
  ]);
122
141
 
123
142
  const elapsed = Date.now() - t0;
124
143
 
125
144
  if (!data || !data.itemList) {
126
- log(` [API拦截] ${elapsed}ms 后未拿到 API 数据,重试...`);
127
- // 清理后重试
128
- page.off('response', responseHandler);
129
- page.off('request', requestHandler);
130
-
131
- // 重试:重新注册拦截器并导航
132
- let apiResolve2 = null;
133
- const apiPromise2 = new Promise(r => { apiResolve2 = r; });
134
- let apiRequestUrl2 = null;
135
-
136
- const responseHandler2 = async (response) => {
137
- if (response.url().includes('/api/post/item_list/')) {
138
- try {
139
- const json = await response.json().catch(() => null);
140
- apiResolve2(json);
141
- } catch (e) {
142
- apiResolve2(null);
143
- }
144
- }
145
- };
146
-
147
- const requestHandler2 = (request) => {
148
- if (request.url().includes('/api/post/item_list/') && !apiRequestUrl2) {
149
- apiRequestUrl2 = request.url();
150
- }
151
- };
152
-
153
- page.on('response', responseHandler2);
154
- page.on('request', requestHandler2);
155
-
156
- try {
157
- await retryWithBackoff(
158
- async () => {
159
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
160
- assertPageUrl(page, url);
161
- },
162
- { retries: 1, log },
163
- );
164
-
165
- const data2 = await Promise.race([
166
- apiPromise2,
167
- new Promise(r => setTimeout(() => r(null), 8000)),
168
- ]);
169
- const elapsed2 = Date.now() - t0;
145
+ // 使用 waitForFunction 等待错误信息出现(最多 8 秒)
146
+ const pageError = await detectPageErrorWithWait(page, 8000);
170
147
 
171
- if (!data2 || !data2.itemList) {
172
- throw new Error(`[API拦截] ${elapsed2}ms 后仍未拿到 API 数据(含重试),请检查网络`);
173
- }
148
+ if (pageError === "service_error") {
149
+ throw new Error(`被封: ${username}`);
150
+ }
174
151
 
175
- return await processAPIResponse(data2, username, maxVideos, items, page, apiRequestUrl2, log);
176
- } finally {
177
- page.off('response', responseHandler2);
178
- page.off('request', requestHandler2);
152
+ // API 超时了,页面肯定有问题
153
+ // 如果识别不出具体错误类型,抛异常(而不是静默返回空 Map)
154
+ if (!pageError) {
155
+ throw new Error(`@${username} 页面异常(无法识别错误类型)`);
179
156
  }
157
+
158
+ // 其他已知错误(login_required, captcha, rate_limited 等)返回空 Map
159
+ log(
160
+ ` [API拦截] ${elapsed}ms 后未拿到 API 数据,页面异常(${pageError})`,
161
+ );
162
+ return new Map();
180
163
  }
181
164
 
182
- // 3. 处理数据(提取首页 + 翻页)
183
- return await processAPIResponse(data, username, maxVideos, items, page, apiRequestUrl, log);
165
+ // 处理数据(提取首页 + 翻页)
166
+ return await processAPIResponse(
167
+ data,
168
+ username,
169
+ maxVideos,
170
+ items,
171
+ page,
172
+ apiRequestUrl,
173
+ log,
174
+ );
184
175
  } finally {
185
176
  // 5. 必须清理拦截器,防止累积
186
- page.off('response', responseHandler);
187
- page.off('request', requestHandler);
177
+ page.off("response", responseHandler);
178
+ page.off("request", requestHandler);
188
179
  }
189
180
  }
190
181
 
@@ -5,7 +5,7 @@ import { execSync } from "child_process";
5
5
  const TOTAL_ACCOUNTS = 10;
6
6
  const BASE_PORT = 9222;
7
7
  const MEMORY_THRESHOLD = 0.9;
8
- const ROTATE_INTERVAL_MS = 2 * 60 * 60 * 1000;
8
+ const ROTATE_INTERVAL_MS = 1 * 60 * 60 * 1000; // 1小时
9
9
 
10
10
  class HealthChecker {
11
11
  constructor() {
@@ -34,6 +34,7 @@ class HealthChecker {
34
34
 
35
35
  getNextAccount() {
36
36
  this.currentIndex = (this.currentIndex + 1) % TOTAL_ACCOUNTS;
37
+ this.startTime = Date.now(); // 切换后重置运行时间
37
38
  return this.accounts[this.currentIndex];
38
39
  }
39
40
 
@@ -86,7 +87,15 @@ class HealthChecker {
86
87
  return { shouldSwitch: true, reason: `运行 ${mins} 分钟` };
87
88
  }
88
89
 
89
- return { shouldSwitch: false };
90
+ // 计算预计切换时间
91
+ const remainingMs = ROTATE_INTERVAL_MS - elapsed;
92
+ const remainingMins = Math.round(remainingMs / 60000);
93
+ const memHeadroom = ((MEMORY_THRESHOLD - memUsage) * 100).toFixed(1);
94
+
95
+ return {
96
+ shouldSwitch: false,
97
+ info: `内存 ${memPercent}% (余量 ${memHeadroom}%) | 定时轮换 ${remainingMins} 分钟后`,
98
+ };
90
99
  }
91
100
  }
92
101
 
@@ -49,6 +49,7 @@ const PATTERNS = {
49
49
  "没有内容",
50
50
  "发起对话",
51
51
  "0 条评论",
52
+ "找不到此账号",
52
53
  ],
53
54
  service_error: ["出错了", "很抱歉"],
54
55
  };
@@ -69,3 +70,36 @@ export async function detectPageError(page) {
69
70
  return null;
70
71
  }, PATTERNS);
71
72
  }
73
+
74
+ /**
75
+ * 等待页面错误信息出现(轮询检测,最多等待 timeout ms)
76
+ * @param {import('playwright').Page} page
77
+ * @param {number} timeout - 超时时间(毫秒),默认 8000
78
+ * @returns {Promise<string|null>} 错误类型或 null
79
+ */
80
+ export async function detectPageErrorWithWait(page, timeout = 8000) {
81
+ try {
82
+ const handle = await page.waitForFunction(
83
+ (patterns) => {
84
+ const bodyText = document.body.innerText;
85
+ const lower = bodyText.toLowerCase();
86
+
87
+ for (const [type, phrases] of Object.entries(patterns)) {
88
+ for (const phrase of phrases) {
89
+ if (lower.includes(phrase.toLowerCase())) {
90
+ return type;
91
+ }
92
+ }
93
+ }
94
+
95
+ return null;
96
+ },
97
+ PATTERNS,
98
+ { timeout },
99
+ );
100
+ return await handle.jsonValue();
101
+ } catch {
102
+ // 超时或未检测到错误
103
+ return null;
104
+ }
105
+ }
@@ -1,9 +1,4 @@
1
- import {
2
- ensureBrowserReady,
3
- detectPageError,
4
- isLoggedIn,
5
- delay,
6
- } from "./modules/page-helpers.js";
1
+ import { ensureBrowserReady, delay } from "./modules/page-helpers.js";
7
2
  import { detectCaptcha } from "./modules/captcha-handler.js";
8
3
  export { ensureBrowserReady };
9
4
  import { getUserInfo, collectVideos } from "../videos/core.js";
@@ -18,6 +13,7 @@ async function processExplore(page, username, options, log) {
18
13
  const {
19
14
  maxVideos = 16,
20
15
  enableFollow = true,
16
+ loggedIn = false, // 由外部传入登录状态,避免每次调用 isLoggedIn(page)
21
17
  maxFollowing = 50,
22
18
  maxFollowers = 50,
23
19
  location = "PL,NL,BE,DE,FR,IT,ES,IE",
@@ -63,23 +59,10 @@ async function processExplore(page, username, options, log) {
63
59
  result.collectedVideos = videoArray.length;
64
60
 
65
61
  if (videoArray.length <= 0) {
62
+ // 视频为空:可能是页面受限或用户真的没有视频
66
63
  result.processed = true;
67
64
  result.noVideo = true;
68
- const pageError = await detectPageError(page);
69
- if (pageError === "service_error") {
70
- result.error = `被封: ${username}`;
71
- result.errorStack = "";
72
- result.processed = false;
73
- result.noVideo = false;
74
- log(` @${username} 服务异常(service_error),标记被封`);
75
- } else if (pageError) {
76
- result.restricted = true;
77
- result.processed = true;
78
- result.noVideo = true;
79
- log(` @${username} 页面受限(${pageError}),标记跳过`);
80
- } else {
81
- log(` @${username} 没有视频,标记已处理`);
82
- }
65
+ log(` @${username} 没有视频,标记已处理`);
83
66
  return result;
84
67
  }
85
68
 
@@ -111,7 +94,6 @@ async function processExplore(page, username, options, log) {
111
94
 
112
95
  // 提取关注/粉丝
113
96
  if (enableFollow) {
114
- const loggedIn = await isLoggedIn(page);
115
97
  await delay(100, 1000);
116
98
  if (!loggedIn) {
117
99
  log(" [跳过] 获取关注/粉丝:未登录,请先登录 TikTok");
@@ -171,27 +153,11 @@ async function processExplore(page, username, options, log) {
171
153
  result.errorStack = e.stack || "";
172
154
  log(` [错误] ${e.message}`);
173
155
 
174
- // API 拦截失败时,复用 videoArray.length <= 0 的被封检测逻辑
175
- if (e.message.includes("API拦截")) {
176
- log(` [被封检测] 检查页面状态...`);
177
- const hasErrorPage = await page
178
- .waitForFunction(
179
- (keywords) => {
180
- const text = document.body.innerText;
181
- return keywords.some((k) => text.includes(k));
182
- },
183
- ["出错了", "很抱歉"],
184
- { timeout: 5000 },
185
- )
186
- .then(() => true)
187
- .catch(() => false);
188
-
189
- if (hasErrorPage) {
190
- result.error = `被封: ${username}`;
191
- result.processed = false;
192
- result.noVideo = false;
193
- log(` @${username} API 拦截失败且页面异常,认定为被封`);
194
- }
156
+ // 被封会抛出 "被封: username" 异常
157
+ if (e.message.startsWith("被封:")) {
158
+ result.processed = false;
159
+ result.noVideo = false;
160
+ log(` @${username} 认定为被封`);
195
161
  }
196
162
  }
197
163
 
@@ -1,5 +1,10 @@
1
- import { delay, ensureBrowserReady, ensureTikTokPage, retryWithBackoff } from '../scraper/modules/page-helpers.js';
2
- import { fetchUserVideosAPI } from '../lib/api-interceptor.js';
1
+ import {
2
+ delay,
3
+ ensureBrowserReady,
4
+ ensureTikTokPage,
5
+ retryWithBackoff,
6
+ } from "../scraper/modules/page-helpers.js";
7
+ import { fetchUserVideosAPI } from "../lib/api-interceptor.js";
3
8
 
4
9
  async function getUserInfo(page) {
5
10
  return await page.evaluate(() => {
@@ -23,15 +28,21 @@ async function getUserInfo(page) {
23
28
  region: /"region":"([^"]*)/,
24
29
  };
25
30
 
26
- const boolKeys = ['ttSeller', 'verified'];
27
- const numKeys = ['followerCount', 'videoCount', 'followingCount', 'heartCount'];
31
+ const boolKeys = ["ttSeller", "verified"];
32
+ const numKeys = [
33
+ "followerCount",
34
+ "videoCount",
35
+ "followingCount",
36
+ "heartCount",
37
+ ];
28
38
 
29
39
  for (const [key, pat] of Object.entries(patterns)) {
30
40
  const match = html.match(pat);
31
41
  if (match) {
32
- if (boolKeys.includes(key)) result[key] = match[1] === 'true';
42
+ if (boolKeys.includes(key)) result[key] = match[1] === "true";
33
43
  else if (numKeys.includes(key)) result[key] = parseInt(match[1], 10);
34
- else if (key === 'signature') result[key] = match[1].replace(/\\n/g, '\n').replace(/\\\\/g, '\\');
44
+ else if (key === "signature")
45
+ result[key] = match[1].replace(/\\n/g, "\n").replace(/\\\\/g, "\\");
35
46
  else result[key] = match[1];
36
47
  }
37
48
  }
@@ -41,16 +52,22 @@ async function getUserInfo(page) {
41
52
  }
42
53
 
43
54
  async function collectVideos(page, username, maxVideos, log) {
44
- const apiResult = await fetchUserVideosAPI(page, username, maxVideos, log);
45
- if (apiResult && apiResult.size > 0) {
46
- log(`收集完成: ${apiResult.size} 个视频`);
47
- return apiResult;
48
- }
49
- if (apiResult && apiResult.size === 0) {
55
+ try {
56
+ const apiResult = await fetchUserVideosAPI(page, username, maxVideos, log);
57
+ if (apiResult && apiResult.size > 0) {
58
+ log(`收集完成: ${apiResult.size} 个视频`);
59
+ return apiResult;
60
+ }
61
+ // apiResult 为空 Map 表示页面有其他异常(login_required, captcha 等)
50
62
  return new Map();
63
+ } catch (e) {
64
+ // 被封会抛出 "被封: username" 异常
65
+ if (e.message.startsWith("被封:")) {
66
+ throw e; // 向上抛出,由 explore-core 处理
67
+ }
68
+ // 其他异常也向上抛出
69
+ throw new Error(`API 拦截失败:@${username} ${e.message}`);
51
70
  }
52
- // apiResult is null means timeout before refactor; now it should throw instead
53
- throw new Error(`API 拦截失败:@${username} 获取视频列表超时,请检查网络`);
54
71
  }
55
72
 
56
73
  async function runGetUserVideos(options) {
@@ -61,7 +78,7 @@ async function runGetUserVideos(options) {
61
78
  log(`URL: ${url}`);
62
79
  log(`最大视频数: ${maxVideos}\n`);
63
80
 
64
- log('连接浏览器...');
81
+ log("连接浏览器...");
65
82
  const browser = await ensureBrowserReady();
66
83
 
67
84
  let page;
@@ -72,15 +89,20 @@ async function runGetUserVideos(options) {
72
89
  throw e;
73
90
  }
74
91
 
75
- await retryWithBackoff(() => page.goto(url, { waitUntil: 'load', timeout: 30000 }), { log });
92
+ await retryWithBackoff(
93
+ () => page.goto(url, { waitUntil: "load", timeout: 30000 }),
94
+ { log },
95
+ );
76
96
  await delay(3000, 5000);
77
- await page.waitForSelector('[class*="DivVideoList"]', { timeout: 10000 }).catch(() => {});
97
+ await page
98
+ .waitForSelector('[class*="DivVideoList"]', { timeout: 10000 })
99
+ .catch(() => {});
78
100
 
79
- log('获取用户信息...');
101
+ log("获取用户信息...");
80
102
  const userInfo = await getUserInfo(page);
81
- log('用户信息: ' + JSON.stringify(userInfo, null, 2));
103
+ log("用户信息: " + JSON.stringify(userInfo, null, 2));
82
104
 
83
- log('\n开始滚动收集视频...');
105
+ log("\n开始滚动收集视频...");
84
106
  const videos = await collectVideos(page, username, maxVideos, log);
85
107
  const allVideos = Array.from(videos.values());
86
108
 
@@ -89,9 +111,11 @@ async function runGetUserVideos(options) {
89
111
  const output = {
90
112
  user: userInfo,
91
113
  totalVideos: Math.min(allVideos.length, maxVideos),
92
- videos: allVideos.slice(0, maxVideos).map(v => ({
114
+ videos: allVideos.slice(0, maxVideos).map((v) => ({
93
115
  id: v.id,
94
- url: v.href.startsWith('http') ? v.href : `https://www.tiktok.com${v.href}`,
116
+ url: v.href.startsWith("http")
117
+ ? v.href
118
+ : `https://www.tiktok.com${v.href}`,
95
119
  })),
96
120
  };
97
121