@skrillex1224/playwright-toolkit 2.1.60 → 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.js CHANGED
@@ -32,6 +32,17 @@ var FAILED_KEY_SEPARATOR = "::<@>::";
32
32
  var PresetOfLiveViewKey = "LIVE_VIEW_SCREENSHOT";
33
33
 
34
34
  // src/internals/logger.js
35
+ var pad = (value, size = 2) => String(value).padStart(size, "0");
36
+ var formatTimestamp = (date = /* @__PURE__ */ new Date()) => {
37
+ const year = date.getFullYear();
38
+ const month = pad(date.getMonth() + 1);
39
+ const day = pad(date.getDate());
40
+ const hours = pad(date.getHours());
41
+ const minutes = pad(date.getMinutes());
42
+ const seconds = pad(date.getSeconds());
43
+ const millis = pad(date.getMilliseconds(), 3);
44
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${millis}`;
45
+ };
35
46
  var formatLine = (prefix, icon, message) => {
36
47
  const parts = [];
37
48
  if (prefix) parts.push(`[${prefix}]`);
@@ -85,7 +96,10 @@ var createBaseLogger = (prefix = "", explicitLogger) => {
85
96
  const dispatch = (methodName, icon, message, color) => {
86
97
  const logger11 = resolveLogger(explicitLogger);
87
98
  const logFn = resolveLogMethod(logger11, methodName);
88
- logFn(colorize(formatLine(name, icon, message), color));
99
+ const timestamp = colorize(`[${formatTimestamp()}]`, ANSI.gray);
100
+ const line = formatLine(name, icon, message);
101
+ const coloredLine = colorize(line, color);
102
+ logFn(`${timestamp} ${coloredLine}`.trim());
89
103
  };
90
104
  return {
91
105
  info: (message) => dispatch("info", "\u{1F4D6}", message, ANSI.cyan),
@@ -507,59 +521,6 @@ import delay2 from "delay";
507
521
  import { createCursor } from "ghost-cursor-playwright";
508
522
  var logger5 = createInternalLogger("Humanize");
509
523
  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
- }
563
524
  function $GetCursor(page) {
564
525
  const cursor = $CursorWeakMap.get(page);
565
526
  if (!cursor) {
@@ -640,353 +601,158 @@ var Humanize = {
640
601
  * 返回 restore 方法,用于将滚动容器恢复到原位置
641
602
  *
642
603
  * @param {import('playwright').Page} page
643
- * @param {string|import('playwright').ElementHandle|import('playwright').Locator} target - CSS 选择器、元素句柄或 Locator
604
+ * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
644
605
  * @param {Object} [options]
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
+ * @param {number} [options.maxSteps=14] - 最大滚动步数
607
+ * @param {number} [options.minStep=260] - 单次滚动最小步长
608
+ * @param {number} [options.maxStep=800] - 单次滚动最大步长
609
+ * @param {number} [options.maxDurationMs] - 最长耗时上限 (默认随 maxSteps 估算)
649
610
  */
650
611
  async humanScroll(page, target, options = {}) {
651
612
  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
613
+ maxSteps = 14,
614
+ minStep = 260,
615
+ maxStep = 800,
616
+ maxDurationMs = maxSteps * 220 + 800
656
617
  } = options;
657
- const targetDesc = typeof target === "string" ? target : isLocatorLike(target) ? "Locator" : "ElementHandle";
658
- logger5.info(`humanScroll | \u5F00\u59CB\u6EDA\u52A8\u76EE\u6807: ${targetDesc}`);
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 };
618
+ const targetDesc = typeof target === "string" ? target : "ElementHandle";
619
+ logger5.start("humanScroll", `target=${targetDesc}`);
620
+ let element;
621
+ if (typeof target === "string") {
622
+ element = await page.$(target);
623
+ if (!element) {
624
+ logger5.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
625
+ return { element: null, didScroll: false };
626
+ }
627
+ } else {
628
+ element = target;
663
629
  }
664
630
  const cursor = $GetCursor(page);
665
631
  let didScroll = false;
666
- let lastRect = null;
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
- };
736
632
  const checkVisibility = async () => {
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
- }
633
+ return await element.evaluate((el) => {
763
634
  const rect = el.getBoundingClientRect();
764
635
  if (!rect || rect.width === 0 || rect.height === 0) {
765
- return { code: payload.codes.ZERO_DIMENSIONS, reason: payload.reasons.ZERO_DIMENSIONS };
636
+ return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6" };
766
637
  }
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;
789
638
  const cx = rect.left + rect.width / 2;
790
639
  const cy = rect.top + rect.height / 2;
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;
640
+ const viewH = window.innerHeight;
641
+ const viewW = window.innerWidth;
642
+ let isFixed = false;
643
+ for (let node = el; node && node !== document.body; node = node.parentElement) {
644
+ const style = window.getComputedStyle(node);
645
+ if (style && style.position === "fixed") {
646
+ isFixed = true;
824
647
  break;
825
648
  }
826
649
  }
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
- };
650
+ if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
651
+ const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
652
+ return { code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy, viewH, isFixed };
854
653
  }
855
654
  const pointElement = document.elementFromPoint(cx, cy);
856
655
  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
- }
867
656
  return {
868
- code: payload.codes.OBSTRUCTED,
869
- reason: payload.reasons.OBSTRUCTED,
657
+ code: "OBSTRUCTED",
658
+ reason: "\u88AB\u906E\u6321",
870
659
  obstruction: {
871
660
  tag: pointElement.tagName,
872
661
  id: pointElement.id,
873
662
  className: pointElement.className
874
663
  },
875
- rect,
876
- viewH: target2.rect.height,
877
- viewW: target2.rect.width,
878
664
  cy,
879
- cx,
880
- scrollTargetIndex: fallbackIndex,
881
- isWindow: target2.isWindow,
882
- canScroll: target2.canScroll,
883
- isFixed: hasFixedAncestor,
884
- targetRect: target2.rect,
885
- windowIndex: windowIndex2
665
+ // Return Center Y for smart direction calculation
666
+ viewH,
667
+ isFixed
886
668
  };
887
669
  }
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
670
+ return { code: "VISIBLE", isFixed };
671
+ });
672
+ };
673
+ const getScrollableRect = async () => {
674
+ return await element.evaluate((el) => {
675
+ const isScrollable = (node) => {
676
+ const style = window.getComputedStyle(node);
677
+ if (!style) return false;
678
+ const overflowY = style.overflowY;
679
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
680
+ return node.scrollHeight > node.clientHeight + 1;
901
681
  };
902
- }, visibilityPayload);
682
+ let current = el;
683
+ while (current && current !== document.body) {
684
+ if (isScrollable(current)) {
685
+ const rect = current.getBoundingClientRect();
686
+ if (rect && rect.width > 0 && rect.height > 0) {
687
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
688
+ }
689
+ }
690
+ current = current.parentElement;
691
+ }
692
+ return null;
693
+ });
903
694
  };
695
+ const startTime = Date.now();
904
696
  try {
905
697
  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 };
698
+ if (Date.now() - startTime > maxDurationMs) {
699
+ logger5.warn(`humanScroll | \u8D85\u65F6\u4FDD\u62A4\u89E6\u53D1 (${maxDurationMs}ms)`);
700
+ return { element, didScroll };
909
701
  }
910
702
  const status = await checkVisibility();
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) {
918
- logger5.info("humanScroll | \u5143\u7D20\u5DF2\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
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 };
924
- }
925
- const directionStr = status.direction ? `(${status.direction})` : "";
926
- logger5.info(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${directionStr}`);
927
- const currentRect = status.rect;
928
- if (i > 0 && lastRect && currentRect) {
929
- const dy = Math.abs(currentRect.top - lastRect.top);
930
- const dx = Math.abs(currentRect.left - lastRect.left);
931
- if (dy < SAME_POSITION_THRESHOLD_PX && dx < SAME_POSITION_THRESHOLD_PX) {
932
- samePositionCount++;
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) {
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");
936
- return { element, didScroll, restore };
937
- }
703
+ if (status.code === "VISIBLE") {
704
+ if (status.isFixed) {
705
+ logger5.info("humanScroll | fixed \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
938
706
  } else {
939
- samePositionCount = 0;
707
+ logger5.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
940
708
  }
709
+ logger5.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
710
+ return { element, didScroll };
941
711
  }
942
- if (currentRect) lastRect = currentRect;
943
- if (status.code === VISIBILITY_CODE.OBSTRUCTED && status.obstruction) {
944
- logger5.info(`humanScroll | \u88AB\u906E\u6321: <${status.obstruction.tag}.${status.obstruction.className}>`);
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 };
712
+ logger5.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${status.direction ? `(${status.direction})` : ""}`);
713
+ if (status.code === "OBSTRUCTED" && status.obstruction) {
714
+ logger5.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
949
715
  }
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 };
716
+ const scrollRect = await getScrollableRect();
717
+ if (!scrollRect && status.isFixed) {
718
+ logger5.warn("humanScroll | fixed \u5BB9\u5668\u5185\u4E14\u65E0\u53EF\u6EDA\u52A8\u7956\u5148\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
719
+ return { element, didScroll };
953
720
  }
721
+ const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
722
+ const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
954
723
  let deltaY = 0;
955
- if (status.code === VISIBILITY_CODE.OUT_OF_VIEWPORT) {
956
- if (status.direction === SCROLL_DIRECTION.DOWN) {
957
- deltaY = minStep + Math.random() * (maxStep - minStep);
958
- } else if (status.direction === SCROLL_DIRECTION.UP) {
959
- deltaY = -(minStep + Math.random() * (maxStep - minStep));
724
+ if (status.code === "OUT_OF_VIEWPORT") {
725
+ if (status.direction === "down") {
726
+ deltaY = stepMin + Math.random() * (stepMax - stepMin);
727
+ } else if (status.direction === "up") {
728
+ deltaY = -(stepMin + Math.random() * (stepMax - stepMin));
960
729
  } else {
961
730
  deltaY = 100;
962
731
  }
963
- } else if (status.code === VISIBILITY_CODE.OBSTRUCTED) {
964
- const isBottomHalf = status.cy > status.viewH / 2;
965
- const dir = isBottomHalf ? 1 : -1;
966
- deltaY = dir * (minStep + Math.random() * 50);
732
+ } else if (status.code === "OBSTRUCTED") {
733
+ const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
734
+ const isBottomHalf = status.cy > halfY;
735
+ const direction = isBottomHalf ? 1 : -1;
736
+ deltaY = direction * (stepMin + Math.random() * 50);
967
737
  }
968
- if (i === 0 || Math.random() < 0.2) {
738
+ if (i === 0) {
969
739
  const viewSize = page.viewportSize();
970
- if (viewSize) {
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;
973
- await cursor.actions.move({ x: safeX, y: safeY }).catch(() => {
974
- });
740
+ if (scrollRect) {
741
+ const safeX = scrollRect.x + scrollRect.width * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.width * 0.4);
742
+ const safeY = scrollRect.y + scrollRect.height * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.height * 0.4);
743
+ await cursor.actions.move({ x: safeX, y: safeY });
744
+ } else if (viewSize) {
745
+ const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 80;
746
+ const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 80;
747
+ await cursor.actions.move({ x: safeX, y: safeY });
975
748
  }
976
749
  }
977
- if (!status.isWindow) {
978
- await moveMouseToRect(status.targetRect);
979
- }
980
750
  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
- }
985
751
  didScroll = true;
986
- await delay2(this.jitterMs(150 + Math.random() * 100, 0.2));
752
+ await delay2(this.jitterMs(20 + Math.random() * 40, 0.2));
987
753
  }
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`);
989
- return { element, didScroll, restore };
754
+ logger5.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
755
+ return { element, didScroll };
990
756
  } catch (error) {
991
757
  logger5.fail("humanScroll", error);
992
758
  throw error;
@@ -996,7 +762,7 @@ var Humanize = {
996
762
  * 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
997
763
  *
998
764
  * @param {import('playwright').Page} page
999
- * @param {string|import('playwright').ElementHandle|import('playwright').Locator} [target] - CSS 选择器、元素句柄或 Locator。如果为空,则点击当前鼠标位置
765
+ * @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
1000
766
  * @param {Object} [options]
1001
767
  * @param {number} [options.reactionDelay=250] - 反应延迟基础值 (ms),实际 ±30% 抖动
1002
768
  * @param {boolean} [options.throwOnMissing=true] - 元素不存在时是否抛出错误
@@ -1005,10 +771,8 @@ var Humanize = {
1005
771
  async humanClick(page, target, options = {}) {
1006
772
  const cursor = $GetCursor(page);
1007
773
  const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
1008
- const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : isLocatorLike(target) ? "Locator" : "ElementHandle";
774
+ const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
1009
775
  logger5.start("humanClick", `target=${targetDesc}`);
1010
- let element = null;
1011
- let owned = false;
1012
776
  const restoreOnce = async () => {
1013
777
  if (restoreOnce.restored) return;
1014
778
  restoreOnce.restored = true;
@@ -1027,15 +791,18 @@ var Humanize = {
1027
791
  logger5.success("humanClick", "Clicked current position");
1028
792
  return true;
1029
793
  }
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}`);
794
+ let element;
795
+ if (typeof target === "string") {
796
+ element = await page.$(target);
797
+ if (!element) {
798
+ if (throwOnMissing) {
799
+ throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${target}`);
800
+ }
801
+ logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
802
+ return false;
1036
803
  }
1037
- logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
1038
- return false;
804
+ } else {
805
+ element = target;
1039
806
  }
1040
807
  if (scrollIfNeeded) {
1041
808
  const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
@@ -1044,7 +811,6 @@ var Humanize = {
1044
811
  const box = await element.boundingBox();
1045
812
  if (!box) {
1046
813
  await restoreOnce();
1047
- await disposeElementHandle(element, owned);
1048
814
  if (throwOnMissing) {
1049
815
  throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
1050
816
  }
@@ -1057,12 +823,10 @@ var Humanize = {
1057
823
  await delay2(this.jitterMs(reactionDelay, 0.4));
1058
824
  await cursor.actions.click();
1059
825
  await restoreOnce();
1060
- await disposeElementHandle(element, owned);
1061
826
  logger5.success("humanClick");
1062
827
  return true;
1063
828
  } catch (error) {
1064
829
  await restoreOnce();
1065
- await disposeElementHandle(element, owned);
1066
830
  logger5.fail("humanClick", error);
1067
831
  throw error;
1068
832
  }
@@ -1199,6 +963,34 @@ var Humanize = {
1199
963
  logger5.fail("warmUpBrowsing", error);
1200
964
  throw error;
1201
965
  }
966
+ },
967
+ /**
968
+ * 自然滚动 - 带惯性、减速效果和随机抖动
969
+ * @param {import('playwright').Page} page
970
+ * @param {'up' | 'down'} [direction='down'] - 滚动方向
971
+ * @param {number} [distance=300] - 总滚动距离基础值 (px),±15% 抖动
972
+ * @param {number} [baseSteps=5] - 分几步完成基础值,±1 随机
973
+ */
974
+ async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
975
+ const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
976
+ const actualDistance = this.jitterMs(distance, 0.15);
977
+ logger5.start("naturalScroll", `dir=${direction}, dist=${actualDistance}, steps=${steps}`);
978
+ const sign = direction === "down" ? 1 : -1;
979
+ const stepDistance = actualDistance / steps;
980
+ try {
981
+ for (let i = 0; i < steps; i++) {
982
+ const factor = 1 - i / steps * 0.5;
983
+ const jitter = 0.9 + Math.random() * 0.2;
984
+ const scrollAmount = stepDistance * factor * sign * jitter;
985
+ await page.mouse.wheel(0, scrollAmount);
986
+ const baseDelay = 60 + i * 25;
987
+ await delay2(this.jitterMs(baseDelay, 0.3));
988
+ }
989
+ logger5.success("naturalScroll");
990
+ } catch (error) {
991
+ logger5.fail("naturalScroll", error);
992
+ throw error;
993
+ }
1202
994
  }
1203
995
  };
1204
996