@skrillex1224/playwright-toolkit 2.1.59 → 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/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,79 +667,287 @@ var Humanize = {
614
667
  * 返回 restore 方法,用于将滚动容器恢复到原位置
615
668
  *
616
669
  * @param {import('playwright').Page} page
617
- * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
618
- * @param {Object} [options]
619
- * @param {number} [options.maxSteps=25] - 最大滚动步数
620
- * @param {number} [options.minStep=80] - 单次滚动最小步长
621
- * @param {number} [options.maxStep=220] - 单次滚动最大步长
622
- */
623
- /**
624
- * 渐进式滚动到元素可见(仅处理 Y 轴滚动)
625
- * 返回 restore 方法,用于将滚动容器恢复到原位置
626
- *
627
- * @param {import('playwright').Page} page
628
- * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
670
+ * @param {string|import('playwright').ElementHandle|import('playwright').Locator} target - CSS 选择器、元素句柄或 Locator
629
671
  * @param {Object} [options]
630
- * @param {number} [options.maxSteps=25] - 最大滚动步数
631
- * @param {number} [options.minStep=80] - 单次滚动最小步长
632
- * @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] - 最大滚动耗时上限
633
676
  */
634
677
  async humanScroll(page, target, options = {}) {
635
- const { maxSteps = 30, minStep = 150, maxStep = 400 } = options;
636
- const targetDesc = typeof target === "string" ? target : "ElementHandle";
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";
637
685
  logger5.info(`humanScroll | \u5F00\u59CB\u6EDA\u52A8\u76EE\u6807: ${targetDesc}`);
638
- let element;
639
- if (typeof target === "string") {
640
- element = await page.$(target);
641
- if (!element) {
642
- logger5.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
643
- return { element: null, didScroll: false };
644
- }
645
- } else {
646
- element = target;
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 };
647
690
  }
648
691
  const cursor = $GetCursor(page);
649
692
  let didScroll = false;
650
693
  let lastRect = null;
651
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
+ };
652
763
  const checkVisibility = async () => {
653
- 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
+ }
654
790
  const rect = el.getBoundingClientRect();
655
791
  if (!rect || rect.width === 0 || rect.height === 0) {
656
- return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6" };
792
+ return { code: payload.codes.ZERO_DIMENSIONS, reason: payload.reasons.ZERO_DIMENSIONS };
657
793
  }
658
- const viewH = window.innerHeight;
659
- const viewW = window.innerWidth;
660
- const resultBase = { rect, viewH, viewW };
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;
661
816
  const cx = rect.left + rect.width / 2;
662
817
  const cy = rect.top + rect.height / 2;
663
- if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
664
- const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
665
- return { ...resultBase, code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy };
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
+ };
666
881
  }
667
882
  const pointElement = document.elementFromPoint(cx, cy);
668
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
+ }
669
894
  return {
670
- ...resultBase,
671
- code: "OBSTRUCTED",
672
- reason: "\u88AB\u906E\u6321",
895
+ code: payload.codes.OBSTRUCTED,
896
+ reason: payload.reasons.OBSTRUCTED,
673
897
  obstruction: {
674
898
  tag: pointElement.tagName,
675
899
  id: pointElement.id,
676
900
  className: pointElement.className
677
901
  },
678
- cy
902
+ rect,
903
+ viewH: target2.rect.height,
904
+ viewW: target2.rect.width,
905
+ cy,
906
+ cx,
907
+ scrollTargetIndex: fallbackIndex,
908
+ isWindow: target2.isWindow,
909
+ canScroll: target2.canScroll,
910
+ isFixed: hasFixedAncestor,
911
+ targetRect: target2.rect,
912
+ windowIndex: windowIndex2
679
913
  };
680
914
  }
681
- return { ...resultBase, code: "VISIBLE" };
682
- });
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);
683
930
  };
684
931
  try {
685
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
+ }
686
937
  const status = await checkVisibility();
687
- if (status.code === "VISIBLE") {
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) {
688
945
  logger5.info("humanScroll | \u5143\u7D20\u5DF2\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
689
- return { element, didScroll };
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 };
690
951
  }
691
952
  const directionStr = status.direction ? `(${status.direction})` : "";
692
953
  logger5.info(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${directionStr}`);
@@ -694,31 +955,39 @@ var Humanize = {
694
955
  if (i > 0 && lastRect && currentRect) {
695
956
  const dy = Math.abs(currentRect.top - lastRect.top);
696
957
  const dx = Math.abs(currentRect.left - lastRect.left);
697
- if (dy < 2 && dx < 2) {
958
+ if (dy < SAME_POSITION_THRESHOLD_PX && dx < SAME_POSITION_THRESHOLD_PX) {
698
959
  samePositionCount++;
699
- logger5.info(`humanScroll | \u26A0\uFE0F \u8B66\u544A: \u6EDA\u52A8\u540E\u4F4D\u7F6E\u672A\u53D8 (${samePositionCount}/3). dy=${dy}`);
700
- if (samePositionCount >= 3) {
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) {
701
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");
702
- return { element, didScroll };
963
+ return { element, didScroll, restore };
703
964
  }
704
965
  } else {
705
966
  samePositionCount = 0;
706
967
  }
707
968
  }
708
969
  if (currentRect) lastRect = currentRect;
709
- if (status.code === "OBSTRUCTED" && status.obstruction) {
970
+ if (status.code === VISIBILITY_CODE.OBSTRUCTED && status.obstruction) {
710
971
  logger5.info(`humanScroll | \u88AB\u906E\u6321: <${status.obstruction.tag}.${status.obstruction.className}>`);
711
972
  }
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 };
980
+ }
712
981
  let deltaY = 0;
713
- if (status.code === "OUT_OF_VIEWPORT") {
714
- if (status.direction === "down") {
982
+ if (status.code === VISIBILITY_CODE.OUT_OF_VIEWPORT) {
983
+ if (status.direction === SCROLL_DIRECTION.DOWN) {
715
984
  deltaY = minStep + Math.random() * (maxStep - minStep);
716
- } else if (status.direction === "up") {
985
+ } else if (status.direction === SCROLL_DIRECTION.UP) {
717
986
  deltaY = -(minStep + Math.random() * (maxStep - minStep));
718
987
  } else {
719
988
  deltaY = 100;
720
989
  }
721
- } else if (status.code === "OBSTRUCTED") {
990
+ } else if (status.code === VISIBILITY_CODE.OBSTRUCTED) {
722
991
  const isBottomHalf = status.cy > status.viewH / 2;
723
992
  const dir = isBottomHalf ? 1 : -1;
724
993
  deltaY = dir * (minStep + Math.random() * 50);
@@ -726,18 +995,25 @@ var Humanize = {
726
995
  if (i === 0 || Math.random() < 0.2) {
727
996
  const viewSize = page.viewportSize();
728
997
  if (viewSize) {
729
- const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 100;
730
- const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 100;
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;
731
1000
  await cursor.actions.move({ x: safeX, y: safeY }).catch(() => {
732
1001
  });
733
1002
  }
734
1003
  }
1004
+ if (!status.isWindow) {
1005
+ await moveMouseToRect(status.targetRect);
1006
+ }
735
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
+ }
736
1012
  didScroll = true;
737
1013
  await (0, import_delay2.default)(this.jitterMs(150 + Math.random() * 100, 0.2));
738
1014
  }
739
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`);
740
- return { element, didScroll };
1016
+ return { element, didScroll, restore };
741
1017
  } catch (error) {
742
1018
  logger5.fail("humanScroll", error);
743
1019
  throw error;
@@ -747,7 +1023,7 @@ var Humanize = {
747
1023
  * 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
748
1024
  *
749
1025
  * @param {import('playwright').Page} page
750
- * @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
1026
+ * @param {string|import('playwright').ElementHandle|import('playwright').Locator} [target] - CSS 选择器、元素句柄或 Locator。如果为空,则点击当前鼠标位置
751
1027
  * @param {Object} [options]
752
1028
  * @param {number} [options.reactionDelay=250] - 反应延迟基础值 (ms),实际 ±30% 抖动
753
1029
  * @param {boolean} [options.throwOnMissing=true] - 元素不存在时是否抛出错误
@@ -756,8 +1032,10 @@ var Humanize = {
756
1032
  async humanClick(page, target, options = {}) {
757
1033
  const cursor = $GetCursor(page);
758
1034
  const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
759
- 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";
760
1036
  logger5.start("humanClick", `target=${targetDesc}`);
1037
+ let element = null;
1038
+ let owned = false;
761
1039
  const restoreOnce = async () => {
762
1040
  if (restoreOnce.restored) return;
763
1041
  restoreOnce.restored = true;
@@ -776,18 +1054,15 @@ var Humanize = {
776
1054
  logger5.success("humanClick", "Clicked current position");
777
1055
  return true;
778
1056
  }
779
- let element;
780
- if (typeof target === "string") {
781
- element = await page.$(target);
782
- if (!element) {
783
- if (throwOnMissing) {
784
- throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${target}`);
785
- }
786
- logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
787
- 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}`);
788
1063
  }
789
- } else {
790
- element = target;
1064
+ logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
1065
+ return false;
791
1066
  }
792
1067
  if (scrollIfNeeded) {
793
1068
  const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
@@ -796,6 +1071,7 @@ var Humanize = {
796
1071
  const box = await element.boundingBox();
797
1072
  if (!box) {
798
1073
  await restoreOnce();
1074
+ await disposeElementHandle(element, owned);
799
1075
  if (throwOnMissing) {
800
1076
  throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
801
1077
  }
@@ -808,10 +1084,12 @@ var Humanize = {
808
1084
  await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
809
1085
  await cursor.actions.click();
810
1086
  await restoreOnce();
1087
+ await disposeElementHandle(element, owned);
811
1088
  logger5.success("humanClick");
812
1089
  return true;
813
1090
  } catch (error) {
814
1091
  await restoreOnce();
1092
+ await disposeElementHandle(element, owned);
815
1093
  logger5.fail("humanClick", error);
816
1094
  throw error;
817
1095
  }