tt-help-cli-ycl 1.3.83 → 1.3.85

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.
@@ -24,6 +24,7 @@ import {
24
24
  } from "../lib/browser/cdp.js";
25
25
  import { showResourceUsage } from "../utils/index.js";
26
26
  import { HealthChecker } from "../lib/browser/health-checker.js";
27
+ import { createApiClient } from "../lib/api-client.js";
27
28
  import path from "path";
28
29
  import os from "os";
29
30
 
@@ -46,24 +47,6 @@ async function withRetry(label, fn) {
46
47
  }
47
48
  }
48
49
 
49
- async function apiPost(url, body) {
50
- return withRetry(`POST ${url}`, async () => {
51
- const res = await fetch(url, {
52
- method: "POST",
53
- headers: { "Content-Type": "application/json" },
54
- body: JSON.stringify(body),
55
- });
56
- return res.json();
57
- });
58
- }
59
-
60
- async function apiGet(url) {
61
- return withRetry(`GET ${url}`, async () => {
62
- const res = await fetch(url);
63
- return res.json();
64
- });
65
- }
66
-
67
50
  export async function handleExplore(options) {
68
51
  const {
69
52
  exploreUsernames,
@@ -118,15 +101,6 @@ export async function handleExplore(options) {
118
101
 
119
102
  setDelayConfig(explorePreset);
120
103
 
121
- await apiGet(`${serverUrl}/api/stats`);
122
-
123
- if (exploreUsernames && exploreUsernames.length > 0) {
124
- const { added, skipped } = await apiPost(`${serverUrl}/api/users`, {
125
- usernames: exploreUsernames,
126
- });
127
- console.error(`种子用户: ${added} 个新增, ${skipped} 个已存在`);
128
- }
129
-
130
104
  console.error(`\n国家筛选: ${exploreLocation}`);
131
105
  if (exploreJobLocations) console.error(`任务国家: ${exploreJobLocations}`);
132
106
  console.error(`视频采集: ${exploreMaxVideos || 1}`);
@@ -168,6 +142,17 @@ export async function handleExplore(options) {
168
142
 
169
143
  console.error(`CDP 端口: ${cdpOptions.port}, 用户编号: ${userId}`);
170
144
  console.error(`浏览器配置: ${path.basename(cdpOptions.userDataDir)}`);
145
+
146
+ const { apiGet, apiPost } = createApiClient({ meta: { port: cdpOptions.port } });
147
+
148
+ await apiGet(`${serverUrl}/api/stats`);
149
+
150
+ if (exploreUsernames && exploreUsernames.length > 0) {
151
+ const { added, skipped } = await apiPost(`${serverUrl}/api/users`, {
152
+ usernames: exploreUsernames,
153
+ });
154
+ console.error(`种子用户: ${added} 个新增, ${skipped} 个已存在`);
155
+ }
171
156
  if (!explorePort) {
172
157
  const portRange = `${currentAccount.port}-${currentAccount.port + healthChecker.accounts.length - 1}`;
173
158
  console.error(
@@ -269,140 +254,116 @@ export async function handleExplore(options) {
269
254
  ? `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}&locations=${encodeURIComponent(exploreJobLocations)}&loggedIn=${loggedIn}`
270
255
  : `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}&loggedIn=${loggedIn}`;
271
256
  const job = await apiGet(jobQuery);
272
- if (!job.hasJob) {
273
- console.error(`\n[Explore] 当前无任务,${exploreInterval} 秒后重试...`);
274
- await new Promise((r) => setTimeout(r, exploreInterval * 1000));
275
- continue;
276
- }
257
+ if (!job.hasJob) {
258
+ console.error(
259
+ `\n[Explore] 当前无任务,${exploreInterval} 秒后重试...`,
260
+ );
261
+ await new Promise((r) => setTimeout(r, exploreInterval * 1000));
262
+ continue;
263
+ }
277
264
 
278
- const username = job.user.uniqueId;
279
- processedCount++;
265
+ const username = job.user.uniqueId;
266
+ processedCount++;
280
267
 
281
- if (processedCount % 10 === 0) {
282
- showResourceUsage();
283
- }
268
+ if (processedCount % 10 === 0) {
269
+ showResourceUsage();
270
+ }
284
271
 
285
- const checkResult = healthChecker.check();
286
- if (checkResult.shouldSwitch) {
287
- await handleAccountSwitch(checkResult.reason);
288
- } else if (checkResult.info && processedCount % 10 === 0) {
289
- // 关注/粉丝封禁检测状态(仅登录状态)
290
- let followStatus = "";
291
- if (loggedIn && exploreEnableFollow) {
292
- const followElapsed = Math.round(
293
- (Date.now() - lastFollowSuccessTime) / 60000,
294
- );
295
- const followRemaining = Math.max(
296
- 0,
297
- Math.round(FOLLOW_BLOCK_THRESHOLD / 60000) - followElapsed,
298
- );
299
- followStatus = ` | 关注/粉丝上次成功 ${followElapsed} 分钟前 | 封禁检测 ${followRemaining} 分钟后`;
272
+ const checkResult = healthChecker.check();
273
+ if (checkResult.shouldSwitch) {
274
+ await handleAccountSwitch(checkResult.reason);
275
+ } else if (checkResult.info && processedCount % 10 === 0) {
276
+ // 关注/粉丝封禁检测状态(仅登录状态)
277
+ let followStatus = "";
278
+ if (loggedIn && exploreEnableFollow) {
279
+ const followElapsed = Math.round(
280
+ (Date.now() - lastFollowSuccessTime) / 60000,
281
+ );
282
+ const followRemaining = Math.max(
283
+ 0,
284
+ Math.round(FOLLOW_BLOCK_THRESHOLD / 60000) - followElapsed,
285
+ );
286
+ followStatus = ` | 关注/粉丝上次成功 ${followElapsed} 分钟前 | 封禁检测 ${followRemaining} 分钟后`;
287
+ }
288
+ console.error(`\n[健康检查] ${checkResult.info}${followStatus}`);
300
289
  }
301
- console.error(`\n[健康检查] ${checkResult.info}${followStatus}`);
302
- }
303
290
 
304
- // 切换任务前检测验证码
305
- const switchCaptcha = await detectCaptcha(page);
306
- if (switchCaptcha && switchCaptcha.visible) {
307
- console.error(`\n[验证码] 切换任务前检测到验证码,等待3分钟...`);
308
- captchaCount++;
309
- console.error(` [验证码] 累计 ${captchaCount} 次`);
291
+ // 切换任务前检测验证码
292
+ const switchCaptcha = await detectCaptcha(page);
293
+ if (switchCaptcha && switchCaptcha.visible) {
294
+ console.error(`\n[验证码] 切换任务前检测到验证码,等待3分钟...`);
295
+ captchaCount++;
296
+ console.error(` [验证码] 累计 ${captchaCount} 次`);
297
+
298
+ // 等待3分钟
299
+ await new Promise((r) => setTimeout(r, 3 * 60 * 1000));
300
+
301
+ // 尝试关闭
302
+ const closeResult = await closeCaptcha(page);
303
+ if (closeResult.success) {
304
+ console.error(` [验证码] 已关闭`);
305
+ } else {
306
+ console.error(` [验证码] 关闭失败: ${closeResult.reason}`);
307
+ }
310
308
 
311
- // 等待3分钟
312
- await new Promise((r) => setTimeout(r, 3 * 60 * 1000));
309
+ // 上报
310
+ await withRetry("report captcha", () =>
311
+ apiPost(`${serverUrl}/api/error-report`, {
312
+ userId,
313
+ username: "unknown",
314
+ errorType: "captcha",
315
+ errorMessage: "切换任务前检测到验证码",
316
+ stage: "between-tasks",
317
+ errorStack: "",
318
+ }),
319
+ ).catch(() => {});
313
320
 
314
- // 尝试关闭
315
- const closeResult = await closeCaptcha(page);
316
- if (closeResult.success) {
317
- console.error(` [验证码] 已关闭`);
318
- } else {
319
- console.error(` [验证码] 关闭失败: ${closeResult.reason}`);
321
+ // 累计2次切换账户
322
+ if (captchaCount >= 2) {
323
+ await handleAccountSwitch(`验证码累计 ${captchaCount} 次`);
324
+ captchaCount = 0;
325
+ }
320
326
  }
321
327
 
322
- // 上报
323
- await withRetry("report captcha", () =>
324
- apiPost(`${serverUrl}/api/error-report`, {
325
- userId,
326
- username: "unknown",
327
- errorType: "captcha",
328
- errorMessage: "切换任务前检测到验证码",
329
- stage: "between-tasks",
330
- errorStack: "",
331
- }),
332
- ).catch(() => {});
333
-
334
- // 累计2次切换账户
335
- if (captchaCount >= 2) {
336
- await handleAccountSwitch(`验证码累计 ${captchaCount} 次`);
337
- captchaCount = 0;
328
+ // 关注/粉丝封禁检测移到健康检查中(仅登录状态下)
329
+ if (loggedIn && exploreEnableFollow) {
330
+ const followElapsed = Date.now() - lastFollowSuccessTime;
331
+ if (followElapsed > FOLLOW_BLOCK_THRESHOLD) {
332
+ const minutes = Math.round(followElapsed / 60000);
333
+ console.error(
334
+ `\n[封禁检测] 关注/粉丝功能已 ${minutes} 分钟未成功获取,切换到下一个账户...`,
335
+ );
336
+ await handleAccountSwitch(`关注/粉丝功能被封`);
337
+ }
338
338
  }
339
- }
340
339
 
341
- // 关注/粉丝封禁检测移到健康检查中(仅登录状态下)
342
- if (loggedIn && exploreEnableFollow) {
343
- const followElapsed = Date.now() - lastFollowSuccessTime;
344
- if (followElapsed > FOLLOW_BLOCK_THRESHOLD) {
345
- const minutes = Math.round(followElapsed / 60000);
346
- console.error(
347
- `\n[封禁检测] 关注/粉丝功能已 ${minutes} 分钟未成功获取,切换到下一个账户...`,
348
- );
349
- await handleAccountSwitch(`关注/粉丝功能被封`);
340
+ if (consecutiveNetworkErrors > 0) {
341
+ const waitTime =
342
+ consecutiveNetworkErrors <= 2
343
+ ? 0
344
+ : consecutiveNetworkErrors <= 5
345
+ ? 30000
346
+ : 300000;
347
+ if (waitTime > 0) {
348
+ console.error(
349
+ ` [网络] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 ${waitTime / 1000}s 后重试...`,
350
+ );
351
+ await new Promise((r) => setTimeout(r, waitTime));
352
+ }
350
353
  }
351
- }
352
354
 
353
- if (consecutiveNetworkErrors > 0) {
354
- const waitTime =
355
- consecutiveNetworkErrors <= 2
356
- ? 0
357
- : consecutiveNetworkErrors <= 5
358
- ? 30000
359
- : 300000;
360
- if (waitTime > 0) {
361
- console.error(
362
- ` [网络] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 ${waitTime / 1000}s 后重试...`,
363
- );
364
- await new Promise((r) => setTimeout(r, waitTime));
365
- }
366
- }
355
+ console.error(`\n[${processedCount}] 探索 @${username}...`);
367
356
 
368
- console.error(`\n[${processedCount}] 探索 @${username}...`);
369
-
370
- const { switchMax } = getDelayConfig();
371
- await delay(switchMax, switchMax * 3);
372
-
373
- let result = await processExplore(
374
- page,
375
- username,
376
- {
377
- maxVideos: exploreMaxVideos,
378
- enableFollow: exploreEnableFollow,
379
- loggedIn, // 传入登录状态,避免每次调用 isLoggedIn(page)
380
- maxFollowing: exploreMaxFollowing,
381
- maxFollowers: exploreMaxFollowers,
382
- location: exploreLocation,
383
- proxyServer: cdpOptions.proxyServer || null,
384
- browser,
385
- },
386
- console.error,
387
- );
357
+ const { switchMax } = getDelayConfig();
358
+ await delay(switchMax, switchMax * 3);
388
359
 
389
- // 浏览器关闭检测:processExplore 内部 catch 了异常,需要从 result.error 判断
390
- if (result.error && isBrowserClosedError(new Error(result.error))) {
391
- const newBrowser = await relaunchBrowser(
392
- cdpOptions,
393
- cdpOptions.port || 9222,
394
- );
395
- browser = newBrowser;
396
- const newPage = await setupNewPage(browser);
397
- Object.assign(page, newPage);
398
- // 重试当前用户
399
- result = await processExplore(
360
+ let result = await processExplore(
400
361
  page,
401
362
  username,
402
363
  {
403
364
  maxVideos: exploreMaxVideos,
404
365
  enableFollow: exploreEnableFollow,
405
- loggedIn, // 传入登录状态
366
+ loggedIn, // 传入登录状态,避免每次调用 isLoggedIn(page)
406
367
  maxFollowing: exploreMaxFollowing,
407
368
  maxFollowers: exploreMaxFollowers,
408
369
  location: exploreLocation,
@@ -411,36 +372,24 @@ export async function handleExplore(options) {
411
372
  },
412
373
  console.error,
413
374
  );
414
- }
415
375
 
416
- if (result.restricted) {
417
- consecutiveNetworkErrors = 0;
418
- await apiPost(`${serverUrl}/api/job/${username}`, {
419
- restricted: true,
420
- userInfo: result.userInfo || {},
421
- });
422
- if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
423
- console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
424
- break;
425
- }
426
- continue;
427
- }
428
-
429
- if (result.error) {
430
- consecutiveNetworkErrors++;
431
- errorCount++;
432
-
433
- // 临时性错误:自动重试一次
434
- if (result.retryable) {
435
- console.error(` [临时错误] 等待 5 秒后重试 @${username}...`);
436
- await new Promise((r) => setTimeout(r, 5000));
376
+ // 浏览器关闭检测:processExplore 内部 catch 了异常,需要从 result.error 判断
377
+ if (result.error && isBrowserClosedError(new Error(result.error))) {
378
+ const newBrowser = await relaunchBrowser(
379
+ cdpOptions,
380
+ cdpOptions.port || 9222,
381
+ );
382
+ browser = newBrowser;
383
+ const newPage = await setupNewPage(browser);
384
+ Object.assign(page, newPage);
385
+ // 重试当前用户
437
386
  result = await processExplore(
438
387
  page,
439
388
  username,
440
389
  {
441
390
  maxVideos: exploreMaxVideos,
442
391
  enableFollow: exploreEnableFollow,
443
- loggedIn,
392
+ loggedIn, // 传入登录状态
444
393
  maxFollowing: exploreMaxFollowing,
445
394
  maxFollowers: exploreMaxFollowers,
446
395
  location: exploreLocation,
@@ -449,127 +398,167 @@ export async function handleExplore(options) {
449
398
  },
450
399
  console.error,
451
400
  );
452
- // 重试成功后继续正常流程
453
- if (!result.error) {
454
- consecutiveNetworkErrors = 0;
455
- errorCount--;
456
- }
457
401
  }
458
402
 
459
- if (result.error) {
460
- // 上报错误(重试后仍有错误才上报)
403
+ if (result.restricted) {
404
+ consecutiveNetworkErrors = 0;
461
405
  await apiPost(`${serverUrl}/api/job/${username}`, {
462
- error: result.error,
406
+ restricted: true,
407
+ userInfo: result.userInfo || {},
463
408
  });
464
- const errorType = result.error.startsWith("被封:")
465
- ? "被封"
466
- : consecutiveNetworkErrors > 1
467
- ? "network"
468
- : "other";
469
- await withRetry("report error", () =>
470
- apiPost(`${serverUrl}/api/error-report`, {
471
- userId,
472
- username,
473
- errorType,
474
- errorMessage: result.error,
475
- stage: "process",
476
- errorStack: result.errorStack || "",
477
- }),
478
- ).catch(() => {});
479
- if (errorType === "被封") {
480
- blockedCount++;
481
- console.error(` [被封] 累计 ${blockedCount} 次`);
482
- if (blockedCount >= 3) {
483
- await handleAccountSwitch(`账号被封累计 ${blockedCount} 次`);
484
- blockedCount = 0;
485
- }
486
- continue;
487
- }
488
409
  if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
489
410
  console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
490
411
  break;
491
412
  }
492
413
  continue;
493
414
  }
494
- }
495
415
 
496
- if (result.captchaDetected) {
497
- captchaCount++;
498
- console.error(` [验证码] 累计 ${captchaCount} 次`);
416
+ if (result.error) {
417
+ consecutiveNetworkErrors++;
418
+ errorCount++;
419
+
420
+ // 临时性错误:自动重试一次
421
+ if (result.retryable) {
422
+ console.error(` [临时错误] 等待 5 秒后重试 @${username}...`);
423
+ await new Promise((r) => setTimeout(r, 5000));
424
+ result = await processExplore(
425
+ page,
426
+ username,
427
+ {
428
+ maxVideos: exploreMaxVideos,
429
+ enableFollow: exploreEnableFollow,
430
+ loggedIn,
431
+ maxFollowing: exploreMaxFollowing,
432
+ maxFollowers: exploreMaxFollowers,
433
+ location: exploreLocation,
434
+ proxyServer: cdpOptions.proxyServer || null,
435
+ browser,
436
+ },
437
+ console.error,
438
+ );
439
+ // 重试成功后继续正常流程
440
+ if (!result.error) {
441
+ consecutiveNetworkErrors = 0;
442
+ errorCount--;
443
+ }
444
+ }
499
445
 
500
- await withRetry("report captcha", () =>
501
- apiPost(`${serverUrl}/api/error-report`, {
502
- userId,
503
- username,
504
- errorType: "captcha",
505
- errorMessage: result.captchaMessage || "页面出现验证码",
506
- stage: result.captchaStage || "video-page",
507
- errorStack: "",
508
- }),
509
- ).catch(() => {});
510
-
511
- // 累计2次验证码,切换账户
512
- if (captchaCount >= 2) {
513
- await handleAccountSwitch(`验证码累计 ${captchaCount} 次`);
514
- captchaCount = 0;
446
+ if (result.error) {
447
+ // 上报错误(重试后仍有错误才上报)
448
+ await apiPost(`${serverUrl}/api/job/${username}`, {
449
+ error: result.error,
450
+ });
451
+ const errorType = result.error.startsWith("被封:")
452
+ ? "被封"
453
+ : consecutiveNetworkErrors > 1
454
+ ? "network"
455
+ : "other";
456
+ await withRetry("report error", () =>
457
+ apiPost(`${serverUrl}/api/error-report`, {
458
+ userId,
459
+ username,
460
+ errorType,
461
+ errorMessage: result.error,
462
+ stage: "process",
463
+ errorStack: result.errorStack || "",
464
+ }),
465
+ ).catch(() => {});
466
+ if (errorType === "被封") {
467
+ blockedCount++;
468
+ console.error(` [被封] 累计 ${blockedCount} 次`);
469
+ if (blockedCount >= 3) {
470
+ await handleAccountSwitch(`账号被封累计 ${blockedCount} 次`);
471
+ blockedCount = 0;
472
+ }
473
+ continue;
474
+ }
475
+ if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
476
+ console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
477
+ break;
478
+ }
479
+ continue;
480
+ }
515
481
  }
516
- }
517
482
 
518
- consecutiveNetworkErrors = 0;
483
+ if (result.captchaDetected) {
484
+ captchaCount++;
485
+ console.error(` [验证码] 累计 ${captchaCount} 次`);
486
+
487
+ await withRetry("report captcha", () =>
488
+ apiPost(`${serverUrl}/api/error-report`, {
489
+ userId,
490
+ username,
491
+ errorType: "captcha",
492
+ errorMessage: result.captchaMessage || "页面出现验证码",
493
+ stage: result.captchaStage || "video-page",
494
+ errorStack: "",
495
+ }),
496
+ ).catch(() => {});
519
497
 
520
- // 更新关注/粉丝成功时间(仅登录状态下)
521
- if (result.hasFollowData && result.keepFollow) {
522
- const totalFollows =
523
- (result.discoveredFollowing || []).length +
524
- (result.discoveredFollowers || []).length;
525
- if (totalFollows > 0) {
526
- lastFollowSuccessTime = Date.now();
498
+ // 累计2次验证码,切换账户
499
+ if (captchaCount >= 2) {
500
+ await handleAccountSwitch(`验证码累计 ${captchaCount} 次`);
501
+ captchaCount = 0;
502
+ }
527
503
  }
528
- }
529
504
 
530
- const guessedLocation = result.locationCreated || null;
531
-
532
- const payload = {
533
- userInfo: result.userInfo || {},
534
- discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
535
- handle: Array.isArray(f) ? f[0] : f,
536
- displayName: Array.isArray(f) ? f[1] : null,
537
- guessedLocation,
538
- })),
539
- discoveredFollowers: (result.discoveredFollowers || []).map((f) => ({
540
- handle: Array.isArray(f) ? f[0] : f,
541
- displayName: Array.isArray(f) ? f[1] : null,
542
- guessedLocation,
543
- })),
544
- processed: result.processed,
545
- hasFollowData: result.hasFollowData,
546
- keepFollow: result.keepFollow,
547
- locationCreated: result.locationCreated,
548
- noVideo: result.noVideo,
549
- collectedVideos: result.collectedVideos,
550
- };
551
- await apiPost(`${serverUrl}/api/job/${username}`, payload);
552
-
553
- // 视频登记
554
- if (result.videoList && result.videoList.length > 0) {
555
- const { registered, skipped } = await apiPost(
556
- `${serverUrl}/api/videos`,
557
- {
558
- sourceUser: username,
559
- videoList: result.videoList,
560
- locationCreated: result.locationCreated,
561
- ttSeller: result.userInfo?.ttSeller || false,
562
- },
563
- );
564
- console.error(` 视频登记: ${registered} 条新增, ${skipped} 条已存在`);
565
- }
505
+ consecutiveNetworkErrors = 0;
566
506
 
567
- console.error(" 已提交");
507
+ // 更新关注/粉丝成功时间(仅登录状态下)
508
+ if (result.hasFollowData && result.keepFollow) {
509
+ const totalFollows =
510
+ (result.discoveredFollowing || []).length +
511
+ (result.discoveredFollowers || []).length;
512
+ if (totalFollows > 0) {
513
+ lastFollowSuccessTime = Date.now();
514
+ }
515
+ }
568
516
 
569
- if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
570
- console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
571
- break;
572
- }
517
+ const guessedLocation = result.locationCreated || null;
518
+
519
+ const payload = {
520
+ userInfo: result.userInfo || {},
521
+ discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
522
+ handle: Array.isArray(f) ? f[0] : f,
523
+ displayName: Array.isArray(f) ? f[1] : null,
524
+ guessedLocation,
525
+ })),
526
+ discoveredFollowers: (result.discoveredFollowers || []).map((f) => ({
527
+ handle: Array.isArray(f) ? f[0] : f,
528
+ displayName: Array.isArray(f) ? f[1] : null,
529
+ guessedLocation,
530
+ })),
531
+ processed: result.processed,
532
+ hasFollowData: result.hasFollowData,
533
+ keepFollow: result.keepFollow,
534
+ locationCreated: result.locationCreated,
535
+ noVideo: result.noVideo,
536
+ collectedVideos: result.collectedVideos,
537
+ };
538
+ await apiPost(`${serverUrl}/api/job/${username}`, payload);
539
+
540
+ // 视频登记
541
+ if (result.videoList && result.videoList.length > 0) {
542
+ const { registered, skipped } = await apiPost(
543
+ `${serverUrl}/api/videos`,
544
+ {
545
+ sourceUser: username,
546
+ videoList: result.videoList,
547
+ locationCreated: result.locationCreated,
548
+ ttSeller: result.userInfo?.ttSeller || false,
549
+ },
550
+ );
551
+ console.error(
552
+ ` 视频登记: ${registered} 条新增, ${skipped} 条已存在`,
553
+ );
554
+ }
555
+
556
+ console.error(" 已提交");
557
+
558
+ if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
559
+ console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
560
+ break;
561
+ }
573
562
  } catch (e) {
574
563
  // 浏览器关闭错误:自动重建 browser + page,然后重试当前轮次
575
564
  if (isBrowserClosedError(e)) {
@@ -23,6 +23,7 @@ import {
23
23
  switchAccount,
24
24
  } from "../lib/browser/cdp.js";
25
25
  import { HealthChecker } from "../lib/browser/health-checker.js";
26
+ import { createApiClient } from "../lib/api-client.js";
26
27
  import path from "path";
27
28
  import os from "os";
28
29
 
@@ -45,24 +46,6 @@ async function withRetry(label, fn) {
45
46
  }
46
47
  }
47
48
 
48
- async function apiPost(url, body) {
49
- return withRetry(`POST ${url}`, async () => {
50
- const res = await fetch(url, {
51
- method: "POST",
52
- headers: { "Content-Type": "application/json" },
53
- body: JSON.stringify(body),
54
- });
55
- return res.json();
56
- });
57
- }
58
-
59
- async function apiGet(url) {
60
- return withRetry(`GET ${url}`, async () => {
61
- const res = await fetch(url);
62
- return res.json();
63
- });
64
- }
65
-
66
49
  export async function handleRefresh(options) {
67
50
  const {
68
51
  explorePreset,
@@ -116,9 +99,6 @@ export async function handleRefresh(options) {
116
99
 
117
100
  setDelayConfig(explorePreset);
118
101
 
119
- // 连接服务器验证
120
- await apiGet(`${serverUrl}/api/stats`);
121
-
122
102
  console.error(`\n=== Refresh 模式(基于 explore) ===`);
123
103
  console.error(`服务器: ${serverUrl}`);
124
104
  console.error(`视频采集: ${exploreMaxVideos || 16}`);
@@ -175,6 +155,11 @@ export async function handleRefresh(options) {
175
155
  );
176
156
  }
177
157
 
158
+ const { apiGet, apiPost } = createApiClient({ meta: { port: cdpOptions.port } });
159
+
160
+ // 连接服务器验证
161
+ await apiGet(`${serverUrl}/api/stats`);
162
+
178
163
  browser = await ensureBrowserReadyCDP(cdpOptions);
179
164
  const { processExplore } = await import("../scraper/explore-core.js");
180
165
  const { isLoggedIn } = await import("../lib/browser/page.js");