@skrillex1224/playwright-toolkit 2.1.61 → 2.1.62

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
@@ -59,6 +59,17 @@ var FAILED_KEY_SEPARATOR = "::<@>::";
59
59
  var PresetOfLiveViewKey = "LIVE_VIEW_SCREENSHOT";
60
60
 
61
61
  // src/internals/logger.js
62
+ var pad = (value, size = 2) => String(value).padStart(size, "0");
63
+ var formatTimestamp = (date = /* @__PURE__ */ new Date()) => {
64
+ const year = date.getFullYear();
65
+ const month = pad(date.getMonth() + 1);
66
+ const day = pad(date.getDate());
67
+ const hours = pad(date.getHours());
68
+ const minutes = pad(date.getMinutes());
69
+ const seconds = pad(date.getSeconds());
70
+ const millis = pad(date.getMilliseconds(), 3);
71
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${millis}`;
72
+ };
62
73
  var formatLine = (prefix, icon, message) => {
63
74
  const parts = [];
64
75
  if (prefix) parts.push(`[${prefix}]`);
@@ -112,7 +123,10 @@ var createBaseLogger = (prefix = "", explicitLogger) => {
112
123
  const dispatch = (methodName, icon, message, color) => {
113
124
  const logger11 = resolveLogger(explicitLogger);
114
125
  const logFn = resolveLogMethod(logger11, methodName);
115
- logFn(colorize(formatLine(name, icon, message), color));
126
+ const timestamp = colorize(`[${formatTimestamp()}]`, ANSI.gray);
127
+ const line = formatLine(name, icon, message);
128
+ const coloredLine = colorize(line, color);
129
+ logFn(`${timestamp} ${coloredLine}`.trim());
116
130
  };
117
131
  return {
118
132
  info: (message) => dispatch("info", "\u{1F4D6}", message, ANSI.cyan),
@@ -534,59 +548,6 @@ var import_delay2 = __toESM(require("delay"), 1);
534
548
  var import_ghost_cursor_playwright = require("ghost-cursor-playwright");
535
549
  var logger5 = createInternalLogger("Humanize");
536
550
  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: 12,
559
- minStep: 220,
560
- maxStep: 600,
561
- maxDurationMs: 5e3
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
- }
590
551
  function $GetCursor(page) {
591
552
  const cursor = $CursorWeakMap.get(page);
592
553
  if (!cursor) {
@@ -667,353 +628,158 @@ var Humanize = {
667
628
  * 返回 restore 方法,用于将滚动容器恢复到原位置
668
629
  *
669
630
  * @param {import('playwright').Page} page
670
- * @param {string|import('playwright').ElementHandle|import('playwright').Locator} target - CSS 选择器、元素句柄或 Locator
631
+ * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
671
632
  * @param {Object} [options]
672
- * @param {number} [options.maxSteps=12] - 最大滚动步数
673
- * @param {number} [options.minStep=220] - 单次滚动最小步长
674
- * @param {number} [options.maxStep=600] - 单次滚动最大步长
675
- * @param {number} [options.maxDurationMs=5000] - 最大滚动耗时上限
633
+ * @param {number} [options.maxSteps=14] - 最大滚动步数
634
+ * @param {number} [options.minStep=260] - 单次滚动最小步长
635
+ * @param {number} [options.maxStep=800] - 单次滚动最大步长
636
+ * @param {number} [options.maxDurationMs] - 最长耗时上限 (默认随 maxSteps 估算)
676
637
  */
677
638
  async humanScroll(page, target, options = {}) {
678
639
  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
640
+ maxSteps = 14,
641
+ minStep = 260,
642
+ maxStep = 800,
643
+ maxDurationMs = maxSteps * 220 + 800
683
644
  } = 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 };
645
+ const targetDesc = typeof target === "string" ? target : "ElementHandle";
646
+ logger5.start("humanScroll", `target=${targetDesc}`);
647
+ let element;
648
+ if (typeof target === "string") {
649
+ element = await page.$(target);
650
+ if (!element) {
651
+ logger5.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
652
+ return { element: null, didScroll: false };
653
+ }
654
+ } else {
655
+ element = target;
690
656
  }
691
657
  const cursor = $GetCursor(page);
692
658
  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
- };
763
659
  const checkVisibility = async () => {
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
- }
660
+ return await element.evaluate((el) => {
790
661
  const rect = el.getBoundingClientRect();
791
662
  if (!rect || rect.width === 0 || rect.height === 0) {
792
- return { code: payload.codes.ZERO_DIMENSIONS, reason: payload.reasons.ZERO_DIMENSIONS };
663
+ return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6" };
793
664
  }
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;
816
665
  const cx = rect.left + rect.width / 2;
817
666
  const cy = rect.top + rect.height / 2;
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;
667
+ const viewH = window.innerHeight;
668
+ const viewW = window.innerWidth;
669
+ let isFixed = false;
670
+ for (let node = el; node && node !== document.body; node = node.parentElement) {
671
+ const style = window.getComputedStyle(node);
672
+ if (style && style.position === "fixed") {
673
+ isFixed = true;
851
674
  break;
852
675
  }
853
676
  }
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
- };
677
+ if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
678
+ const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
679
+ return { code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy, viewH, isFixed };
881
680
  }
882
681
  const pointElement = document.elementFromPoint(cx, cy);
883
682
  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
- }
894
683
  return {
895
- code: payload.codes.OBSTRUCTED,
896
- reason: payload.reasons.OBSTRUCTED,
684
+ code: "OBSTRUCTED",
685
+ reason: "\u88AB\u906E\u6321",
897
686
  obstruction: {
898
687
  tag: pointElement.tagName,
899
688
  id: pointElement.id,
900
689
  className: pointElement.className
901
690
  },
902
- rect,
903
- viewH: target2.rect.height,
904
- viewW: target2.rect.width,
905
691
  cy,
906
- cx,
907
- scrollTargetIndex: fallbackIndex,
908
- isWindow: target2.isWindow,
909
- canScroll: target2.canScroll,
910
- isFixed: hasFixedAncestor,
911
- targetRect: target2.rect,
912
- windowIndex: windowIndex2
692
+ // Return Center Y for smart direction calculation
693
+ viewH,
694
+ isFixed
913
695
  };
914
696
  }
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
697
+ return { code: "VISIBLE", isFixed };
698
+ });
699
+ };
700
+ const getScrollableRect = async () => {
701
+ return await element.evaluate((el) => {
702
+ const isScrollable = (node) => {
703
+ const style = window.getComputedStyle(node);
704
+ if (!style) return false;
705
+ const overflowY = style.overflowY;
706
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
707
+ return node.scrollHeight > node.clientHeight + 1;
928
708
  };
929
- }, visibilityPayload);
709
+ let current = el;
710
+ while (current && current !== document.body) {
711
+ if (isScrollable(current)) {
712
+ const rect = current.getBoundingClientRect();
713
+ if (rect && rect.width > 0 && rect.height > 0) {
714
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
715
+ }
716
+ }
717
+ current = current.parentElement;
718
+ }
719
+ return null;
720
+ });
930
721
  };
722
+ const startTime = Date.now();
931
723
  try {
932
724
  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 };
725
+ if (Date.now() - startTime > maxDurationMs) {
726
+ logger5.warn(`humanScroll | \u8D85\u65F6\u4FDD\u62A4\u89E6\u53D1 (${maxDurationMs}ms)`);
727
+ return { element, didScroll };
936
728
  }
937
729
  const status = await checkVisibility();
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
- }
730
+ if (status.code === "VISIBLE") {
731
+ if (status.isFixed) {
732
+ logger5.info("humanScroll | fixed \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
965
733
  } else {
966
- samePositionCount = 0;
734
+ logger5.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
967
735
  }
736
+ logger5.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
737
+ return { element, didScroll };
968
738
  }
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}>`);
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 };
739
+ logger5.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${status.direction ? `(${status.direction})` : ""}`);
740
+ if (status.code === "OBSTRUCTED" && status.obstruction) {
741
+ logger5.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
976
742
  }
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 };
743
+ const scrollRect = await getScrollableRect();
744
+ if (!scrollRect && status.isFixed) {
745
+ logger5.warn("humanScroll | fixed \u5BB9\u5668\u5185\u4E14\u65E0\u53EF\u6EDA\u52A8\u7956\u5148\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
746
+ return { element, didScroll };
980
747
  }
748
+ const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
749
+ const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
981
750
  let deltaY = 0;
982
- if (status.code === VISIBILITY_CODE.OUT_OF_VIEWPORT) {
983
- if (status.direction === SCROLL_DIRECTION.DOWN) {
984
- deltaY = minStep + Math.random() * (maxStep - minStep);
985
- } else if (status.direction === SCROLL_DIRECTION.UP) {
986
- deltaY = -(minStep + Math.random() * (maxStep - minStep));
751
+ if (status.code === "OUT_OF_VIEWPORT") {
752
+ if (status.direction === "down") {
753
+ deltaY = stepMin + Math.random() * (stepMax - stepMin);
754
+ } else if (status.direction === "up") {
755
+ deltaY = -(stepMin + Math.random() * (stepMax - stepMin));
987
756
  } else {
988
757
  deltaY = 100;
989
758
  }
990
- } else if (status.code === VISIBILITY_CODE.OBSTRUCTED) {
991
- const isBottomHalf = status.cy > status.viewH / 2;
992
- const dir = isBottomHalf ? 1 : -1;
993
- deltaY = dir * (minStep + Math.random() * 50);
759
+ } else if (status.code === "OBSTRUCTED") {
760
+ const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
761
+ const isBottomHalf = status.cy > halfY;
762
+ const direction = isBottomHalf ? 1 : -1;
763
+ deltaY = direction * (stepMin + Math.random() * 50);
994
764
  }
995
- if (i === 0 || Math.random() < 0.2) {
765
+ if (i === 0) {
996
766
  const viewSize = page.viewportSize();
997
- if (viewSize) {
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
- });
767
+ if (scrollRect) {
768
+ const safeX = scrollRect.x + scrollRect.width * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.width * 0.4);
769
+ const safeY = scrollRect.y + scrollRect.height * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.height * 0.4);
770
+ await cursor.actions.move({ x: safeX, y: safeY });
771
+ } else if (viewSize) {
772
+ const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 80;
773
+ const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 80;
774
+ await cursor.actions.move({ x: safeX, y: safeY });
1002
775
  }
1003
776
  }
1004
- if (!status.isWindow) {
1005
- await moveMouseToRect(status.targetRect);
1006
- }
1007
777
  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
- }
1012
778
  didScroll = true;
1013
- await (0, import_delay2.default)(this.jitterMs(150 + Math.random() * 100, 0.2));
779
+ await (0, import_delay2.default)(this.jitterMs(20 + Math.random() * 40, 0.2));
1014
780
  }
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 };
781
+ logger5.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
782
+ return { element, didScroll };
1017
783
  } catch (error) {
1018
784
  logger5.fail("humanScroll", error);
1019
785
  throw error;
@@ -1023,7 +789,7 @@ var Humanize = {
1023
789
  * 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
1024
790
  *
1025
791
  * @param {import('playwright').Page} page
1026
- * @param {string|import('playwright').ElementHandle|import('playwright').Locator} [target] - CSS 选择器、元素句柄或 Locator。如果为空,则点击当前鼠标位置
792
+ * @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
1027
793
  * @param {Object} [options]
1028
794
  * @param {number} [options.reactionDelay=250] - 反应延迟基础值 (ms),实际 ±30% 抖动
1029
795
  * @param {boolean} [options.throwOnMissing=true] - 元素不存在时是否抛出错误
@@ -1032,10 +798,8 @@ var Humanize = {
1032
798
  async humanClick(page, target, options = {}) {
1033
799
  const cursor = $GetCursor(page);
1034
800
  const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
1035
- const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : isLocatorLike(target) ? "Locator" : "ElementHandle";
801
+ const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
1036
802
  logger5.start("humanClick", `target=${targetDesc}`);
1037
- let element = null;
1038
- let owned = false;
1039
803
  const restoreOnce = async () => {
1040
804
  if (restoreOnce.restored) return;
1041
805
  restoreOnce.restored = true;
@@ -1054,15 +818,18 @@ var Humanize = {
1054
818
  logger5.success("humanClick", "Clicked current position");
1055
819
  return true;
1056
820
  }
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}`);
821
+ let element;
822
+ if (typeof target === "string") {
823
+ element = await page.$(target);
824
+ if (!element) {
825
+ if (throwOnMissing) {
826
+ throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${target}`);
827
+ }
828
+ logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
829
+ return false;
1063
830
  }
1064
- logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
1065
- return false;
831
+ } else {
832
+ element = target;
1066
833
  }
1067
834
  if (scrollIfNeeded) {
1068
835
  const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
@@ -1071,7 +838,6 @@ var Humanize = {
1071
838
  const box = await element.boundingBox();
1072
839
  if (!box) {
1073
840
  await restoreOnce();
1074
- await disposeElementHandle(element, owned);
1075
841
  if (throwOnMissing) {
1076
842
  throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
1077
843
  }
@@ -1084,12 +850,10 @@ var Humanize = {
1084
850
  await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
1085
851
  await cursor.actions.click();
1086
852
  await restoreOnce();
1087
- await disposeElementHandle(element, owned);
1088
853
  logger5.success("humanClick");
1089
854
  return true;
1090
855
  } catch (error) {
1091
856
  await restoreOnce();
1092
- await disposeElementHandle(element, owned);
1093
857
  logger5.fail("humanClick", error);
1094
858
  throw error;
1095
859
  }
@@ -1226,6 +990,34 @@ var Humanize = {
1226
990
  logger5.fail("warmUpBrowsing", error);
1227
991
  throw error;
1228
992
  }
993
+ },
994
+ /**
995
+ * 自然滚动 - 带惯性、减速效果和随机抖动
996
+ * @param {import('playwright').Page} page
997
+ * @param {'up' | 'down'} [direction='down'] - 滚动方向
998
+ * @param {number} [distance=300] - 总滚动距离基础值 (px),±15% 抖动
999
+ * @param {number} [baseSteps=5] - 分几步完成基础值,±1 随机
1000
+ */
1001
+ async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
1002
+ const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
1003
+ const actualDistance = this.jitterMs(distance, 0.15);
1004
+ logger5.start("naturalScroll", `dir=${direction}, dist=${actualDistance}, steps=${steps}`);
1005
+ const sign = direction === "down" ? 1 : -1;
1006
+ const stepDistance = actualDistance / steps;
1007
+ try {
1008
+ for (let i = 0; i < steps; i++) {
1009
+ const factor = 1 - i / steps * 0.5;
1010
+ const jitter = 0.9 + Math.random() * 0.2;
1011
+ const scrollAmount = stepDistance * factor * sign * jitter;
1012
+ await page.mouse.wheel(0, scrollAmount);
1013
+ const baseDelay = 60 + i * 25;
1014
+ await (0, import_delay2.default)(this.jitterMs(baseDelay, 0.3));
1015
+ }
1016
+ logger5.success("naturalScroll");
1017
+ } catch (error) {
1018
+ logger5.fail("naturalScroll", error);
1019
+ throw error;
1020
+ }
1229
1021
  }
1230
1022
  };
1231
1023