@skrillex1224/playwright-toolkit 2.1.58 → 2.1.60

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/README.md CHANGED
@@ -82,24 +82,23 @@ await Actor.exit();
82
82
 
83
83
  ### API 一览
84
84
 
85
- | 模块 | 方法 | 说明 |
86
- | ----------- | ---------------------------------------------------- | -------------------------------------- |
87
- | `Launch` | `getAdvancedLaunchOptions()` | 增强版启动参数 |
88
- | `Launch` | `getLaunchOptions()` | 基础启动参数 |
89
- | `Launch` | `getFingerprintGeneratorOptions()` | 指纹生成器选项 |
90
- | `AntiCheat` | `applyPage(page, options?)` | 应用时区/语言/权限/视口 |
91
- | `AntiCheat` | `applyContext(context, options?)` | 仅应用 Context 设置 |
92
- | `AntiCheat` | `syncViewportWithScreen(page)` | 同步视口与屏幕 |
93
- | `AntiCheat` | `getTlsFingerprintOptions(userAgent?)` | got-scraping TLS 指纹 |
94
- | `Humanize` | `initializeCursor(page)` | 初始化 Cursor (必须先调用) |
95
- | `Humanize` | `jitterMs(base, jitterPercent?)` | 生成带抖动的毫秒数 (同步,返回 number) |
96
- | `Humanize` | `humanType(page, selector, text, options?)` | 人类化输入 (baseDelay=180ms ±40%) |
97
- | `Humanize` | `humanClick(page, selector, options?)` | 人类化点击 (reactionDelay=250ms ±40%) |
98
- | `Humanize` | `warmUpBrowsing(page, baseDuration?)` | 页面预热 (3500ms ±40%) |
99
- | `Humanize` | `naturalScroll(page, direction?, distance?, steps?)` | 自然滚动 (带惯性+抖动) |
100
- | `Humanize` | `simulateGaze(page, baseDurationMs?)` | 模拟注视 (2500ms ±40%) |
101
- | `Humanize` | `randomSleep(baseMs, jitterPercent?)` | 随机延迟 (±30% 抖动) |
102
- | `Captcha` | `useCaptchaMonitor(page, options)` | 验证码监控 |
85
+ | 模块 | 方法 | 说明 |
86
+ | ----------- | ------------------------------------------- | -------------------------------------- |
87
+ | `Launch` | `getAdvancedLaunchOptions()` | 增强版启动参数 |
88
+ | `Launch` | `getLaunchOptions()` | 基础启动参数 |
89
+ | `Launch` | `getFingerprintGeneratorOptions()` | 指纹生成器选项 |
90
+ | `AntiCheat` | `applyPage(page, options?)` | 应用时区/语言/权限/视口 |
91
+ | `AntiCheat` | `applyContext(context, options?)` | 仅应用 Context 设置 |
92
+ | `AntiCheat` | `syncViewportWithScreen(page)` | 同步视口与屏幕 |
93
+ | `AntiCheat` | `getTlsFingerprintOptions(userAgent?)` | got-scraping TLS 指纹 |
94
+ | `Humanize` | `initializeCursor(page)` | 初始化 Cursor (必须先调用) |
95
+ | `Humanize` | `jitterMs(base, jitterPercent?)` | 生成带抖动的毫秒数 (同步,返回 number) |
96
+ | `Humanize` | `humanType(page, selector, text, options?)` | 人类化输入 (baseDelay=180ms ±40%) |
97
+ | `Humanize` | `humanClick(page, selector, options?)` | 人类化点击 (reactionDelay=250ms ±40%) |
98
+ | `Humanize` | `warmUpBrowsing(page, baseDuration?)` | 页面预热 (3500ms ±40%) |
99
+ | `Humanize` | `simulateGaze(page, baseDurationMs?)` | 模拟注视 (2500ms ±40%) |
100
+ | `Humanize` | `randomSleep(baseMs, jitterPercent?)` | 随机延迟30% 抖动) |
101
+ | `Captcha` | `useCaptchaMonitor(page, options)` | 验证码监控 |
103
102
 
104
103
  ---
105
104
 
package/dist/index.cjs CHANGED
@@ -534,6 +534,59 @@ var import_delay2 = __toESM(require("delay"), 1);
534
534
  var import_ghost_cursor_playwright = require("ghost-cursor-playwright");
535
535
  var logger5 = createInternalLogger("Humanize");
536
536
  var $CursorWeakMap = /* @__PURE__ */ new WeakMap();
537
+ var VISIBILITY_CODE = {
538
+ VISIBLE: "VISIBLE",
539
+ OUT_OF_VIEWPORT: "OUT_OF_VIEWPORT",
540
+ OBSTRUCTED: "OBSTRUCTED",
541
+ ZERO_DIMENSIONS: "ZERO_DIMENSIONS",
542
+ HIDDEN: "HIDDEN",
543
+ DETACHED: "DETACHED"
544
+ };
545
+ var VISIBILITY_REASON = {
546
+ OUT_OF_VIEWPORT: "\u4E0D\u5728\u89C6\u53E3\u5185",
547
+ OBSTRUCTED: "\u88AB\u906E\u6321",
548
+ ZERO_DIMENSIONS: "\u5C3A\u5BF8\u4E3A\u96F6",
549
+ HIDDEN: "\u5143\u7D20\u4E0D\u53EF\u89C1",
550
+ DETACHED: "\u5143\u7D20\u5DF2\u88AB\u79FB\u9664"
551
+ };
552
+ var SCROLL_DIRECTION = {
553
+ UP: "up",
554
+ DOWN: "down",
555
+ UNKNOWN: "unknown"
556
+ };
557
+ var DEFAULT_SCROLL_OPTIONS = {
558
+ maxSteps: 20,
559
+ minStep: 150,
560
+ maxStep: 400,
561
+ maxDurationMs: 3500
562
+ };
563
+ var SAME_POSITION_THRESHOLD_PX = 2;
564
+ var SAME_POSITION_LIMIT = 3;
565
+ var SAFE_MOUSE_JITTER_PX = 100;
566
+ var isElementHandleLike = (value) => value && typeof value.evaluate === "function" && typeof value.boundingBox === "function";
567
+ var isLocatorLike = (value) => value && typeof value.elementHandle === "function";
568
+ async function resolveElementHandle(page, target) {
569
+ if (!target) return { element: null, owned: false };
570
+ if (typeof target === "string") {
571
+ const element = await page.$(target);
572
+ return { element, owned: true };
573
+ }
574
+ if (isLocatorLike(target)) {
575
+ const element = await target.elementHandle();
576
+ return { element, owned: true };
577
+ }
578
+ if (isElementHandleLike(target)) {
579
+ return { element: target, owned: false };
580
+ }
581
+ return { element: null, owned: false };
582
+ }
583
+ async function disposeElementHandle(element, owned) {
584
+ if (!element || !owned || typeof element.dispose !== "function") return;
585
+ try {
586
+ await element.dispose();
587
+ } catch {
588
+ }
589
+ }
537
590
  function $GetCursor(page) {
538
591
  const cursor = $CursorWeakMap.get(page);
539
592
  if (!cursor) {
@@ -614,99 +667,353 @@ var Humanize = {
614
667
  * 返回 restore 方法,用于将滚动容器恢复到原位置
615
668
  *
616
669
  * @param {import('playwright').Page} page
617
- * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
670
+ * @param {string|import('playwright').ElementHandle|import('playwright').Locator} target - CSS 选择器、元素句柄或 Locator
618
671
  * @param {Object} [options]
619
- * @param {number} [options.maxSteps=25] - 最大滚动步数
620
- * @param {number} [options.minStep=80] - 单次滚动最小步长
621
- * @param {number} [options.maxStep=220] - 单次滚动最大步长
672
+ * @param {number} [options.maxSteps=20] - 最大滚动步数
673
+ * @param {number} [options.minStep=150] - 单次滚动最小步长
674
+ * @param {number} [options.maxStep=400] - 单次滚动最大步长
675
+ * @param {number} [options.maxDurationMs=3500] - 最大滚动耗时上限
622
676
  */
623
677
  async humanScroll(page, target, options = {}) {
624
- const { maxSteps = 30, minStep = 150, maxStep = 400 } = options;
625
- const targetDesc = typeof target === "string" ? target : "ElementHandle";
626
- logger5.debug(`humanScroll | \u76EE\u6807=${targetDesc}`);
627
- let element;
628
- if (typeof target === "string") {
629
- element = await page.$(target);
630
- if (!element) {
631
- logger5.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
632
- return { element: null, didScroll: false };
633
- }
634
- } else {
635
- element = target;
678
+ const {
679
+ maxSteps = DEFAULT_SCROLL_OPTIONS.maxSteps,
680
+ minStep = DEFAULT_SCROLL_OPTIONS.minStep,
681
+ maxStep = DEFAULT_SCROLL_OPTIONS.maxStep,
682
+ maxDurationMs = DEFAULT_SCROLL_OPTIONS.maxDurationMs
683
+ } = options;
684
+ const targetDesc = typeof target === "string" ? target : isLocatorLike(target) ? "Locator" : "ElementHandle";
685
+ logger5.info(`humanScroll | \u5F00\u59CB\u6EDA\u52A8\u76EE\u6807: ${targetDesc}`);
686
+ const { element } = await resolveElementHandle(page, target);
687
+ if (!element) {
688
+ logger5.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${targetDesc}`);
689
+ return { element: null, didScroll: false, restore: null };
636
690
  }
637
691
  const cursor = $GetCursor(page);
638
692
  let didScroll = false;
693
+ let lastRect = null;
694
+ let samePositionCount = 0;
695
+ const startAt = Date.now();
696
+ const visibilityPayload = {
697
+ codes: VISIBILITY_CODE,
698
+ reasons: VISIBILITY_REASON,
699
+ directions: SCROLL_DIRECTION
700
+ };
701
+ const scrollDeltas = /* @__PURE__ */ new Map();
702
+ const scrollTargetRects = /* @__PURE__ */ new Map();
703
+ let windowIndex = null;
704
+ const moveMouseToRect = async (rect) => {
705
+ if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top)) return false;
706
+ const width = Number.isFinite(rect.width) ? rect.width : Math.max(0, (rect.right ?? rect.left) - rect.left);
707
+ const height = Number.isFinite(rect.height) ? rect.height : Math.max(0, (rect.bottom ?? rect.top) - rect.top);
708
+ const viewSize = page.viewportSize();
709
+ const viewW = viewSize?.width ?? null;
710
+ const viewH = viewSize?.height ?? null;
711
+ if (viewW && viewH) {
712
+ const rectRight = rect.right ?? rect.left + width;
713
+ const rectBottom = rect.bottom ?? rect.top + height;
714
+ if (rectBottom < 0 || rect.top > viewH || rectRight < 0 || rect.left > viewW) {
715
+ return false;
716
+ }
717
+ }
718
+ const rawX = rect.left + width / 2;
719
+ const rawY = rect.top + height / 2;
720
+ if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) return false;
721
+ const padding = 6;
722
+ let x = rawX;
723
+ let y = rawY;
724
+ if (viewW && viewH) {
725
+ const maxX = Math.max(padding, viewW - padding);
726
+ const maxY = Math.max(padding, viewH - padding);
727
+ x = Math.min(Math.max(rawX, padding), maxX);
728
+ y = Math.min(Math.max(rawY, padding), maxY);
729
+ }
730
+ x += (Math.random() - 0.5) * 4;
731
+ y += (Math.random() - 0.5) * 4;
732
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
733
+ await page.mouse.move(x, y, { steps: 6 }).catch(() => {
734
+ });
735
+ return true;
736
+ };
737
+ const wheelByChunks = async (delta, rect, isWindowTarget) => {
738
+ let remaining = delta;
739
+ const stepBase = Math.min(maxStep, 400);
740
+ while (Math.abs(remaining) > 1) {
741
+ const step = Math.abs(remaining) > stepBase ? Math.sign(remaining) * stepBase : remaining;
742
+ if (!isWindowTarget) {
743
+ await moveMouseToRect(rect);
744
+ }
745
+ await page.mouse.wheel(0, step);
746
+ remaining -= step;
747
+ await (0, import_delay2.default)(this.jitterMs(60, 0.35));
748
+ }
749
+ };
750
+ const restore = async () => {
751
+ if (scrollDeltas.size === 0) return;
752
+ try {
753
+ for (const [index, delta] of scrollDeltas.entries()) {
754
+ if (!delta) continue;
755
+ const rect = scrollTargetRects.get(index);
756
+ const isWindowTarget = windowIndex !== null && index === windowIndex;
757
+ await wheelByChunks(-delta, rect, isWindowTarget);
758
+ }
759
+ } catch (err) {
760
+ logger5.warn(`humanScroll | restore failed: ${err?.message || err}`);
761
+ }
762
+ };
639
763
  const checkVisibility = async () => {
640
- return await element.evaluate((el) => {
764
+ return await element.evaluate((el, payload) => {
765
+ const overflowRe = /(auto|scroll|overlay)/i;
766
+ const isScrollable = (node) => {
767
+ if (!node || node === document.body || node === document.documentElement) return false;
768
+ const style2 = window.getComputedStyle(node);
769
+ const overflowY = style2.overflowY;
770
+ return overflowRe.test(overflowY) && node.scrollHeight > node.clientHeight + 1;
771
+ };
772
+ const getScrollableAncestors = (node) => {
773
+ const list = [];
774
+ let current = node?.parentElement;
775
+ while (current && current !== document.body && current !== document.documentElement) {
776
+ if (isScrollable(current)) {
777
+ list.push(current);
778
+ }
779
+ current = current.parentElement;
780
+ }
781
+ return list;
782
+ };
783
+ if (!el || !el.isConnected) {
784
+ return { code: payload.codes.DETACHED, reason: payload.reasons.DETACHED };
785
+ }
786
+ const style = window.getComputedStyle(el);
787
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) {
788
+ return { code: payload.codes.HIDDEN, reason: payload.reasons.HIDDEN };
789
+ }
641
790
  const rect = el.getBoundingClientRect();
642
791
  if (!rect || rect.width === 0 || rect.height === 0) {
643
- return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6" };
792
+ return { code: payload.codes.ZERO_DIMENSIONS, reason: payload.reasons.ZERO_DIMENSIONS };
644
793
  }
794
+ const hasFixedAncestor = (() => {
795
+ let node = el;
796
+ while (node && node !== document.body && node !== document.documentElement) {
797
+ const position = window.getComputedStyle(node).position;
798
+ if (position === "fixed") return true;
799
+ node = node.parentElement;
800
+ }
801
+ return false;
802
+ })();
803
+ const ancestors = getScrollableAncestors(el);
804
+ const targets = ancestors.map((node) => ({
805
+ isWindow: false,
806
+ rect: node.getBoundingClientRect(),
807
+ canScroll: node.scrollHeight > node.clientHeight + 1
808
+ }));
809
+ const windowTarget = {
810
+ isWindow: true,
811
+ rect: { top: 0, left: 0, right: window.innerWidth, bottom: window.innerHeight, width: window.innerWidth, height: window.innerHeight },
812
+ canScroll: document.documentElement.scrollHeight > window.innerHeight
813
+ };
814
+ targets.push(windowTarget);
815
+ const windowIndex2 = targets.length - 1;
645
816
  const cx = rect.left + rect.width / 2;
646
817
  const cy = rect.top + rect.height / 2;
647
- const viewH = window.innerHeight;
648
- const viewW = window.innerWidth;
649
- if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
650
- const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
651
- return { code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy, viewH };
818
+ const promoteToVisibleTarget = (index) => {
819
+ let idx = index;
820
+ while (idx < targets.length - 1) {
821
+ const current = targets[idx];
822
+ const parent = targets[idx + 1];
823
+ const c = current.rect;
824
+ const p = parent.rect;
825
+ const outOfParent = c.bottom < p.top || c.top > p.bottom || c.right < p.left || c.left > p.right;
826
+ if (outOfParent) {
827
+ idx += 1;
828
+ continue;
829
+ }
830
+ break;
831
+ }
832
+ return idx;
833
+ };
834
+ let outIndex = -1;
835
+ let direction = payload.directions.UNKNOWN;
836
+ for (let i = 0; i < targets.length; i += 1) {
837
+ const bounds = targets[i].rect;
838
+ if (cy < bounds.top) {
839
+ outIndex = i;
840
+ direction = payload.directions.UP;
841
+ break;
842
+ }
843
+ if (cy > bounds.bottom) {
844
+ outIndex = i;
845
+ direction = payload.directions.DOWN;
846
+ break;
847
+ }
848
+ if (cx < bounds.left || cx > bounds.right) {
849
+ outIndex = i;
850
+ direction = payload.directions.UNKNOWN;
851
+ break;
852
+ }
853
+ }
854
+ if (outIndex !== -1) {
855
+ let scrollTargetIndex = promoteToVisibleTarget(outIndex);
856
+ let target2 = targets[scrollTargetIndex];
857
+ if (!target2.isWindow) {
858
+ const targetRect = target2.rect;
859
+ const outOfWindow = targetRect.bottom < 0 || targetRect.top > window.innerHeight || targetRect.right < 0 || targetRect.left > window.innerWidth;
860
+ if (outOfWindow) {
861
+ scrollTargetIndex = windowIndex2;
862
+ target2 = windowTarget;
863
+ }
864
+ }
865
+ return {
866
+ code: payload.codes.OUT_OF_VIEWPORT,
867
+ reason: payload.reasons.OUT_OF_VIEWPORT,
868
+ direction,
869
+ rect,
870
+ viewH: target2.rect.height,
871
+ viewW: target2.rect.width,
872
+ cy,
873
+ cx,
874
+ scrollTargetIndex,
875
+ isWindow: target2.isWindow,
876
+ canScroll: target2.canScroll,
877
+ isFixed: hasFixedAncestor,
878
+ targetRect: target2.rect,
879
+ windowIndex: windowIndex2
880
+ };
652
881
  }
653
882
  const pointElement = document.elementFromPoint(cx, cy);
654
883
  if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
884
+ let fallbackIndex = promoteToVisibleTarget(targets.length > 1 ? 0 : targets.length - 1);
885
+ let target2 = targets[fallbackIndex];
886
+ if (!target2.isWindow) {
887
+ const targetRect = target2.rect;
888
+ const outOfWindow = targetRect.bottom < 0 || targetRect.top > window.innerHeight || targetRect.right < 0 || targetRect.left > window.innerWidth;
889
+ if (outOfWindow) {
890
+ fallbackIndex = windowIndex2;
891
+ target2 = windowTarget;
892
+ }
893
+ }
655
894
  return {
656
- code: "OBSTRUCTED",
657
- reason: "\u88AB\u906E\u6321",
895
+ code: payload.codes.OBSTRUCTED,
896
+ reason: payload.reasons.OBSTRUCTED,
658
897
  obstruction: {
659
898
  tag: pointElement.tagName,
660
899
  id: pointElement.id,
661
900
  className: pointElement.className
662
901
  },
902
+ rect,
903
+ viewH: target2.rect.height,
904
+ viewW: target2.rect.width,
663
905
  cy,
664
- // Return Center Y for smart direction calculation
665
- viewH
906
+ cx,
907
+ scrollTargetIndex: fallbackIndex,
908
+ isWindow: target2.isWindow,
909
+ canScroll: target2.canScroll,
910
+ isFixed: hasFixedAncestor,
911
+ targetRect: target2.rect,
912
+ windowIndex: windowIndex2
666
913
  };
667
914
  }
668
- return { code: "VISIBLE" };
669
- });
915
+ return {
916
+ code: payload.codes.VISIBLE,
917
+ rect,
918
+ viewH: windowTarget.rect.height,
919
+ viewW: windowTarget.rect.width,
920
+ cy,
921
+ cx,
922
+ scrollTargetIndex: targets.length - 1,
923
+ isWindow: true,
924
+ canScroll: windowTarget.canScroll,
925
+ isFixed: hasFixedAncestor,
926
+ targetRect: windowTarget.rect,
927
+ windowIndex: windowIndex2
928
+ };
929
+ }, visibilityPayload);
670
930
  };
671
931
  try {
672
932
  for (let i = 0; i < maxSteps; i++) {
933
+ if (Date.now() - startAt > maxDurationMs) {
934
+ logger5.warn(`humanScroll | \u26A0\uFE0F \u8D85\u8FC7\u6700\u5927\u8017\u65F6 ${maxDurationMs}ms\uFF0C\u63D0\u524D\u7ED3\u675F`);
935
+ return { element, didScroll, restore };
936
+ }
673
937
  const status = await checkVisibility();
674
- if (status.code === "VISIBLE") {
675
- logger5.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
676
- return { element, didScroll };
938
+ if (windowIndex === null && typeof status.windowIndex === "number") {
939
+ windowIndex = status.windowIndex;
940
+ }
941
+ if (typeof status.scrollTargetIndex === "number" && status.targetRect) {
942
+ scrollTargetRects.set(status.scrollTargetIndex, status.targetRect);
943
+ }
944
+ if (status.code === VISIBILITY_CODE.VISIBLE) {
945
+ logger5.info("humanScroll | \u5143\u7D20\u5DF2\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
946
+ return { element, didScroll, restore };
947
+ }
948
+ if (status.code === VISIBILITY_CODE.DETACHED || status.code === VISIBILITY_CODE.HIDDEN || status.code === VISIBILITY_CODE.ZERO_DIMENSIONS) {
949
+ logger5.warn(`humanScroll | ${status.reason || "\u5143\u7D20\u4E0D\u53EF\u7528"}`);
950
+ return { element, didScroll, restore };
951
+ }
952
+ const directionStr = status.direction ? `(${status.direction})` : "";
953
+ logger5.info(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${directionStr}`);
954
+ const currentRect = status.rect;
955
+ if (i > 0 && lastRect && currentRect) {
956
+ const dy = Math.abs(currentRect.top - lastRect.top);
957
+ const dx = Math.abs(currentRect.left - lastRect.left);
958
+ if (dy < SAME_POSITION_THRESHOLD_PX && dx < SAME_POSITION_THRESHOLD_PX) {
959
+ samePositionCount++;
960
+ logger5.info(`humanScroll | \u26A0\uFE0F \u8B66\u544A: \u6EDA\u52A8\u540E\u4F4D\u7F6E\u672A\u53D8 (${samePositionCount}/${SAME_POSITION_LIMIT}). dy=${dy}`);
961
+ if (samePositionCount >= SAME_POSITION_LIMIT) {
962
+ logger5.warn("humanScroll | \u26A0\uFE0F \u68C0\u6D4B\u5230\u65E0\u6548\u6EDA\u52A8 (\u5143\u7D20\u53EF\u80FD\u662F Fixed \u6216\u4F4D\u4E8E\u4E0D\u53EF\u6EDA\u52A8\u7684\u5BB9\u5668)\uFF0C\u5F3A\u5236\u505C\u6B62\uFF0C\u5C1D\u8BD5\u76F4\u63A5\u4EA4\u4E92");
963
+ return { element, didScroll, restore };
964
+ }
965
+ } else {
966
+ samePositionCount = 0;
967
+ }
968
+ }
969
+ if (currentRect) lastRect = currentRect;
970
+ if (status.code === VISIBILITY_CODE.OBSTRUCTED && status.obstruction) {
971
+ logger5.info(`humanScroll | \u88AB\u906E\u6321: <${status.obstruction.tag}.${status.obstruction.className}>`);
677
972
  }
678
- logger5.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${status.direction ? `(${status.direction})` : ""}`);
679
- if (status.code === "OBSTRUCTED" && status.obstruction) {
680
- logger5.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
973
+ if (status.code === VISIBILITY_CODE.OBSTRUCTED && status.isFixed) {
974
+ logger5.warn("humanScroll | \u5143\u7D20\u4E3A Fixed \u4E14\u88AB\u906E\u6321\uFF0C\u505C\u6B62\u6EDA\u52A8\u5C1D\u8BD5");
975
+ return { element, didScroll, restore };
976
+ }
977
+ if (!status.canScroll) {
978
+ logger5.warn("humanScroll | \u5F53\u524D\u5BB9\u5668\u4E0D\u53EF\u6EDA\u52A8\uFF0C\u505C\u6B62\u6EDA\u52A8\u5C1D\u8BD5");
979
+ return { element, didScroll, restore };
681
980
  }
682
981
  let deltaY = 0;
683
- if (status.code === "OUT_OF_VIEWPORT") {
684
- if (status.direction === "down") {
982
+ if (status.code === VISIBILITY_CODE.OUT_OF_VIEWPORT) {
983
+ if (status.direction === SCROLL_DIRECTION.DOWN) {
685
984
  deltaY = minStep + Math.random() * (maxStep - minStep);
686
- } else if (status.direction === "up") {
985
+ } else if (status.direction === SCROLL_DIRECTION.UP) {
687
986
  deltaY = -(minStep + Math.random() * (maxStep - minStep));
688
987
  } else {
689
988
  deltaY = 100;
690
989
  }
691
- } else if (status.code === "OBSTRUCTED") {
990
+ } else if (status.code === VISIBILITY_CODE.OBSTRUCTED) {
692
991
  const isBottomHalf = status.cy > status.viewH / 2;
693
- const direction = isBottomHalf ? 1 : -1;
694
- deltaY = direction * (minStep + Math.random() * 50);
992
+ const dir = isBottomHalf ? 1 : -1;
993
+ deltaY = dir * (minStep + Math.random() * 50);
695
994
  }
696
995
  if (i === 0 || Math.random() < 0.2) {
697
996
  const viewSize = page.viewportSize();
698
997
  if (viewSize) {
699
- const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 100;
700
- const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 100;
701
- await cursor.actions.move({ x: safeX, y: safeY });
998
+ const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * SAFE_MOUSE_JITTER_PX;
999
+ const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * SAFE_MOUSE_JITTER_PX;
1000
+ await cursor.actions.move({ x: safeX, y: safeY }).catch(() => {
1001
+ });
702
1002
  }
703
1003
  }
1004
+ if (!status.isWindow) {
1005
+ await moveMouseToRect(status.targetRect);
1006
+ }
704
1007
  await page.mouse.wheel(0, deltaY);
1008
+ if (typeof status.scrollTargetIndex === "number") {
1009
+ const prev = scrollDeltas.get(status.scrollTargetIndex) || 0;
1010
+ scrollDeltas.set(status.scrollTargetIndex, prev + deltaY);
1011
+ }
705
1012
  didScroll = true;
706
- await (0, import_delay2.default)(this.jitterMs(100 + Math.random() * 150, 0.2));
1013
+ await (0, import_delay2.default)(this.jitterMs(150 + Math.random() * 100, 0.2));
707
1014
  }
708
- logger5.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
709
- return { element, didScroll };
1015
+ logger5.warn(`humanScroll | \u26A0\uFE0F \u8FBE\u5230\u6700\u5927\u6B65\u6570 (${maxSteps}) \u4ECD\u65E0\u6CD5\u5B8C\u5168\u53EF\u89C1\uFF0C\u653E\u5F03\u6EDA\u52A8`);
1016
+ return { element, didScroll, restore };
710
1017
  } catch (error) {
711
1018
  logger5.fail("humanScroll", error);
712
1019
  throw error;
@@ -716,7 +1023,7 @@ var Humanize = {
716
1023
  * 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
717
1024
  *
718
1025
  * @param {import('playwright').Page} page
719
- * @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
1026
+ * @param {string|import('playwright').ElementHandle|import('playwright').Locator} [target] - CSS 选择器、元素句柄或 Locator。如果为空,则点击当前鼠标位置
720
1027
  * @param {Object} [options]
721
1028
  * @param {number} [options.reactionDelay=250] - 反应延迟基础值 (ms),实际 ±30% 抖动
722
1029
  * @param {boolean} [options.throwOnMissing=true] - 元素不存在时是否抛出错误
@@ -725,8 +1032,10 @@ var Humanize = {
725
1032
  async humanClick(page, target, options = {}) {
726
1033
  const cursor = $GetCursor(page);
727
1034
  const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
728
- const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
1035
+ const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : isLocatorLike(target) ? "Locator" : "ElementHandle";
729
1036
  logger5.start("humanClick", `target=${targetDesc}`);
1037
+ let element = null;
1038
+ let owned = false;
730
1039
  const restoreOnce = async () => {
731
1040
  if (restoreOnce.restored) return;
732
1041
  restoreOnce.restored = true;
@@ -745,18 +1054,15 @@ var Humanize = {
745
1054
  logger5.success("humanClick", "Clicked current position");
746
1055
  return true;
747
1056
  }
748
- let element;
749
- if (typeof target === "string") {
750
- element = await page.$(target);
751
- if (!element) {
752
- if (throwOnMissing) {
753
- throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${target}`);
754
- }
755
- logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
756
- return false;
1057
+ const resolved = await resolveElementHandle(page, target);
1058
+ element = resolved.element;
1059
+ owned = resolved.owned;
1060
+ if (!element) {
1061
+ if (throwOnMissing) {
1062
+ throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${targetDesc}`);
757
1063
  }
758
- } else {
759
- element = target;
1064
+ logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
1065
+ return false;
760
1066
  }
761
1067
  if (scrollIfNeeded) {
762
1068
  const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
@@ -765,6 +1071,7 @@ var Humanize = {
765
1071
  const box = await element.boundingBox();
766
1072
  if (!box) {
767
1073
  await restoreOnce();
1074
+ await disposeElementHandle(element, owned);
768
1075
  if (throwOnMissing) {
769
1076
  throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
770
1077
  }
@@ -777,10 +1084,12 @@ var Humanize = {
777
1084
  await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
778
1085
  await cursor.actions.click();
779
1086
  await restoreOnce();
1087
+ await disposeElementHandle(element, owned);
780
1088
  logger5.success("humanClick");
781
1089
  return true;
782
1090
  } catch (error) {
783
1091
  await restoreOnce();
1092
+ await disposeElementHandle(element, owned);
784
1093
  logger5.fail("humanClick", error);
785
1094
  throw error;
786
1095
  }
@@ -917,34 +1226,6 @@ var Humanize = {
917
1226
  logger5.fail("warmUpBrowsing", error);
918
1227
  throw error;
919
1228
  }
920
- },
921
- /**
922
- * 自然滚动 - 带惯性、减速效果和随机抖动
923
- * @param {import('playwright').Page} page
924
- * @param {'up' | 'down'} [direction='down'] - 滚动方向
925
- * @param {number} [distance=300] - 总滚动距离基础值 (px),±15% 抖动
926
- * @param {number} [baseSteps=5] - 分几步完成基础值,±1 随机
927
- */
928
- async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
929
- const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
930
- const actualDistance = this.jitterMs(distance, 0.15);
931
- logger5.start("naturalScroll", `dir=${direction}, dist=${actualDistance}, steps=${steps}`);
932
- const sign = direction === "down" ? 1 : -1;
933
- const stepDistance = actualDistance / steps;
934
- try {
935
- for (let i = 0; i < steps; i++) {
936
- const factor = 1 - i / steps * 0.5;
937
- const jitter = 0.9 + Math.random() * 0.2;
938
- const scrollAmount = stepDistance * factor * sign * jitter;
939
- await page.mouse.wheel(0, scrollAmount);
940
- const baseDelay = 60 + i * 25;
941
- await (0, import_delay2.default)(this.jitterMs(baseDelay, 0.3));
942
- }
943
- logger5.success("naturalScroll");
944
- } catch (error) {
945
- logger5.fail("naturalScroll", error);
946
- throw error;
947
- }
948
1229
  }
949
1230
  };
950
1231