@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.js CHANGED
@@ -507,6 +507,59 @@ import delay2 from "delay";
507
507
  import { createCursor } from "ghost-cursor-playwright";
508
508
  var logger5 = createInternalLogger("Humanize");
509
509
  var $CursorWeakMap = /* @__PURE__ */ new WeakMap();
510
+ var VISIBILITY_CODE = {
511
+ VISIBLE: "VISIBLE",
512
+ OUT_OF_VIEWPORT: "OUT_OF_VIEWPORT",
513
+ OBSTRUCTED: "OBSTRUCTED",
514
+ ZERO_DIMENSIONS: "ZERO_DIMENSIONS",
515
+ HIDDEN: "HIDDEN",
516
+ DETACHED: "DETACHED"
517
+ };
518
+ var VISIBILITY_REASON = {
519
+ OUT_OF_VIEWPORT: "\u4E0D\u5728\u89C6\u53E3\u5185",
520
+ OBSTRUCTED: "\u88AB\u906E\u6321",
521
+ ZERO_DIMENSIONS: "\u5C3A\u5BF8\u4E3A\u96F6",
522
+ HIDDEN: "\u5143\u7D20\u4E0D\u53EF\u89C1",
523
+ DETACHED: "\u5143\u7D20\u5DF2\u88AB\u79FB\u9664"
524
+ };
525
+ var SCROLL_DIRECTION = {
526
+ UP: "up",
527
+ DOWN: "down",
528
+ UNKNOWN: "unknown"
529
+ };
530
+ var DEFAULT_SCROLL_OPTIONS = {
531
+ maxSteps: 20,
532
+ minStep: 150,
533
+ maxStep: 400,
534
+ maxDurationMs: 3500
535
+ };
536
+ var SAME_POSITION_THRESHOLD_PX = 2;
537
+ var SAME_POSITION_LIMIT = 3;
538
+ var SAFE_MOUSE_JITTER_PX = 100;
539
+ var isElementHandleLike = (value) => value && typeof value.evaluate === "function" && typeof value.boundingBox === "function";
540
+ var isLocatorLike = (value) => value && typeof value.elementHandle === "function";
541
+ async function resolveElementHandle(page, target) {
542
+ if (!target) return { element: null, owned: false };
543
+ if (typeof target === "string") {
544
+ const element = await page.$(target);
545
+ return { element, owned: true };
546
+ }
547
+ if (isLocatorLike(target)) {
548
+ const element = await target.elementHandle();
549
+ return { element, owned: true };
550
+ }
551
+ if (isElementHandleLike(target)) {
552
+ return { element: target, owned: false };
553
+ }
554
+ return { element: null, owned: false };
555
+ }
556
+ async function disposeElementHandle(element, owned) {
557
+ if (!element || !owned || typeof element.dispose !== "function") return;
558
+ try {
559
+ await element.dispose();
560
+ } catch {
561
+ }
562
+ }
510
563
  function $GetCursor(page) {
511
564
  const cursor = $CursorWeakMap.get(page);
512
565
  if (!cursor) {
@@ -587,79 +640,287 @@ var Humanize = {
587
640
  * 返回 restore 方法,用于将滚动容器恢复到原位置
588
641
  *
589
642
  * @param {import('playwright').Page} page
590
- * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
591
- * @param {Object} [options]
592
- * @param {number} [options.maxSteps=25] - 最大滚动步数
593
- * @param {number} [options.minStep=80] - 单次滚动最小步长
594
- * @param {number} [options.maxStep=220] - 单次滚动最大步长
595
- */
596
- /**
597
- * 渐进式滚动到元素可见(仅处理 Y 轴滚动)
598
- * 返回 restore 方法,用于将滚动容器恢复到原位置
599
- *
600
- * @param {import('playwright').Page} page
601
- * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
643
+ * @param {string|import('playwright').ElementHandle|import('playwright').Locator} target - CSS 选择器、元素句柄或 Locator
602
644
  * @param {Object} [options]
603
- * @param {number} [options.maxSteps=25] - 最大滚动步数
604
- * @param {number} [options.minStep=80] - 单次滚动最小步长
605
- * @param {number} [options.maxStep=220] - 单次滚动最大步长
645
+ * @param {number} [options.maxSteps=20] - 最大滚动步数
646
+ * @param {number} [options.minStep=150] - 单次滚动最小步长
647
+ * @param {number} [options.maxStep=400] - 单次滚动最大步长
648
+ * @param {number} [options.maxDurationMs=3500] - 最大滚动耗时上限
606
649
  */
607
650
  async humanScroll(page, target, options = {}) {
608
- const { maxSteps = 30, minStep = 150, maxStep = 400 } = options;
609
- const targetDesc = typeof target === "string" ? target : "ElementHandle";
651
+ const {
652
+ maxSteps = DEFAULT_SCROLL_OPTIONS.maxSteps,
653
+ minStep = DEFAULT_SCROLL_OPTIONS.minStep,
654
+ maxStep = DEFAULT_SCROLL_OPTIONS.maxStep,
655
+ maxDurationMs = DEFAULT_SCROLL_OPTIONS.maxDurationMs
656
+ } = options;
657
+ const targetDesc = typeof target === "string" ? target : isLocatorLike(target) ? "Locator" : "ElementHandle";
610
658
  logger5.info(`humanScroll | \u5F00\u59CB\u6EDA\u52A8\u76EE\u6807: ${targetDesc}`);
611
- let element;
612
- if (typeof target === "string") {
613
- element = await page.$(target);
614
- if (!element) {
615
- logger5.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
616
- return { element: null, didScroll: false };
617
- }
618
- } else {
619
- element = target;
659
+ const { element } = await resolveElementHandle(page, target);
660
+ if (!element) {
661
+ logger5.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${targetDesc}`);
662
+ return { element: null, didScroll: false, restore: null };
620
663
  }
621
664
  const cursor = $GetCursor(page);
622
665
  let didScroll = false;
623
666
  let lastRect = null;
624
667
  let samePositionCount = 0;
668
+ const startAt = Date.now();
669
+ const visibilityPayload = {
670
+ codes: VISIBILITY_CODE,
671
+ reasons: VISIBILITY_REASON,
672
+ directions: SCROLL_DIRECTION
673
+ };
674
+ const scrollDeltas = /* @__PURE__ */ new Map();
675
+ const scrollTargetRects = /* @__PURE__ */ new Map();
676
+ let windowIndex = null;
677
+ const moveMouseToRect = async (rect) => {
678
+ if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top)) return false;
679
+ const width = Number.isFinite(rect.width) ? rect.width : Math.max(0, (rect.right ?? rect.left) - rect.left);
680
+ const height = Number.isFinite(rect.height) ? rect.height : Math.max(0, (rect.bottom ?? rect.top) - rect.top);
681
+ const viewSize = page.viewportSize();
682
+ const viewW = viewSize?.width ?? null;
683
+ const viewH = viewSize?.height ?? null;
684
+ if (viewW && viewH) {
685
+ const rectRight = rect.right ?? rect.left + width;
686
+ const rectBottom = rect.bottom ?? rect.top + height;
687
+ if (rectBottom < 0 || rect.top > viewH || rectRight < 0 || rect.left > viewW) {
688
+ return false;
689
+ }
690
+ }
691
+ const rawX = rect.left + width / 2;
692
+ const rawY = rect.top + height / 2;
693
+ if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) return false;
694
+ const padding = 6;
695
+ let x = rawX;
696
+ let y = rawY;
697
+ if (viewW && viewH) {
698
+ const maxX = Math.max(padding, viewW - padding);
699
+ const maxY = Math.max(padding, viewH - padding);
700
+ x = Math.min(Math.max(rawX, padding), maxX);
701
+ y = Math.min(Math.max(rawY, padding), maxY);
702
+ }
703
+ x += (Math.random() - 0.5) * 4;
704
+ y += (Math.random() - 0.5) * 4;
705
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
706
+ await page.mouse.move(x, y, { steps: 6 }).catch(() => {
707
+ });
708
+ return true;
709
+ };
710
+ const wheelByChunks = async (delta, rect, isWindowTarget) => {
711
+ let remaining = delta;
712
+ const stepBase = Math.min(maxStep, 400);
713
+ while (Math.abs(remaining) > 1) {
714
+ const step = Math.abs(remaining) > stepBase ? Math.sign(remaining) * stepBase : remaining;
715
+ if (!isWindowTarget) {
716
+ await moveMouseToRect(rect);
717
+ }
718
+ await page.mouse.wheel(0, step);
719
+ remaining -= step;
720
+ await delay2(this.jitterMs(60, 0.35));
721
+ }
722
+ };
723
+ const restore = async () => {
724
+ if (scrollDeltas.size === 0) return;
725
+ try {
726
+ for (const [index, delta] of scrollDeltas.entries()) {
727
+ if (!delta) continue;
728
+ const rect = scrollTargetRects.get(index);
729
+ const isWindowTarget = windowIndex !== null && index === windowIndex;
730
+ await wheelByChunks(-delta, rect, isWindowTarget);
731
+ }
732
+ } catch (err) {
733
+ logger5.warn(`humanScroll | restore failed: ${err?.message || err}`);
734
+ }
735
+ };
625
736
  const checkVisibility = async () => {
626
- return await element.evaluate((el) => {
737
+ return await element.evaluate((el, payload) => {
738
+ const overflowRe = /(auto|scroll|overlay)/i;
739
+ const isScrollable = (node) => {
740
+ if (!node || node === document.body || node === document.documentElement) return false;
741
+ const style2 = window.getComputedStyle(node);
742
+ const overflowY = style2.overflowY;
743
+ return overflowRe.test(overflowY) && node.scrollHeight > node.clientHeight + 1;
744
+ };
745
+ const getScrollableAncestors = (node) => {
746
+ const list = [];
747
+ let current = node?.parentElement;
748
+ while (current && current !== document.body && current !== document.documentElement) {
749
+ if (isScrollable(current)) {
750
+ list.push(current);
751
+ }
752
+ current = current.parentElement;
753
+ }
754
+ return list;
755
+ };
756
+ if (!el || !el.isConnected) {
757
+ return { code: payload.codes.DETACHED, reason: payload.reasons.DETACHED };
758
+ }
759
+ const style = window.getComputedStyle(el);
760
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) {
761
+ return { code: payload.codes.HIDDEN, reason: payload.reasons.HIDDEN };
762
+ }
627
763
  const rect = el.getBoundingClientRect();
628
764
  if (!rect || rect.width === 0 || rect.height === 0) {
629
- return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6" };
765
+ return { code: payload.codes.ZERO_DIMENSIONS, reason: payload.reasons.ZERO_DIMENSIONS };
630
766
  }
631
- const viewH = window.innerHeight;
632
- const viewW = window.innerWidth;
633
- const resultBase = { rect, viewH, viewW };
767
+ const hasFixedAncestor = (() => {
768
+ let node = el;
769
+ while (node && node !== document.body && node !== document.documentElement) {
770
+ const position = window.getComputedStyle(node).position;
771
+ if (position === "fixed") return true;
772
+ node = node.parentElement;
773
+ }
774
+ return false;
775
+ })();
776
+ const ancestors = getScrollableAncestors(el);
777
+ const targets = ancestors.map((node) => ({
778
+ isWindow: false,
779
+ rect: node.getBoundingClientRect(),
780
+ canScroll: node.scrollHeight > node.clientHeight + 1
781
+ }));
782
+ const windowTarget = {
783
+ isWindow: true,
784
+ rect: { top: 0, left: 0, right: window.innerWidth, bottom: window.innerHeight, width: window.innerWidth, height: window.innerHeight },
785
+ canScroll: document.documentElement.scrollHeight > window.innerHeight
786
+ };
787
+ targets.push(windowTarget);
788
+ const windowIndex2 = targets.length - 1;
634
789
  const cx = rect.left + rect.width / 2;
635
790
  const cy = rect.top + rect.height / 2;
636
- if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
637
- const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
638
- return { ...resultBase, code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy };
791
+ const promoteToVisibleTarget = (index) => {
792
+ let idx = index;
793
+ while (idx < targets.length - 1) {
794
+ const current = targets[idx];
795
+ const parent = targets[idx + 1];
796
+ const c = current.rect;
797
+ const p = parent.rect;
798
+ const outOfParent = c.bottom < p.top || c.top > p.bottom || c.right < p.left || c.left > p.right;
799
+ if (outOfParent) {
800
+ idx += 1;
801
+ continue;
802
+ }
803
+ break;
804
+ }
805
+ return idx;
806
+ };
807
+ let outIndex = -1;
808
+ let direction = payload.directions.UNKNOWN;
809
+ for (let i = 0; i < targets.length; i += 1) {
810
+ const bounds = targets[i].rect;
811
+ if (cy < bounds.top) {
812
+ outIndex = i;
813
+ direction = payload.directions.UP;
814
+ break;
815
+ }
816
+ if (cy > bounds.bottom) {
817
+ outIndex = i;
818
+ direction = payload.directions.DOWN;
819
+ break;
820
+ }
821
+ if (cx < bounds.left || cx > bounds.right) {
822
+ outIndex = i;
823
+ direction = payload.directions.UNKNOWN;
824
+ break;
825
+ }
826
+ }
827
+ if (outIndex !== -1) {
828
+ let scrollTargetIndex = promoteToVisibleTarget(outIndex);
829
+ let target2 = targets[scrollTargetIndex];
830
+ if (!target2.isWindow) {
831
+ const targetRect = target2.rect;
832
+ const outOfWindow = targetRect.bottom < 0 || targetRect.top > window.innerHeight || targetRect.right < 0 || targetRect.left > window.innerWidth;
833
+ if (outOfWindow) {
834
+ scrollTargetIndex = windowIndex2;
835
+ target2 = windowTarget;
836
+ }
837
+ }
838
+ return {
839
+ code: payload.codes.OUT_OF_VIEWPORT,
840
+ reason: payload.reasons.OUT_OF_VIEWPORT,
841
+ direction,
842
+ rect,
843
+ viewH: target2.rect.height,
844
+ viewW: target2.rect.width,
845
+ cy,
846
+ cx,
847
+ scrollTargetIndex,
848
+ isWindow: target2.isWindow,
849
+ canScroll: target2.canScroll,
850
+ isFixed: hasFixedAncestor,
851
+ targetRect: target2.rect,
852
+ windowIndex: windowIndex2
853
+ };
639
854
  }
640
855
  const pointElement = document.elementFromPoint(cx, cy);
641
856
  if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
857
+ let fallbackIndex = promoteToVisibleTarget(targets.length > 1 ? 0 : targets.length - 1);
858
+ let target2 = targets[fallbackIndex];
859
+ if (!target2.isWindow) {
860
+ const targetRect = target2.rect;
861
+ const outOfWindow = targetRect.bottom < 0 || targetRect.top > window.innerHeight || targetRect.right < 0 || targetRect.left > window.innerWidth;
862
+ if (outOfWindow) {
863
+ fallbackIndex = windowIndex2;
864
+ target2 = windowTarget;
865
+ }
866
+ }
642
867
  return {
643
- ...resultBase,
644
- code: "OBSTRUCTED",
645
- reason: "\u88AB\u906E\u6321",
868
+ code: payload.codes.OBSTRUCTED,
869
+ reason: payload.reasons.OBSTRUCTED,
646
870
  obstruction: {
647
871
  tag: pointElement.tagName,
648
872
  id: pointElement.id,
649
873
  className: pointElement.className
650
874
  },
651
- cy
875
+ rect,
876
+ viewH: target2.rect.height,
877
+ viewW: target2.rect.width,
878
+ cy,
879
+ cx,
880
+ scrollTargetIndex: fallbackIndex,
881
+ isWindow: target2.isWindow,
882
+ canScroll: target2.canScroll,
883
+ isFixed: hasFixedAncestor,
884
+ targetRect: target2.rect,
885
+ windowIndex: windowIndex2
652
886
  };
653
887
  }
654
- return { ...resultBase, code: "VISIBLE" };
655
- });
888
+ return {
889
+ code: payload.codes.VISIBLE,
890
+ rect,
891
+ viewH: windowTarget.rect.height,
892
+ viewW: windowTarget.rect.width,
893
+ cy,
894
+ cx,
895
+ scrollTargetIndex: targets.length - 1,
896
+ isWindow: true,
897
+ canScroll: windowTarget.canScroll,
898
+ isFixed: hasFixedAncestor,
899
+ targetRect: windowTarget.rect,
900
+ windowIndex: windowIndex2
901
+ };
902
+ }, visibilityPayload);
656
903
  };
657
904
  try {
658
905
  for (let i = 0; i < maxSteps; i++) {
906
+ if (Date.now() - startAt > maxDurationMs) {
907
+ logger5.warn(`humanScroll | \u26A0\uFE0F \u8D85\u8FC7\u6700\u5927\u8017\u65F6 ${maxDurationMs}ms\uFF0C\u63D0\u524D\u7ED3\u675F`);
908
+ return { element, didScroll, restore };
909
+ }
659
910
  const status = await checkVisibility();
660
- if (status.code === "VISIBLE") {
911
+ if (windowIndex === null && typeof status.windowIndex === "number") {
912
+ windowIndex = status.windowIndex;
913
+ }
914
+ if (typeof status.scrollTargetIndex === "number" && status.targetRect) {
915
+ scrollTargetRects.set(status.scrollTargetIndex, status.targetRect);
916
+ }
917
+ if (status.code === VISIBILITY_CODE.VISIBLE) {
661
918
  logger5.info("humanScroll | \u5143\u7D20\u5DF2\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
662
- return { element, didScroll };
919
+ return { element, didScroll, restore };
920
+ }
921
+ if (status.code === VISIBILITY_CODE.DETACHED || status.code === VISIBILITY_CODE.HIDDEN || status.code === VISIBILITY_CODE.ZERO_DIMENSIONS) {
922
+ logger5.warn(`humanScroll | ${status.reason || "\u5143\u7D20\u4E0D\u53EF\u7528"}`);
923
+ return { element, didScroll, restore };
663
924
  }
664
925
  const directionStr = status.direction ? `(${status.direction})` : "";
665
926
  logger5.info(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${directionStr}`);
@@ -667,31 +928,39 @@ var Humanize = {
667
928
  if (i > 0 && lastRect && currentRect) {
668
929
  const dy = Math.abs(currentRect.top - lastRect.top);
669
930
  const dx = Math.abs(currentRect.left - lastRect.left);
670
- if (dy < 2 && dx < 2) {
931
+ if (dy < SAME_POSITION_THRESHOLD_PX && dx < SAME_POSITION_THRESHOLD_PX) {
671
932
  samePositionCount++;
672
- logger5.info(`humanScroll | \u26A0\uFE0F \u8B66\u544A: \u6EDA\u52A8\u540E\u4F4D\u7F6E\u672A\u53D8 (${samePositionCount}/3). dy=${dy}`);
673
- if (samePositionCount >= 3) {
933
+ logger5.info(`humanScroll | \u26A0\uFE0F \u8B66\u544A: \u6EDA\u52A8\u540E\u4F4D\u7F6E\u672A\u53D8 (${samePositionCount}/${SAME_POSITION_LIMIT}). dy=${dy}`);
934
+ if (samePositionCount >= SAME_POSITION_LIMIT) {
674
935
  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");
675
- return { element, didScroll };
936
+ return { element, didScroll, restore };
676
937
  }
677
938
  } else {
678
939
  samePositionCount = 0;
679
940
  }
680
941
  }
681
942
  if (currentRect) lastRect = currentRect;
682
- if (status.code === "OBSTRUCTED" && status.obstruction) {
943
+ if (status.code === VISIBILITY_CODE.OBSTRUCTED && status.obstruction) {
683
944
  logger5.info(`humanScroll | \u88AB\u906E\u6321: <${status.obstruction.tag}.${status.obstruction.className}>`);
684
945
  }
946
+ if (status.code === VISIBILITY_CODE.OBSTRUCTED && status.isFixed) {
947
+ logger5.warn("humanScroll | \u5143\u7D20\u4E3A Fixed \u4E14\u88AB\u906E\u6321\uFF0C\u505C\u6B62\u6EDA\u52A8\u5C1D\u8BD5");
948
+ return { element, didScroll, restore };
949
+ }
950
+ if (!status.canScroll) {
951
+ logger5.warn("humanScroll | \u5F53\u524D\u5BB9\u5668\u4E0D\u53EF\u6EDA\u52A8\uFF0C\u505C\u6B62\u6EDA\u52A8\u5C1D\u8BD5");
952
+ return { element, didScroll, restore };
953
+ }
685
954
  let deltaY = 0;
686
- if (status.code === "OUT_OF_VIEWPORT") {
687
- if (status.direction === "down") {
955
+ if (status.code === VISIBILITY_CODE.OUT_OF_VIEWPORT) {
956
+ if (status.direction === SCROLL_DIRECTION.DOWN) {
688
957
  deltaY = minStep + Math.random() * (maxStep - minStep);
689
- } else if (status.direction === "up") {
958
+ } else if (status.direction === SCROLL_DIRECTION.UP) {
690
959
  deltaY = -(minStep + Math.random() * (maxStep - minStep));
691
960
  } else {
692
961
  deltaY = 100;
693
962
  }
694
- } else if (status.code === "OBSTRUCTED") {
963
+ } else if (status.code === VISIBILITY_CODE.OBSTRUCTED) {
695
964
  const isBottomHalf = status.cy > status.viewH / 2;
696
965
  const dir = isBottomHalf ? 1 : -1;
697
966
  deltaY = dir * (minStep + Math.random() * 50);
@@ -699,18 +968,25 @@ var Humanize = {
699
968
  if (i === 0 || Math.random() < 0.2) {
700
969
  const viewSize = page.viewportSize();
701
970
  if (viewSize) {
702
- const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 100;
703
- const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 100;
971
+ const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * SAFE_MOUSE_JITTER_PX;
972
+ const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * SAFE_MOUSE_JITTER_PX;
704
973
  await cursor.actions.move({ x: safeX, y: safeY }).catch(() => {
705
974
  });
706
975
  }
707
976
  }
977
+ if (!status.isWindow) {
978
+ await moveMouseToRect(status.targetRect);
979
+ }
708
980
  await page.mouse.wheel(0, deltaY);
981
+ if (typeof status.scrollTargetIndex === "number") {
982
+ const prev = scrollDeltas.get(status.scrollTargetIndex) || 0;
983
+ scrollDeltas.set(status.scrollTargetIndex, prev + deltaY);
984
+ }
709
985
  didScroll = true;
710
986
  await delay2(this.jitterMs(150 + Math.random() * 100, 0.2));
711
987
  }
712
988
  logger5.warn(`humanScroll | \u26A0\uFE0F \u8FBE\u5230\u6700\u5927\u6B65\u6570 (${maxSteps}) \u4ECD\u65E0\u6CD5\u5B8C\u5168\u53EF\u89C1\uFF0C\u653E\u5F03\u6EDA\u52A8`);
713
- return { element, didScroll };
989
+ return { element, didScroll, restore };
714
990
  } catch (error) {
715
991
  logger5.fail("humanScroll", error);
716
992
  throw error;
@@ -720,7 +996,7 @@ var Humanize = {
720
996
  * 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
721
997
  *
722
998
  * @param {import('playwright').Page} page
723
- * @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
999
+ * @param {string|import('playwright').ElementHandle|import('playwright').Locator} [target] - CSS 选择器、元素句柄或 Locator。如果为空,则点击当前鼠标位置
724
1000
  * @param {Object} [options]
725
1001
  * @param {number} [options.reactionDelay=250] - 反应延迟基础值 (ms),实际 ±30% 抖动
726
1002
  * @param {boolean} [options.throwOnMissing=true] - 元素不存在时是否抛出错误
@@ -729,8 +1005,10 @@ var Humanize = {
729
1005
  async humanClick(page, target, options = {}) {
730
1006
  const cursor = $GetCursor(page);
731
1007
  const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
732
- const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
1008
+ const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : isLocatorLike(target) ? "Locator" : "ElementHandle";
733
1009
  logger5.start("humanClick", `target=${targetDesc}`);
1010
+ let element = null;
1011
+ let owned = false;
734
1012
  const restoreOnce = async () => {
735
1013
  if (restoreOnce.restored) return;
736
1014
  restoreOnce.restored = true;
@@ -749,18 +1027,15 @@ var Humanize = {
749
1027
  logger5.success("humanClick", "Clicked current position");
750
1028
  return true;
751
1029
  }
752
- let element;
753
- if (typeof target === "string") {
754
- element = await page.$(target);
755
- if (!element) {
756
- if (throwOnMissing) {
757
- throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${target}`);
758
- }
759
- logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
760
- return false;
1030
+ const resolved = await resolveElementHandle(page, target);
1031
+ element = resolved.element;
1032
+ owned = resolved.owned;
1033
+ if (!element) {
1034
+ if (throwOnMissing) {
1035
+ throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${targetDesc}`);
761
1036
  }
762
- } else {
763
- element = target;
1037
+ logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
1038
+ return false;
764
1039
  }
765
1040
  if (scrollIfNeeded) {
766
1041
  const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
@@ -769,6 +1044,7 @@ var Humanize = {
769
1044
  const box = await element.boundingBox();
770
1045
  if (!box) {
771
1046
  await restoreOnce();
1047
+ await disposeElementHandle(element, owned);
772
1048
  if (throwOnMissing) {
773
1049
  throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
774
1050
  }
@@ -781,10 +1057,12 @@ var Humanize = {
781
1057
  await delay2(this.jitterMs(reactionDelay, 0.4));
782
1058
  await cursor.actions.click();
783
1059
  await restoreOnce();
1060
+ await disposeElementHandle(element, owned);
784
1061
  logger5.success("humanClick");
785
1062
  return true;
786
1063
  } catch (error) {
787
1064
  await restoreOnce();
1065
+ await disposeElementHandle(element, owned);
788
1066
  logger5.fail("humanClick", error);
789
1067
  throw error;
790
1068
  }