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