@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 +345 -67
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +345 -67
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
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=
|
|
604
|
-
* @param {number} [options.minStep=
|
|
605
|
-
* @param {number} [options.maxStep=
|
|
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 {
|
|
609
|
-
|
|
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
|
-
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
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:
|
|
765
|
+
return { code: payload.codes.ZERO_DIMENSIONS, reason: payload.reasons.ZERO_DIMENSIONS };
|
|
630
766
|
}
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
644
|
-
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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 <
|
|
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}
|
|
673
|
-
if (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) {
|
|
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 ===
|
|
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 ===
|
|
687
|
-
if (status.direction ===
|
|
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 ===
|
|
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 ===
|
|
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) *
|
|
703
|
-
const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) *
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
763
|
-
|
|
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
|
}
|