tt-help-cli-ycl 1.4.154 → 1.4.155

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.
@@ -47,6 +47,8 @@ export class ClashDaemon {
47
47
  this.disconnectSampleMs =
48
48
  opts.disconnectSampleMs > 0 ? opts.disconnectSampleMs : 1000;
49
49
  this.dryRun = !!opts.dryRun;
50
+ // 白名单守卫(默认开启):当前节点必须 ∈ 候选集(咱们节点列表),不在的不允许,强制切回
51
+ this.whitelistGuard = opts.whitelistGuard !== false;
50
52
  // 控制端发现参数(CLI 显式值优先,内部再合并 ~/.tt-help.json)
51
53
  this._discoverOpts = {
52
54
  apiPort: opts.apiPort,
@@ -168,6 +170,32 @@ export class ClashDaemon {
168
170
  });
169
171
  }
170
172
 
173
+ /**
174
+ * 逐个尝试候选节点并内容验证(巡检/failover/白名单守卫共用切换循环)
175
+ * @returns {{target: string|null, h2: object|null}} 第一个满足(健康或壳页)的候选;全部失败返回 {target:null}
176
+ */
177
+ async _tryRotateCandidates(c, ordered, currentNode, reason = "") {
178
+ const tag = reason ? ` [原因: ${reason}]` : "";
179
+ let target = null;
180
+ let h2 = null;
181
+ for (const cand of ordered) {
182
+ if (cand === currentNode) continue;
183
+ this._log(`↺ [${c.proxyPort}] 尝试候选: ${cand}${tag}`);
184
+ if (!(await checker.switchToNode(cand))) {
185
+ this._log(` ↺ ${cand} 切换失败,下一个`);
186
+ continue;
187
+ }
188
+ this._markSwitch(c, cand);
189
+ h2 = checker.checkNodeHealth();
190
+ if (h2.healthy || this._isShellPage(h2)) {
191
+ target = cand;
192
+ break;
193
+ }
194
+ this._log(` ↺ ${cand} 内容验证未通过,下一个`);
195
+ }
196
+ return { target, h2 };
197
+ }
198
+
171
199
  /** 将全局 api/checker 上下文指向指定控制器 */
172
200
  _useController(c) {
173
201
  api.configure({ port: c.port, secret: c.secret });
@@ -402,6 +430,8 @@ export class ClashDaemon {
402
430
  this._log(
403
431
  `✓ [${c.proxyPort}] ${h.timeTotal.toFixed(1)}s ${(h.sizeDownload / 1024).toFixed(0)}KB`,
404
432
  );
433
+ // 白名单守卫:节点健康但不在咱们节点列表内(如人工在 Clash Verge 界面手动切换)→ 强制切回
434
+ await this._enforceWhitelist(c, node);
405
435
  return;
406
436
  }
407
437
 
@@ -412,6 +442,8 @@ export class ClashDaemon {
412
442
  this._log(
413
443
  `~ [${c.proxyPort}] 空壳页(降级响应,链路正常),不切换 (${(h.sizeDownload / 1024).toFixed(0)}KB ${h.timeTotal.toFixed(1)}s)`,
414
444
  );
445
+ // 白名单守卫与主分支同口径:壳页=链路正常,但列表外节点仍不允许
446
+ if (await this._enforceWhitelist(c, node)) return;
415
447
  return this._writeHealth(c, {
416
448
  node,
417
449
  healthy: true,
@@ -432,6 +464,8 @@ export class ClashDaemon {
432
464
  this._log(
433
465
  `~ [${c.proxyPort}] 复测恢复(首次异常: ${firstReason}),不切换`,
434
466
  );
467
+ // 白名单守卫:复测恢复同样守口径(列表外节点不允许)
468
+ if (await this._enforceWhitelist(c, node)) return;
435
469
  return this._writeHealth(c, {
436
470
  node,
437
471
  healthy: true,
@@ -476,23 +510,12 @@ export class ClashDaemon {
476
510
  // 逐个切换候选 + 内容验证,第一个满足即采用
477
511
  // 排序:切过的节点按切换时间沉底(后进后出),优先尝试"最久没被切过"的节点
478
512
  const ordered = this._sortCandidates(nodes, c);
479
- let target = null;
480
- let h2 = null;
481
- for (const cand of ordered) {
482
- if (cand === node) continue;
483
- this._log(`↺ [${c.proxyPort}] 尝试候选: ${cand} [原因: ${reason}]`);
484
- if (!(await checker.switchToNode(cand))) {
485
- this._log(` ↺ ${cand} 切换失败,下一个`);
486
- continue;
487
- }
488
- this._markSwitch(c, cand);
489
- h2 = checker.checkNodeHealth();
490
- if (h2.healthy || this._isShellPage(h2)) {
491
- target = cand;
492
- break;
493
- }
494
- this._log(` ↺ ${cand} 内容验证未通过,下一个`);
495
- }
513
+ const { target, h2 } = await this._tryRotateCandidates(
514
+ c,
515
+ ordered,
516
+ node,
517
+ reason,
518
+ );
496
519
 
497
520
  if (!target) {
498
521
  // 全部候选不可用,恢复原节点
@@ -564,6 +587,75 @@ export class ClashDaemon {
564
587
  }
565
588
  }
566
589
 
590
+ /**
591
+ * 白名单守卫:当前 Clash Verge 正在使用的节点必须 ∈ 候选集(咱们节点列表)
592
+ *
593
+ * 场景:人工在 Clash Verge 界面手动切到列表外节点、其它程序乱切等。
594
+ * 每轮巡检执行(健康分支):发现越界 → 立即切回候选集
595
+ *(按轮换排序逐个切换+内容验证,第一个满足即采用,同巡检切换口径)。
596
+ *
597
+ * 不受 failover 冷却期约束(越界纠正而非故障切换);候选集为空时无法判定,
598
+ * 放行(由 delay 初测逻辑自动补齐,下一轮巡检再守卫)。
599
+ *
600
+ * @returns {Promise<boolean>} true=已强制切回候选集(调用方可据此跳过自身的 health 写入)
601
+ */
602
+ async _enforceWhitelist(c, node) {
603
+ if (!this.whitelistGuard) return false;
604
+ if (!node || node === "?") return false;
605
+ const { nodes } = checker.loadHealthyList();
606
+ if (!nodes.length || nodes.includes(node)) return false;
607
+ if (this.dryRun) {
608
+ this._log(
609
+ `[DRY-RUN] [${c.proxyPort}] 白名单守卫: 当前节点 ${node} 不在候选集(${nodes.length}个)内,应切回`,
610
+ );
611
+ return false;
612
+ }
613
+ this._log(
614
+ `⛔ [${c.proxyPort}] 白名单守卫: 当前节点 ${node} 不在咱们节点列表(${nodes.length}个)内,切回`,
615
+ );
616
+ const ordered = this._sortCandidates(nodes, c);
617
+ const { target, h2 } = await this._tryRotateCandidates(
618
+ c,
619
+ ordered,
620
+ node,
621
+ "白名单守卫",
622
+ );
623
+ if (!target) {
624
+ // 与 failover/巡检切换口径一致:候选全部不可用时恢复原节点
625
+ //(_tryRotateCandidates 逐个试切后,当前实际已停在最后尝试的候选上)
626
+ await checker.switchToNode(node).catch(() => {});
627
+ this._markSwitch(c, node);
628
+ this._markSwitchTime(c);
629
+ await this._disconnect(c);
630
+ this._log(
631
+ `✗ [${c.proxyPort}] 白名单守卫: 候选全部不可用,恢复原节点 ${node}`,
632
+ );
633
+ this._writeHealth(c, {
634
+ node,
635
+ healthy: false,
636
+ error: "白名单守卫 — 候选全部不可用",
637
+ lastSwitch: `白名单守卫: 候选全部不可用,恢复原节点 ${node}`,
638
+ });
639
+ return false;
640
+ }
641
+ this._log(
642
+ `⚠ [${c.proxyPort}] 节点切换 [白名单守卫]: ${node} → ${target} (${h2.timeTotal.toFixed(1)}s ${(h2.sizeDownload / 1024).toFixed(0)}KB)`,
643
+ );
644
+ // 切换后优雅断连:回收空闲连接,活跃放行(同巡检切换)
645
+ await this._disconnect(c);
646
+ this._markSwitchTime(c);
647
+ this._writeHealth(c, {
648
+ node: target,
649
+ healthy: h2.healthy,
650
+ httpCode: h2.httpCode,
651
+ timeTotal: h2.timeTotal,
652
+ sizeDownload: h2.sizeDownload,
653
+ title: h2.title,
654
+ lastSwitch: `白名单守卫: ${node} → ${target}`,
655
+ });
656
+ return true;
657
+ }
658
+
567
659
  // ── 候选集 delay 初测(每个控制端独立,零切换) ──
568
660
 
569
661
  _runDelayProbeAll() {
@@ -753,23 +845,12 @@ export class ClashDaemon {
753
845
  // 逐个切换候选 + 内容验证,第一个满足即采用(跳过当前节点)
754
846
  // 排序:切过的节点按切换时间沉底(后进后出),优先尝试"最久没被切过"的节点
755
847
  const ordered = this._sortCandidates(nodes, c);
756
- let target = null;
757
- let h2 = null;
758
- for (const cand of ordered) {
759
- if (cand === node) continue;
760
- this._log(`↺ 尝试候选: ${cand}`);
761
- if (!(await checker.switchToNode(cand))) {
762
- this._log(` ↺ ${cand} 切换失败,下一个`);
763
- continue;
764
- }
765
- this._markSwitch(c, cand);
766
- h2 = checker.checkNodeHealth();
767
- if (h2.healthy || this._isShellPage(h2)) {
768
- target = cand;
769
- break;
770
- }
771
- this._log(` ↺ ${cand} 内容验证未通过,下一个`);
772
- }
848
+ const { target, h2 } = await this._tryRotateCandidates(
849
+ c,
850
+ ordered,
851
+ node,
852
+ reason,
853
+ );
773
854
  // 本次 failover 已尽力处置(成功切换或恢复原节点):更新锚点,给新节点干净观察期
774
855
  this._markSwitchTime(c);
775
856
 
@@ -839,7 +920,7 @@ export class ClashDaemon {
839
920
  ` 检测: ${checker.CONFIG.testUrl} | 巡检: ${this.intervalMs / 1000}s | 候选刷新: ${this.benchmarkMs / 60000}min${filterTag}`,
840
921
  );
841
922
  console.error(
842
- ` 模式: ${this.dryRun ? "DRY-RUN(只检测不切换)" : "正常运行"}`,
923
+ ` 模式: ${this.dryRun ? "DRY-RUN(只检测不切换)" : "正常运行"} | 白名单守卫: ${this.whitelistGuard ? "开启(列表外节点强制切回)" : "关闭"}`,
843
924
  );
844
925
  console.error("═".repeat(50));
845
926
  }
@@ -882,6 +963,14 @@ function _buildCmdHandlers(daemon) {
882
963
  daemon._log(`📋 检测URL: → ${cmd.url}`);
883
964
  }
884
965
  },
966
+ setWhitelistGuard(cmd) {
967
+ const prev = daemon.whitelistGuard;
968
+ const next = cmd.enabled !== false && cmd.enabled !== "false";
969
+ daemon.whitelistGuard = next;
970
+ daemon._log(
971
+ `📋 白名单守卫: ${prev ? "开启" : "关闭"} → ${next ? "开启" : "关闭"}`,
972
+ );
973
+ },
885
974
  runBenchmark() {
886
975
  daemon._log("📋 收到手动刷新候选集命令");
887
976
  daemon._runDelayProbeAll();
@@ -911,6 +1000,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
911
1000
  benchmarkMin: parseInt(getArg("--benchmark")) || undefined,
912
1001
  country: getArg("--country") || undefined,
913
1002
  dryRun: args.includes("--dry-run"),
1003
+ whitelistGuard: !args.includes("--no-whitelist-guard"),
914
1004
  apiPort: parseInt(getArg("--api-port")) || undefined,
915
1005
  secret: getArg("--secret") || undefined,
916
1006
  selection: getArg("--selection") || undefined,
@@ -16,6 +16,7 @@ export async function handleClashDaemon(options) {
16
16
  options["dry-run"] === "" ||
17
17
  options["dry-run"] === true ||
18
18
  process.argv.includes("--dry-run"),
19
+ whitelistGuard: !options["no-whitelist-guard"],
19
20
  apiPort: parseInt(options["api-port"]) || undefined,
20
21
  secret: options.secret || undefined,
21
22
  selection: options.selection || undefined,
@@ -76,6 +76,7 @@ export async function handleExplore(options) {
76
76
  exploreNoProduct,
77
77
  exploreSwitchDelay,
78
78
  exploreCommentDelay,
79
+ captchaWaitMin,
79
80
  } = options;
80
81
 
81
82
  let userId = exploreUserId || configuredUserId;
@@ -352,6 +353,44 @@ export async function handleExplore(options) {
352
353
  switchAccount: (reason) => handleAccountSwitch(reason),
353
354
  });
354
355
 
356
+ /** 等待人工在浏览器中解决验证码:轮询检测,解决即继续;超时返回 false */
357
+ async function waitForCaptchaResolved() {
358
+ const waitMin = captchaWaitMin || 3;
359
+ const deadline = Date.now() + waitMin * 60000;
360
+ console.error(
361
+ ` [验证码] 等待人工解决(上限 ${waitMin} 分钟,每15秒检测一次,解决即继续)...`,
362
+ );
363
+ while (Date.now() < deadline) {
364
+ await new Promise((r) => setTimeout(r, 15000));
365
+ let visible = true;
366
+ try {
367
+ const r = await detectCaptcha(page);
368
+ visible = !!(r && r.visible);
369
+ } catch {
370
+ visible = false; // 检测异常(页面导航中/重建中)按已解决处理,避免卡死
371
+ }
372
+ if (!visible) {
373
+ console.error(" [验证码] 已解决,继续处理");
374
+ return true;
375
+ }
376
+ }
377
+ console.error(` [验证码] 等待 ${waitMin} 分钟未解决,尝试关闭...`);
378
+ // 超时后尝试关闭验证码弹窗,关不掉由调用方计数切账户
379
+ await closeCaptcha(page).catch(() => {});
380
+ await new Promise((r) => setTimeout(r, 2000));
381
+ try {
382
+ const r = await detectCaptcha(page);
383
+ if (!(r && r.visible)) {
384
+ console.error(" [验证码] 超时后关闭成功,继续处理");
385
+ return true;
386
+ }
387
+ } catch {
388
+ /* 忽略 */
389
+ }
390
+ console.error(" [验证码] 超时且关闭失败");
391
+ return false;
392
+ }
393
+
355
394
  let processedCount = 0;
356
395
  let errorCount = 0;
357
396
  let consecutiveNetworkErrors = 0;
@@ -403,20 +442,13 @@ export async function handleExplore(options) {
403
442
  // 切换任务前检测验证码
404
443
  const switchCaptcha = await detectCaptcha(page);
405
444
  if (switchCaptcha && switchCaptcha.visible) {
406
- console.error(`\n[验证码] 切换任务前检测到验证码,等待3分钟...`);
445
+ console.error(`\n[验证码] 切换任务前检测到验证码`);
407
446
  captchaCount++;
408
447
  console.error(` [验证码] 累计 ${captchaCount} 次`);
409
448
 
410
- // 等待3分钟
411
- await new Promise((r) => setTimeout(r, 3 * 60 * 1000));
412
-
413
- // 尝试关闭
414
- const closeResult = await closeCaptcha(page);
415
- if (closeResult.success) {
416
- console.error(` [验证码] 已关闭`);
417
- } else {
418
- console.error(` [验证码] 关闭失败: ${closeResult.reason}`);
419
- }
449
+ // 轮询等待人工解决(解决即继续,超时尝试关闭);
450
+ // 不再盲等 3 分钟——同一个验证码若未解决会在下一轮重复计数凑满切账户阈值
451
+ const resolved = await waitForCaptchaResolved();
420
452
 
421
453
  // 上报
422
454
  await withRetry("report captcha", () =>
@@ -424,16 +456,24 @@ export async function handleExplore(options) {
424
456
  userId,
425
457
  username: "unknown",
426
458
  errorType: "captcha",
427
- errorMessage: "切换任务前检测到验证码",
459
+ errorMessage: resolved
460
+ ? "切换任务前检测到验证码(已解决)"
461
+ : "切换任务前检测到验证码(超时未解决)",
428
462
  stage: "between-tasks",
429
463
  errorStack: "",
430
464
  }),
431
465
  ).catch(() => {});
432
466
 
433
- // 累计2次切换账户
434
- if (captchaCount >= 2) {
435
- await handleAccountSwitch(`验证码累计 ${captchaCount} 次`);
436
- captchaCount = 0;
467
+ // 超时未解决才累计切账户;已解决的不计入(人工处理完了不应消耗账户)
468
+ if (!resolved) {
469
+ if (captchaCount >= 3) {
470
+ await handleAccountSwitch(
471
+ `验证码累计 ${captchaCount} 次(超时未解决)`,
472
+ );
473
+ captchaCount = 0;
474
+ }
475
+ } else {
476
+ captchaCount--; // 解决了就不算一次独立事件
437
477
  }
438
478
  }
439
479
 
@@ -657,9 +697,17 @@ export async function handleExplore(options) {
657
697
  }),
658
698
  ).catch(() => {});
659
699
 
660
- // 累计2次验证码,切换账户
661
- if (captchaCount >= 2) {
662
- await handleAccountSwitch(`验证码累计 ${captchaCount} 次`);
700
+ // 轮询等待人工解决(解决即继续):
701
+ // 不等待会导致同一个验证码在下一轮"切换任务前检测"重复计数,白白凑满阈值切账户
702
+ const resolved = await waitForCaptchaResolved();
703
+
704
+ if (resolved) {
705
+ captchaCount--; // 人工解决了,不算独立事件
706
+ } else if (captchaCount >= 3) {
707
+ // 累计3次验证码(均超时未解决),切换账户
708
+ await handleAccountSwitch(
709
+ `验证码累计 ${captchaCount} 次(超时未解决)`,
710
+ );
663
711
  captchaCount = 0;
664
712
  }
665
713
  }
package/src/lib/args.js CHANGED
@@ -187,6 +187,7 @@ function parseExploreArgs(args) {
187
187
  let exploreNoProduct = false;
188
188
  let exploreSwitchDelay = null;
189
189
  let exploreCommentDelay = null;
190
+ let captchaWaitMin = 3;
190
191
 
191
192
  const positional = [];
192
193
  const PRESETS = ["fast", "normal", "slow", "stealth"];
@@ -237,6 +238,8 @@ function parseExploreArgs(args) {
237
238
  exploreSwitchDelay = parseInt(args[++i], 10) || null;
238
239
  } else if (arg === "--comment-delay") {
239
240
  exploreCommentDelay = parseInt(args[++i], 10) || null;
241
+ } else if (arg === "--captcha-wait-min") {
242
+ captchaWaitMin = parseInt(args[++i], 10) || 3;
240
243
  } else {
241
244
  positional.push(arg);
242
245
  }
@@ -264,6 +267,7 @@ function parseExploreArgs(args) {
264
267
  explorePreset,
265
268
  exploreSwitchDelay,
266
269
  exploreCommentDelay,
270
+ captchaWaitMin,
267
271
  exploreInterval,
268
272
  exploreMaxComments,
269
273
  exploreMaxGuess,
@@ -813,6 +817,7 @@ function parseClashDaemonArgs(args) {
813
817
  let interval = 120;
814
818
  let benchmark = 10;
815
819
  let dryRun = false;
820
+ let noWhitelistGuard = false;
816
821
  let country = null;
817
822
  let apiPort = null;
818
823
  let secret = null;
@@ -829,6 +834,8 @@ function parseClashDaemonArgs(args) {
829
834
  country = args[++i] || null;
830
835
  } else if (arg === "--dry-run") {
831
836
  dryRun = true;
837
+ } else if (arg === "--no-whitelist-guard") {
838
+ noWhitelistGuard = true;
832
839
  } else if (arg === "--api-port") {
833
840
  apiPort = parseInt(args[++i]) || 0;
834
841
  } else if (arg === "--secret") {
@@ -847,6 +854,7 @@ function parseClashDaemonArgs(args) {
847
854
  benchmark,
848
855
  country,
849
856
  "dry-run": dryRun,
857
+ "no-whitelist-guard": noWhitelistGuard,
850
858
  "api-port": apiPort,
851
859
  secret,
852
860
  selection,
@@ -127,6 +127,7 @@ const HELP_TEXT = [
127
127
  " --preset <预设> 延迟预设 fast/normal/slow/stealth,默认 normal",
128
128
  " --switch-delay <毫秒> 任务切换延迟上限,覆盖预设值(实际等待随机 delay(switchMax, switchMax*3))",
129
129
  " --comment-delay <毫秒> 评论滚动延迟上限,覆盖预设值",
130
+ " --captcha-wait-min <分钟> 验证码等待人工解决上限(轮询检测,解决即继续),默认 3",
130
131
  ` --location <国家代码> 国家筛选,逗号分隔,默认 ${DEFAULT_TARGET_LOCATIONS_CSV}`,
131
132
  " --job-locations <国家> 任务国家筛选,逗号分隔(仅筛选服务端任务)",
132
133
  " --max-comments <数量> 每视频最大评论数,默认 10",
@@ -460,6 +461,7 @@ const HELP_TEXT = [
460
461
  " --country <国家代码> 只保留指定国家节点,默认不过滤",
461
462
  " 支持: US, GB, DE, FR, JP, KR, SG, HK, TW, RU 等",
462
463
  " --dry-run 只检测不切换(调试用)",
464
+ " --no-whitelist-guard 关闭白名单守卫(默认开启:当前节点不在候选集内时强制切回)",
463
465
  " --api-port <端口> 控制 API 端口(默认自动发现;显式指定验证失败时报错)",
464
466
  " --secret <secret> 控制 API 密钥(默认自动获取)",
465
467
  " --selection <策略> 多控制端选择策略: system-proxy / preference / first-alive",
@@ -469,6 +471,7 @@ const HELP_TEXT = [
469
471
  ' {"action":"switchNode","node":"节点名"} 一次性切换到指定节点(不干扰自检)',
470
472
  ' {"action":"setCountryFilter","country":"US"} 切换国家过滤',
471
473
  ' {"action":"setCountryFilter","country":null} 取消过滤',
474
+ ' {"action":"setWhitelistGuard","enabled":false} 关闭白名单守卫(enabled:true 开启)',
472
475
  ' {"action":"runBenchmark"} 手动刷新候选集(delay 初测)',
473
476
  ' {"action":"setInterval","seconds":60} 修改巡检间隔',
474
477
  " 示例: tt-help clash-daemon",
@@ -1,6 +1,6 @@
1
1
  export async function detectCaptcha(page) {
2
2
  return page.evaluate(() => {
3
- const container = document.querySelector('.captcha-verify-container');
3
+ const container = document.querySelector(".captcha-verify-container");
4
4
  if (!container) return null;
5
5
 
6
6
  const r = container.getBoundingClientRect();
@@ -26,15 +26,22 @@ export async function detectCaptchaWithWait(page, timeoutMs = 5000) {
26
26
  try {
27
27
  const handle = await page.waitForFunction(
28
28
  () => {
29
- const c = document.querySelector('.captcha-verify-container');
29
+ const c = document.querySelector(".captcha-verify-container");
30
30
  return !!(c && c.offsetParent !== null);
31
31
  },
32
32
  { timeout: timeoutMs },
33
33
  );
34
- const appeared = handle ? await handle.jsonValue().catch(() => false) : false;
34
+ const appeared = handle
35
+ ? await handle.jsonValue().catch(() => false)
36
+ : false;
35
37
  if (!appeared) return null;
36
38
  // 出现后用原函数拿完整信息(rect 等);异常按未出现处理
37
- return (await detectCaptcha(page).catch(() => null)) || { exists: true, visible: true };
39
+ return (
40
+ (await detectCaptcha(page).catch(() => null)) || {
41
+ exists: true,
42
+ visible: true,
43
+ }
44
+ );
38
45
  } catch {
39
46
  return null; // 超时或页面异常:未出现
40
47
  }
@@ -42,12 +49,12 @@ export async function detectCaptchaWithWait(page, timeoutMs = 5000) {
42
49
 
43
50
  export async function closeCaptcha(page) {
44
51
  return page.evaluate(() => {
45
- const closeBtn = document.getElementById('captcha_close_button');
46
- if (!closeBtn) return { success: false, reason: 'close button not found' };
52
+ const closeBtn = document.getElementById("captcha_close_button");
53
+ if (!closeBtn) return { success: false, reason: "close button not found" };
47
54
 
48
- closeBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
49
- closeBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
50
- closeBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
55
+ closeBtn.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
56
+ closeBtn.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
57
+ closeBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
51
58
 
52
59
  return { success: true };
53
60
  });
@@ -59,12 +66,13 @@ export async function handleCaptcha(page, options = {}) {
59
66
  const captcha = await detectCaptcha(page);
60
67
  if (!captcha) return { detected: false, closed: false };
61
68
 
62
- await new Promise(r => setTimeout(r, waitMs));
69
+ await new Promise((r) => setTimeout(r, waitMs));
63
70
 
64
71
  const result = await closeCaptcha(page);
65
- if (!result.success) return { detected: true, closed: false, reason: result.reason };
72
+ if (!result.success)
73
+ return { detected: true, closed: false, reason: result.reason };
66
74
 
67
- await new Promise(r => setTimeout(r, 1000));
75
+ await new Promise((r) => setTimeout(r, 1000));
68
76
 
69
77
  const stillThere = await detectCaptcha(page);
70
78
  return { detected: true, closed: !stillThere };
@@ -74,8 +82,8 @@ export async function getIncognitoPage(browser, url, options = {}) {
74
82
  const { waitMs = 3000 } = options;
75
83
  const context = await browser.newContext();
76
84
  const page = await context.newPage();
77
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
78
- await new Promise(r => setTimeout(r, waitMs));
85
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
86
+ await new Promise((r) => setTimeout(r, waitMs));
79
87
  return { page, context };
80
88
  }
81
89
 
@@ -85,13 +93,13 @@ export async function waitAndGetCaptcha(page, options = {}) {
85
93
  const captcha = await detectCaptcha(page);
86
94
  if (!captcha) return { detected: false, resolved: false, waited: 0 };
87
95
 
88
- if (log) log(' 检测到验证码,等待用户手动输入...');
96
+ if (log) log(" 检测到验证码,等待用户手动输入...");
89
97
 
90
98
  const startTime = Date.now();
91
99
  const deadline = startTime + waitMs;
92
100
 
93
101
  while (Date.now() < deadline) {
94
- await new Promise(r => setTimeout(r, pollInterval));
102
+ await new Promise((r) => setTimeout(r, pollInterval));
95
103
  const remaining = await detectCaptcha(page);
96
104
  if (!remaining) {
97
105
  const waited = Math.round((Date.now() - startTime) / 1000);
@@ -105,7 +113,7 @@ export async function waitAndGetCaptcha(page, options = {}) {
105
113
 
106
114
  // 超时后尝试关闭验证码弹窗
107
115
  await closeCaptcha(page);
108
- await new Promise(r => setTimeout(r, 1000));
116
+ await new Promise((r) => setTimeout(r, 1000));
109
117
 
110
118
  return { detected: true, resolved: false, waited };
111
119
  }
@@ -115,22 +123,30 @@ export async function safeClickComment(page, options = {}) {
115
123
 
116
124
  // 点击评论
117
125
  await page.evaluate(() => {
118
- const all = document.querySelectorAll('button');
126
+ const all = document.querySelectorAll("button");
119
127
  for (const el of all) {
120
- if (/^评论$/.test(el.textContent?.trim()) && el.offsetParent !== null && el.getBoundingClientRect().width > 0) {
128
+ if (
129
+ /^评论$/.test(el.textContent?.trim()) &&
130
+ el.offsetParent !== null &&
131
+ el.getBoundingClientRect().width > 0
132
+ ) {
121
133
  el.click();
122
134
  break;
123
135
  }
124
136
  }
125
137
  });
126
138
 
127
- await new Promise(r => setTimeout(r, waitMs));
139
+ await new Promise((r) => setTimeout(r, waitMs));
128
140
 
129
141
  // 检测并关闭验证码
130
142
  const captcha = await detectCaptcha(page);
131
143
  if (captcha) {
132
144
  const result = await handleCaptcha(page);
133
- return { clicked: true, captchaDetected: true, captchaClosed: result.closed };
145
+ return {
146
+ clicked: true,
147
+ captchaDetected: true,
148
+ captchaClosed: result.closed,
149
+ };
134
150
  }
135
151
 
136
152
  return { clicked: true, captchaDetected: false, captchaClosed: false };