tt-help-cli-ycl 1.3.39 → 1.3.40

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.39",
3
+ "version": "1.3.40",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -393,38 +393,67 @@ export async function handleExplore(options) {
393
393
  if (result.error) {
394
394
  consecutiveNetworkErrors++;
395
395
  errorCount++;
396
- await apiPost(`${serverUrl}/api/job/${username}`, {
397
- error: result.error,
398
- });
399
- const errorType = result.error.startsWith("被封:")
400
- ? "被封"
401
- : consecutiveNetworkErrors > 1
402
- ? "network"
403
- : "other";
404
- await withRetry("report error", () =>
405
- apiPost(`${serverUrl}/api/error-report`, {
406
- userId,
396
+
397
+ // 临时性错误:自动重试一次
398
+ if (result.retryable) {
399
+ console.error(` [临时错误] 等待 5 秒后重试 @${username}...`);
400
+ await new Promise((r) => setTimeout(r, 5000));
401
+ result = await processExplore(
402
+ page,
407
403
  username,
408
- errorType,
409
- errorMessage: result.error,
410
- stage: "process",
411
- errorStack: result.errorStack || "",
412
- }),
413
- ).catch(() => {});
414
- if (errorType === "被封") {
415
- blockedCount++;
416
- console.error(` [被封] 累计 ${blockedCount} 次`);
417
- if (blockedCount >= 3) {
418
- await handleAccountSwitch(`账号被封累计 ${blockedCount} 次`);
419
- blockedCount = 0;
404
+ {
405
+ maxVideos: exploreMaxVideos,
406
+ enableFollow: exploreEnableFollow,
407
+ loggedIn,
408
+ maxFollowing: exploreMaxFollowing,
409
+ maxFollowers: exploreMaxFollowers,
410
+ location: exploreLocation,
411
+ browser,
412
+ },
413
+ console.error,
414
+ );
415
+ // 重试成功后继续正常流程
416
+ if (!result.error) {
417
+ consecutiveNetworkErrors = 0;
418
+ errorCount--;
420
419
  }
421
- continue;
422
420
  }
423
- if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
424
- console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
425
- break;
421
+
422
+ if (result.error) {
423
+ // 上报错误(重试后仍有错误才上报)
424
+ await apiPost(`${serverUrl}/api/job/${username}`, {
425
+ error: result.error,
426
+ });
427
+ const errorType = result.error.startsWith("被封:")
428
+ ? "被封"
429
+ : consecutiveNetworkErrors > 1
430
+ ? "network"
431
+ : "other";
432
+ await withRetry("report error", () =>
433
+ apiPost(`${serverUrl}/api/error-report`, {
434
+ userId,
435
+ username,
436
+ errorType,
437
+ errorMessage: result.error,
438
+ stage: "process",
439
+ errorStack: result.errorStack || "",
440
+ }),
441
+ ).catch(() => {});
442
+ if (errorType === "被封") {
443
+ blockedCount++;
444
+ console.error(` [被封] 累计 ${blockedCount} 次`);
445
+ if (blockedCount >= 3) {
446
+ await handleAccountSwitch(`账号被封累计 ${blockedCount} 次`);
447
+ blockedCount = 0;
448
+ }
449
+ continue;
450
+ }
451
+ if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
452
+ console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
453
+ break;
454
+ }
455
+ continue;
426
456
  }
427
- continue;
428
457
  }
429
458
 
430
459
  if (result.captchaDetected) {
@@ -42,7 +42,7 @@ async function processAPIResponse(
42
42
  }, newUrl);
43
43
  } catch (e) {
44
44
  if (
45
- e.message.includes('Execution context was destroyed') &&
45
+ e.message.includes("Execution context was destroyed") &&
46
46
  i < retries - 1
47
47
  ) {
48
48
  await delay(500 * (i + 1), 500 * (i + 1));
@@ -102,10 +102,10 @@ async function fetchUserVideosAPI(page, username, maxVideos, log) {
102
102
  let interceptionError = null;
103
103
 
104
104
  try {
105
- await page.goto(
106
- `https://www.tiktok.com/@${username}`,
107
- { waitUntil: "domcontentloaded", timeout: 30000 },
108
- );
105
+ await page.goto(`https://www.tiktok.com/@${username}`, {
106
+ waitUntil: "domcontentloaded",
107
+ timeout: 30000,
108
+ });
109
109
  await assertPageUrl(page, `@${username}`);
110
110
 
111
111
  const response = await page.waitForResponse(
@@ -158,4 +158,4 @@ async function fetchUserVideosAPI(page, username, maxVideos, log) {
158
158
  return new Map();
159
159
  }
160
160
 
161
- export { fetchUserVideosAPI };
161
+ export { fetchUserVideosAPI };
@@ -56,7 +56,9 @@ const PATTERNS = {
56
56
 
57
57
  export async function detectPageError(page) {
58
58
  return page.evaluate((patterns) => {
59
- const bodyText = document.body.innerText;
59
+ const body = document.body;
60
+ if (!body) return null;
61
+ const bodyText = body.innerText;
60
62
  const lower = bodyText.toLowerCase();
61
63
 
62
64
  for (const [type, phrases] of Object.entries(patterns)) {
@@ -81,7 +83,9 @@ export async function detectPageErrorWithWait(page, timeout = 8000) {
81
83
  try {
82
84
  const handle = await page.waitForFunction(
83
85
  (patterns) => {
84
- const bodyText = document.body.innerText;
86
+ const body = document.body;
87
+ if (!body) return null;
88
+ const bodyText = body.innerText;
85
89
  const lower = bodyText.toLowerCase();
86
90
 
87
91
  for (const [type, phrases] of Object.entries(patterns)) {
@@ -174,7 +174,7 @@ export class TikTokScraper {
174
174
  timeout: 15000,
175
175
  });
176
176
 
177
- return await page.evaluate(() => {
177
+ const content = await page.evaluate(() => {
178
178
  const rows = document.querySelectorAll("tr");
179
179
  let content = "";
180
180
  rows.forEach((r) => {
@@ -183,6 +183,16 @@ export class TikTokScraper {
183
183
  });
184
184
  return content;
185
185
  });
186
+
187
+ // 导航到 about:blank 释放当前页面的 DOM 和 JS 堆
188
+ await page
189
+ .goto("about:blank", {
190
+ waitUntil: "domcontentloaded",
191
+ timeout: 5000,
192
+ })
193
+ .catch(() => {});
194
+
195
+ return content;
186
196
  }
187
197
 
188
198
  async getUserInfo(uniqueId) {
@@ -173,6 +173,15 @@ async function processExplore(page, username, options, log) {
173
173
  result.noVideo = false;
174
174
  log(` @${username} 认定为被封`);
175
175
  }
176
+
177
+ // 临时性错误:超时、执行上下文销毁、网络异常 — 标记为可重试
178
+ const retryablePatterns = [
179
+ "Timeout",
180
+ "Execution context was destroyed",
181
+ "ERR_CONNECTION",
182
+ "net::",
183
+ ];
184
+ result.retryable = retryablePatterns.some((p) => e.message.includes(p));
176
185
  }
177
186
 
178
187
  return result;
@@ -14,7 +14,7 @@ async function getUserInfo(page) {
14
14
  return await page.evaluate(fn);
15
15
  } catch (e) {
16
16
  if (
17
- e.message.includes('Execution context was destroyed') &&
17
+ e.message.includes("Execution context was destroyed") &&
18
18
  i < retries - 1
19
19
  ) {
20
20
  await new Promise((r) => setTimeout(r, 500 * (i + 1)));