@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/browser.js +15 -1
- package/dist/browser.js.map +2 -2
- package/dist/index.cjs +157 -365
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +157 -365
- package/dist/index.js.map +3 -3
- package/index.d.ts +3 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
604
|
+
* @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
|
|
644
605
|
* @param {Object} [options]
|
|
645
|
-
* @param {number} [options.maxSteps=
|
|
646
|
-
* @param {number} [options.minStep=
|
|
647
|
-
* @param {number} [options.maxStep=
|
|
648
|
-
* @param {number} [options.maxDurationMs
|
|
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 =
|
|
653
|
-
minStep =
|
|
654
|
-
maxStep =
|
|
655
|
-
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 :
|
|
658
|
-
logger5.
|
|
659
|
-
|
|
660
|
-
if (
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
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:
|
|
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
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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 (
|
|
828
|
-
|
|
829
|
-
|
|
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:
|
|
869
|
-
reason:
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
-
|
|
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() -
|
|
907
|
-
logger5.warn(`humanScroll | \
|
|
908
|
-
return { element, didScroll
|
|
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 (
|
|
912
|
-
|
|
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
|
-
|
|
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
|
-
|
|
943
|
-
if (status.code ===
|
|
944
|
-
logger5.
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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 ===
|
|
956
|
-
if (status.direction ===
|
|
957
|
-
deltaY =
|
|
958
|
-
} else if (status.direction ===
|
|
959
|
-
deltaY = -(
|
|
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 ===
|
|
964
|
-
const
|
|
965
|
-
const
|
|
966
|
-
|
|
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
|
|
738
|
+
if (i === 0) {
|
|
969
739
|
const viewSize = page.viewportSize();
|
|
970
|
-
if (
|
|
971
|
-
const safeX =
|
|
972
|
-
const safeY =
|
|
973
|
-
await cursor.actions.move({ x: safeX, y: safeY })
|
|
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(
|
|
752
|
+
await delay2(this.jitterMs(20 + Math.random() * 40, 0.2));
|
|
987
753
|
}
|
|
988
|
-
logger5.warn(`humanScroll | \
|
|
989
|
-
return { element, didScroll
|
|
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
|
|
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 :
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
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
|
|