@skrillex1224/playwright-toolkit 2.1.61 → 2.1.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +2 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -59,6 +59,17 @@ var FAILED_KEY_SEPARATOR = "::<@>::";
|
|
|
59
59
|
var PresetOfLiveViewKey = "LIVE_VIEW_SCREENSHOT";
|
|
60
60
|
|
|
61
61
|
// src/internals/logger.js
|
|
62
|
+
var pad = (value, size = 2) => String(value).padStart(size, "0");
|
|
63
|
+
var formatTimestamp = (date = /* @__PURE__ */ new Date()) => {
|
|
64
|
+
const year = date.getFullYear();
|
|
65
|
+
const month = pad(date.getMonth() + 1);
|
|
66
|
+
const day = pad(date.getDate());
|
|
67
|
+
const hours = pad(date.getHours());
|
|
68
|
+
const minutes = pad(date.getMinutes());
|
|
69
|
+
const seconds = pad(date.getSeconds());
|
|
70
|
+
const millis = pad(date.getMilliseconds(), 3);
|
|
71
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${millis}`;
|
|
72
|
+
};
|
|
62
73
|
var formatLine = (prefix, icon, message) => {
|
|
63
74
|
const parts = [];
|
|
64
75
|
if (prefix) parts.push(`[${prefix}]`);
|
|
@@ -112,7 +123,10 @@ var createBaseLogger = (prefix = "", explicitLogger) => {
|
|
|
112
123
|
const dispatch = (methodName, icon, message, color) => {
|
|
113
124
|
const logger11 = resolveLogger(explicitLogger);
|
|
114
125
|
const logFn = resolveLogMethod(logger11, methodName);
|
|
115
|
-
|
|
126
|
+
const timestamp = colorize(`[${formatTimestamp()}]`, ANSI.gray);
|
|
127
|
+
const line = formatLine(name, icon, message);
|
|
128
|
+
const coloredLine = colorize(line, color);
|
|
129
|
+
logFn(`${timestamp} ${coloredLine}`.trim());
|
|
116
130
|
};
|
|
117
131
|
return {
|
|
118
132
|
info: (message) => dispatch("info", "\u{1F4D6}", message, ANSI.cyan),
|
|
@@ -534,59 +548,6 @@ var import_delay2 = __toESM(require("delay"), 1);
|
|
|
534
548
|
var import_ghost_cursor_playwright = require("ghost-cursor-playwright");
|
|
535
549
|
var logger5 = createInternalLogger("Humanize");
|
|
536
550
|
var $CursorWeakMap = /* @__PURE__ */ new WeakMap();
|
|
537
|
-
var VISIBILITY_CODE = {
|
|
538
|
-
VISIBLE: "VISIBLE",
|
|
539
|
-
OUT_OF_VIEWPORT: "OUT_OF_VIEWPORT",
|
|
540
|
-
OBSTRUCTED: "OBSTRUCTED",
|
|
541
|
-
ZERO_DIMENSIONS: "ZERO_DIMENSIONS",
|
|
542
|
-
HIDDEN: "HIDDEN",
|
|
543
|
-
DETACHED: "DETACHED"
|
|
544
|
-
};
|
|
545
|
-
var VISIBILITY_REASON = {
|
|
546
|
-
OUT_OF_VIEWPORT: "\u4E0D\u5728\u89C6\u53E3\u5185",
|
|
547
|
-
OBSTRUCTED: "\u88AB\u906E\u6321",
|
|
548
|
-
ZERO_DIMENSIONS: "\u5C3A\u5BF8\u4E3A\u96F6",
|
|
549
|
-
HIDDEN: "\u5143\u7D20\u4E0D\u53EF\u89C1",
|
|
550
|
-
DETACHED: "\u5143\u7D20\u5DF2\u88AB\u79FB\u9664"
|
|
551
|
-
};
|
|
552
|
-
var SCROLL_DIRECTION = {
|
|
553
|
-
UP: "up",
|
|
554
|
-
DOWN: "down",
|
|
555
|
-
UNKNOWN: "unknown"
|
|
556
|
-
};
|
|
557
|
-
var DEFAULT_SCROLL_OPTIONS = {
|
|
558
|
-
maxSteps: 12,
|
|
559
|
-
minStep: 220,
|
|
560
|
-
maxStep: 600,
|
|
561
|
-
maxDurationMs: 5e3
|
|
562
|
-
};
|
|
563
|
-
var SAME_POSITION_THRESHOLD_PX = 2;
|
|
564
|
-
var SAME_POSITION_LIMIT = 3;
|
|
565
|
-
var SAFE_MOUSE_JITTER_PX = 100;
|
|
566
|
-
var isElementHandleLike = (value) => value && typeof value.evaluate === "function" && typeof value.boundingBox === "function";
|
|
567
|
-
var isLocatorLike = (value) => value && typeof value.elementHandle === "function";
|
|
568
|
-
async function resolveElementHandle(page, target) {
|
|
569
|
-
if (!target) return { element: null, owned: false };
|
|
570
|
-
if (typeof target === "string") {
|
|
571
|
-
const element = await page.$(target);
|
|
572
|
-
return { element, owned: true };
|
|
573
|
-
}
|
|
574
|
-
if (isLocatorLike(target)) {
|
|
575
|
-
const element = await target.elementHandle();
|
|
576
|
-
return { element, owned: true };
|
|
577
|
-
}
|
|
578
|
-
if (isElementHandleLike(target)) {
|
|
579
|
-
return { element: target, owned: false };
|
|
580
|
-
}
|
|
581
|
-
return { element: null, owned: false };
|
|
582
|
-
}
|
|
583
|
-
async function disposeElementHandle(element, owned) {
|
|
584
|
-
if (!element || !owned || typeof element.dispose !== "function") return;
|
|
585
|
-
try {
|
|
586
|
-
await element.dispose();
|
|
587
|
-
} catch {
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
551
|
function $GetCursor(page) {
|
|
591
552
|
const cursor = $CursorWeakMap.get(page);
|
|
592
553
|
if (!cursor) {
|
|
@@ -667,353 +628,158 @@ var Humanize = {
|
|
|
667
628
|
* 返回 restore 方法,用于将滚动容器恢复到原位置
|
|
668
629
|
*
|
|
669
630
|
* @param {import('playwright').Page} page
|
|
670
|
-
* @param {string|import('playwright').ElementHandle
|
|
631
|
+
* @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
|
|
671
632
|
* @param {Object} [options]
|
|
672
|
-
* @param {number} [options.maxSteps=
|
|
673
|
-
* @param {number} [options.minStep=
|
|
674
|
-
* @param {number} [options.maxStep=
|
|
675
|
-
* @param {number} [options.maxDurationMs
|
|
633
|
+
* @param {number} [options.maxSteps=14] - 最大滚动步数
|
|
634
|
+
* @param {number} [options.minStep=260] - 单次滚动最小步长
|
|
635
|
+
* @param {number} [options.maxStep=800] - 单次滚动最大步长
|
|
636
|
+
* @param {number} [options.maxDurationMs] - 最长耗时上限 (默认随 maxSteps 估算)
|
|
676
637
|
*/
|
|
677
638
|
async humanScroll(page, target, options = {}) {
|
|
678
639
|
const {
|
|
679
|
-
maxSteps =
|
|
680
|
-
minStep =
|
|
681
|
-
maxStep =
|
|
682
|
-
maxDurationMs =
|
|
640
|
+
maxSteps = 14,
|
|
641
|
+
minStep = 260,
|
|
642
|
+
maxStep = 800,
|
|
643
|
+
maxDurationMs = maxSteps * 220 + 800
|
|
683
644
|
} = options;
|
|
684
|
-
const targetDesc = typeof target === "string" ? target :
|
|
685
|
-
logger5.
|
|
686
|
-
|
|
687
|
-
if (
|
|
688
|
-
|
|
689
|
-
|
|
645
|
+
const targetDesc = typeof target === "string" ? target : "ElementHandle";
|
|
646
|
+
logger5.start("humanScroll", `target=${targetDesc}`);
|
|
647
|
+
let element;
|
|
648
|
+
if (typeof target === "string") {
|
|
649
|
+
element = await page.$(target);
|
|
650
|
+
if (!element) {
|
|
651
|
+
logger5.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
|
|
652
|
+
return { element: null, didScroll: false };
|
|
653
|
+
}
|
|
654
|
+
} else {
|
|
655
|
+
element = target;
|
|
690
656
|
}
|
|
691
657
|
const cursor = $GetCursor(page);
|
|
692
658
|
let didScroll = false;
|
|
693
|
-
let lastRect = null;
|
|
694
|
-
let samePositionCount = 0;
|
|
695
|
-
const startAt = Date.now();
|
|
696
|
-
const visibilityPayload = {
|
|
697
|
-
codes: VISIBILITY_CODE,
|
|
698
|
-
reasons: VISIBILITY_REASON,
|
|
699
|
-
directions: SCROLL_DIRECTION
|
|
700
|
-
};
|
|
701
|
-
const scrollDeltas = /* @__PURE__ */ new Map();
|
|
702
|
-
const scrollTargetRects = /* @__PURE__ */ new Map();
|
|
703
|
-
let windowIndex = null;
|
|
704
|
-
const moveMouseToRect = async (rect) => {
|
|
705
|
-
if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top)) return false;
|
|
706
|
-
const width = Number.isFinite(rect.width) ? rect.width : Math.max(0, (rect.right ?? rect.left) - rect.left);
|
|
707
|
-
const height = Number.isFinite(rect.height) ? rect.height : Math.max(0, (rect.bottom ?? rect.top) - rect.top);
|
|
708
|
-
const viewSize = page.viewportSize();
|
|
709
|
-
const viewW = viewSize?.width ?? null;
|
|
710
|
-
const viewH = viewSize?.height ?? null;
|
|
711
|
-
if (viewW && viewH) {
|
|
712
|
-
const rectRight = rect.right ?? rect.left + width;
|
|
713
|
-
const rectBottom = rect.bottom ?? rect.top + height;
|
|
714
|
-
if (rectBottom < 0 || rect.top > viewH || rectRight < 0 || rect.left > viewW) {
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
const rawX = rect.left + width / 2;
|
|
719
|
-
const rawY = rect.top + height / 2;
|
|
720
|
-
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) return false;
|
|
721
|
-
const padding = 6;
|
|
722
|
-
let x = rawX;
|
|
723
|
-
let y = rawY;
|
|
724
|
-
if (viewW && viewH) {
|
|
725
|
-
const maxX = Math.max(padding, viewW - padding);
|
|
726
|
-
const maxY = Math.max(padding, viewH - padding);
|
|
727
|
-
x = Math.min(Math.max(rawX, padding), maxX);
|
|
728
|
-
y = Math.min(Math.max(rawY, padding), maxY);
|
|
729
|
-
}
|
|
730
|
-
x += (Math.random() - 0.5) * 4;
|
|
731
|
-
y += (Math.random() - 0.5) * 4;
|
|
732
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
|
|
733
|
-
await page.mouse.move(x, y, { steps: 6 }).catch(() => {
|
|
734
|
-
});
|
|
735
|
-
return true;
|
|
736
|
-
};
|
|
737
|
-
const wheelByChunks = async (delta, rect, isWindowTarget) => {
|
|
738
|
-
let remaining = delta;
|
|
739
|
-
const stepBase = Math.min(maxStep, 400);
|
|
740
|
-
while (Math.abs(remaining) > 1) {
|
|
741
|
-
const step = Math.abs(remaining) > stepBase ? Math.sign(remaining) * stepBase : remaining;
|
|
742
|
-
if (!isWindowTarget) {
|
|
743
|
-
await moveMouseToRect(rect);
|
|
744
|
-
}
|
|
745
|
-
await page.mouse.wheel(0, step);
|
|
746
|
-
remaining -= step;
|
|
747
|
-
await (0, import_delay2.default)(this.jitterMs(60, 0.35));
|
|
748
|
-
}
|
|
749
|
-
};
|
|
750
|
-
const restore = async () => {
|
|
751
|
-
if (scrollDeltas.size === 0) return;
|
|
752
|
-
try {
|
|
753
|
-
for (const [index, delta] of scrollDeltas.entries()) {
|
|
754
|
-
if (!delta) continue;
|
|
755
|
-
const rect = scrollTargetRects.get(index);
|
|
756
|
-
const isWindowTarget = windowIndex !== null && index === windowIndex;
|
|
757
|
-
await wheelByChunks(-delta, rect, isWindowTarget);
|
|
758
|
-
}
|
|
759
|
-
} catch (err) {
|
|
760
|
-
logger5.warn(`humanScroll | restore failed: ${err?.message || err}`);
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
659
|
const checkVisibility = async () => {
|
|
764
|
-
return await element.evaluate((el
|
|
765
|
-
const overflowRe = /(auto|scroll|overlay)/i;
|
|
766
|
-
const isScrollable = (node) => {
|
|
767
|
-
if (!node || node === document.body || node === document.documentElement) return false;
|
|
768
|
-
const style2 = window.getComputedStyle(node);
|
|
769
|
-
const overflowY = style2.overflowY;
|
|
770
|
-
return overflowRe.test(overflowY) && node.scrollHeight > node.clientHeight + 1;
|
|
771
|
-
};
|
|
772
|
-
const getScrollableAncestors = (node) => {
|
|
773
|
-
const list = [];
|
|
774
|
-
let current = node?.parentElement;
|
|
775
|
-
while (current && current !== document.body && current !== document.documentElement) {
|
|
776
|
-
if (isScrollable(current)) {
|
|
777
|
-
list.push(current);
|
|
778
|
-
}
|
|
779
|
-
current = current.parentElement;
|
|
780
|
-
}
|
|
781
|
-
return list;
|
|
782
|
-
};
|
|
783
|
-
if (!el || !el.isConnected) {
|
|
784
|
-
return { code: payload.codes.DETACHED, reason: payload.reasons.DETACHED };
|
|
785
|
-
}
|
|
786
|
-
const style = window.getComputedStyle(el);
|
|
787
|
-
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) {
|
|
788
|
-
return { code: payload.codes.HIDDEN, reason: payload.reasons.HIDDEN };
|
|
789
|
-
}
|
|
660
|
+
return await element.evaluate((el) => {
|
|
790
661
|
const rect = el.getBoundingClientRect();
|
|
791
662
|
if (!rect || rect.width === 0 || rect.height === 0) {
|
|
792
|
-
return { code:
|
|
663
|
+
return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6" };
|
|
793
664
|
}
|
|
794
|
-
const hasFixedAncestor = (() => {
|
|
795
|
-
let node = el;
|
|
796
|
-
while (node && node !== document.body && node !== document.documentElement) {
|
|
797
|
-
const position = window.getComputedStyle(node).position;
|
|
798
|
-
if (position === "fixed") return true;
|
|
799
|
-
node = node.parentElement;
|
|
800
|
-
}
|
|
801
|
-
return false;
|
|
802
|
-
})();
|
|
803
|
-
const ancestors = getScrollableAncestors(el);
|
|
804
|
-
const targets = ancestors.map((node) => ({
|
|
805
|
-
isWindow: false,
|
|
806
|
-
rect: node.getBoundingClientRect(),
|
|
807
|
-
canScroll: node.scrollHeight > node.clientHeight + 1
|
|
808
|
-
}));
|
|
809
|
-
const windowTarget = {
|
|
810
|
-
isWindow: true,
|
|
811
|
-
rect: { top: 0, left: 0, right: window.innerWidth, bottom: window.innerHeight, width: window.innerWidth, height: window.innerHeight },
|
|
812
|
-
canScroll: document.documentElement.scrollHeight > window.innerHeight
|
|
813
|
-
};
|
|
814
|
-
targets.push(windowTarget);
|
|
815
|
-
const windowIndex2 = targets.length - 1;
|
|
816
665
|
const cx = rect.left + rect.width / 2;
|
|
817
666
|
const cy = rect.top + rect.height / 2;
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
const outOfParent = c.bottom < p.top || c.top > p.bottom || c.right < p.left || c.left > p.right;
|
|
826
|
-
if (outOfParent) {
|
|
827
|
-
idx += 1;
|
|
828
|
-
continue;
|
|
829
|
-
}
|
|
830
|
-
break;
|
|
831
|
-
}
|
|
832
|
-
return idx;
|
|
833
|
-
};
|
|
834
|
-
let outIndex = -1;
|
|
835
|
-
let direction = payload.directions.UNKNOWN;
|
|
836
|
-
for (let i = 0; i < targets.length; i += 1) {
|
|
837
|
-
const bounds = targets[i].rect;
|
|
838
|
-
if (cy < bounds.top) {
|
|
839
|
-
outIndex = i;
|
|
840
|
-
direction = payload.directions.UP;
|
|
841
|
-
break;
|
|
842
|
-
}
|
|
843
|
-
if (cy > bounds.bottom) {
|
|
844
|
-
outIndex = i;
|
|
845
|
-
direction = payload.directions.DOWN;
|
|
846
|
-
break;
|
|
847
|
-
}
|
|
848
|
-
if (cx < bounds.left || cx > bounds.right) {
|
|
849
|
-
outIndex = i;
|
|
850
|
-
direction = payload.directions.UNKNOWN;
|
|
667
|
+
const viewH = window.innerHeight;
|
|
668
|
+
const viewW = window.innerWidth;
|
|
669
|
+
let isFixed = false;
|
|
670
|
+
for (let node = el; node && node !== document.body; node = node.parentElement) {
|
|
671
|
+
const style = window.getComputedStyle(node);
|
|
672
|
+
if (style && style.position === "fixed") {
|
|
673
|
+
isFixed = true;
|
|
851
674
|
break;
|
|
852
675
|
}
|
|
853
676
|
}
|
|
854
|
-
if (
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
if (!target2.isWindow) {
|
|
858
|
-
const targetRect = target2.rect;
|
|
859
|
-
const outOfWindow = targetRect.bottom < 0 || targetRect.top > window.innerHeight || targetRect.right < 0 || targetRect.left > window.innerWidth;
|
|
860
|
-
if (outOfWindow) {
|
|
861
|
-
scrollTargetIndex = windowIndex2;
|
|
862
|
-
target2 = windowTarget;
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
return {
|
|
866
|
-
code: payload.codes.OUT_OF_VIEWPORT,
|
|
867
|
-
reason: payload.reasons.OUT_OF_VIEWPORT,
|
|
868
|
-
direction,
|
|
869
|
-
rect,
|
|
870
|
-
viewH: target2.rect.height,
|
|
871
|
-
viewW: target2.rect.width,
|
|
872
|
-
cy,
|
|
873
|
-
cx,
|
|
874
|
-
scrollTargetIndex,
|
|
875
|
-
isWindow: target2.isWindow,
|
|
876
|
-
canScroll: target2.canScroll,
|
|
877
|
-
isFixed: hasFixedAncestor,
|
|
878
|
-
targetRect: target2.rect,
|
|
879
|
-
windowIndex: windowIndex2
|
|
880
|
-
};
|
|
677
|
+
if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
|
|
678
|
+
const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
|
|
679
|
+
return { code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy, viewH, isFixed };
|
|
881
680
|
}
|
|
882
681
|
const pointElement = document.elementFromPoint(cx, cy);
|
|
883
682
|
if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
|
|
884
|
-
let fallbackIndex = promoteToVisibleTarget(targets.length > 1 ? 0 : targets.length - 1);
|
|
885
|
-
let target2 = targets[fallbackIndex];
|
|
886
|
-
if (!target2.isWindow) {
|
|
887
|
-
const targetRect = target2.rect;
|
|
888
|
-
const outOfWindow = targetRect.bottom < 0 || targetRect.top > window.innerHeight || targetRect.right < 0 || targetRect.left > window.innerWidth;
|
|
889
|
-
if (outOfWindow) {
|
|
890
|
-
fallbackIndex = windowIndex2;
|
|
891
|
-
target2 = windowTarget;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
683
|
return {
|
|
895
|
-
code:
|
|
896
|
-
reason:
|
|
684
|
+
code: "OBSTRUCTED",
|
|
685
|
+
reason: "\u88AB\u906E\u6321",
|
|
897
686
|
obstruction: {
|
|
898
687
|
tag: pointElement.tagName,
|
|
899
688
|
id: pointElement.id,
|
|
900
689
|
className: pointElement.className
|
|
901
690
|
},
|
|
902
|
-
rect,
|
|
903
|
-
viewH: target2.rect.height,
|
|
904
|
-
viewW: target2.rect.width,
|
|
905
691
|
cy,
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
canScroll: target2.canScroll,
|
|
910
|
-
isFixed: hasFixedAncestor,
|
|
911
|
-
targetRect: target2.rect,
|
|
912
|
-
windowIndex: windowIndex2
|
|
692
|
+
// Return Center Y for smart direction calculation
|
|
693
|
+
viewH,
|
|
694
|
+
isFixed
|
|
913
695
|
};
|
|
914
696
|
}
|
|
915
|
-
return {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
targetRect: windowTarget.rect,
|
|
927
|
-
windowIndex: windowIndex2
|
|
697
|
+
return { code: "VISIBLE", isFixed };
|
|
698
|
+
});
|
|
699
|
+
};
|
|
700
|
+
const getScrollableRect = async () => {
|
|
701
|
+
return await element.evaluate((el) => {
|
|
702
|
+
const isScrollable = (node) => {
|
|
703
|
+
const style = window.getComputedStyle(node);
|
|
704
|
+
if (!style) return false;
|
|
705
|
+
const overflowY = style.overflowY;
|
|
706
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
707
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
928
708
|
};
|
|
929
|
-
|
|
709
|
+
let current = el;
|
|
710
|
+
while (current && current !== document.body) {
|
|
711
|
+
if (isScrollable(current)) {
|
|
712
|
+
const rect = current.getBoundingClientRect();
|
|
713
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
714
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
current = current.parentElement;
|
|
718
|
+
}
|
|
719
|
+
return null;
|
|
720
|
+
});
|
|
930
721
|
};
|
|
722
|
+
const startTime = Date.now();
|
|
931
723
|
try {
|
|
932
724
|
for (let i = 0; i < maxSteps; i++) {
|
|
933
|
-
if (Date.now() -
|
|
934
|
-
logger5.warn(`humanScroll | \
|
|
935
|
-
return { element, didScroll
|
|
725
|
+
if (Date.now() - startTime > maxDurationMs) {
|
|
726
|
+
logger5.warn(`humanScroll | \u8D85\u65F6\u4FDD\u62A4\u89E6\u53D1 (${maxDurationMs}ms)`);
|
|
727
|
+
return { element, didScroll };
|
|
936
728
|
}
|
|
937
729
|
const status = await checkVisibility();
|
|
938
|
-
if (
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
if (typeof status.scrollTargetIndex === "number" && status.targetRect) {
|
|
942
|
-
scrollTargetRects.set(status.scrollTargetIndex, status.targetRect);
|
|
943
|
-
}
|
|
944
|
-
if (status.code === VISIBILITY_CODE.VISIBLE) {
|
|
945
|
-
logger5.info("humanScroll | \u5143\u7D20\u5DF2\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
|
|
946
|
-
return { element, didScroll, restore };
|
|
947
|
-
}
|
|
948
|
-
if (status.code === VISIBILITY_CODE.DETACHED || status.code === VISIBILITY_CODE.HIDDEN || status.code === VISIBILITY_CODE.ZERO_DIMENSIONS) {
|
|
949
|
-
logger5.warn(`humanScroll | ${status.reason || "\u5143\u7D20\u4E0D\u53EF\u7528"}`);
|
|
950
|
-
return { element, didScroll, restore };
|
|
951
|
-
}
|
|
952
|
-
const directionStr = status.direction ? `(${status.direction})` : "";
|
|
953
|
-
logger5.info(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${directionStr}`);
|
|
954
|
-
const currentRect = status.rect;
|
|
955
|
-
if (i > 0 && lastRect && currentRect) {
|
|
956
|
-
const dy = Math.abs(currentRect.top - lastRect.top);
|
|
957
|
-
const dx = Math.abs(currentRect.left - lastRect.left);
|
|
958
|
-
if (dy < SAME_POSITION_THRESHOLD_PX && dx < SAME_POSITION_THRESHOLD_PX) {
|
|
959
|
-
samePositionCount++;
|
|
960
|
-
logger5.info(`humanScroll | \u26A0\uFE0F \u8B66\u544A: \u6EDA\u52A8\u540E\u4F4D\u7F6E\u672A\u53D8 (${samePositionCount}/${SAME_POSITION_LIMIT}). dy=${dy}`);
|
|
961
|
-
if (samePositionCount >= SAME_POSITION_LIMIT) {
|
|
962
|
-
logger5.warn("humanScroll | \u26A0\uFE0F \u68C0\u6D4B\u5230\u65E0\u6548\u6EDA\u52A8 (\u5143\u7D20\u53EF\u80FD\u662F Fixed \u6216\u4F4D\u4E8E\u4E0D\u53EF\u6EDA\u52A8\u7684\u5BB9\u5668)\uFF0C\u5F3A\u5236\u505C\u6B62\uFF0C\u5C1D\u8BD5\u76F4\u63A5\u4EA4\u4E92");
|
|
963
|
-
return { element, didScroll, restore };
|
|
964
|
-
}
|
|
730
|
+
if (status.code === "VISIBLE") {
|
|
731
|
+
if (status.isFixed) {
|
|
732
|
+
logger5.info("humanScroll | fixed \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
965
733
|
} else {
|
|
966
|
-
|
|
734
|
+
logger5.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
|
|
967
735
|
}
|
|
736
|
+
logger5.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
737
|
+
return { element, didScroll };
|
|
968
738
|
}
|
|
969
|
-
|
|
970
|
-
if (status.code ===
|
|
971
|
-
logger5.
|
|
972
|
-
}
|
|
973
|
-
if (status.code === VISIBILITY_CODE.OBSTRUCTED && status.isFixed) {
|
|
974
|
-
logger5.warn("humanScroll | \u5143\u7D20\u4E3A Fixed \u4E14\u88AB\u906E\u6321\uFF0C\u505C\u6B62\u6EDA\u52A8\u5C1D\u8BD5");
|
|
975
|
-
return { element, didScroll, restore };
|
|
739
|
+
logger5.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${status.direction ? `(${status.direction})` : ""}`);
|
|
740
|
+
if (status.code === "OBSTRUCTED" && status.obstruction) {
|
|
741
|
+
logger5.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
|
|
976
742
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
743
|
+
const scrollRect = await getScrollableRect();
|
|
744
|
+
if (!scrollRect && status.isFixed) {
|
|
745
|
+
logger5.warn("humanScroll | fixed \u5BB9\u5668\u5185\u4E14\u65E0\u53EF\u6EDA\u52A8\u7956\u5148\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
746
|
+
return { element, didScroll };
|
|
980
747
|
}
|
|
748
|
+
const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
|
|
749
|
+
const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
|
|
981
750
|
let deltaY = 0;
|
|
982
|
-
if (status.code ===
|
|
983
|
-
if (status.direction ===
|
|
984
|
-
deltaY =
|
|
985
|
-
} else if (status.direction ===
|
|
986
|
-
deltaY = -(
|
|
751
|
+
if (status.code === "OUT_OF_VIEWPORT") {
|
|
752
|
+
if (status.direction === "down") {
|
|
753
|
+
deltaY = stepMin + Math.random() * (stepMax - stepMin);
|
|
754
|
+
} else if (status.direction === "up") {
|
|
755
|
+
deltaY = -(stepMin + Math.random() * (stepMax - stepMin));
|
|
987
756
|
} else {
|
|
988
757
|
deltaY = 100;
|
|
989
758
|
}
|
|
990
|
-
} else if (status.code ===
|
|
991
|
-
const
|
|
992
|
-
const
|
|
993
|
-
|
|
759
|
+
} else if (status.code === "OBSTRUCTED") {
|
|
760
|
+
const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
761
|
+
const isBottomHalf = status.cy > halfY;
|
|
762
|
+
const direction = isBottomHalf ? 1 : -1;
|
|
763
|
+
deltaY = direction * (stepMin + Math.random() * 50);
|
|
994
764
|
}
|
|
995
|
-
if (i === 0
|
|
765
|
+
if (i === 0) {
|
|
996
766
|
const viewSize = page.viewportSize();
|
|
997
|
-
if (
|
|
998
|
-
const safeX =
|
|
999
|
-
const safeY =
|
|
1000
|
-
await cursor.actions.move({ x: safeX, y: safeY })
|
|
1001
|
-
|
|
767
|
+
if (scrollRect) {
|
|
768
|
+
const safeX = scrollRect.x + scrollRect.width * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.width * 0.4);
|
|
769
|
+
const safeY = scrollRect.y + scrollRect.height * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.height * 0.4);
|
|
770
|
+
await cursor.actions.move({ x: safeX, y: safeY });
|
|
771
|
+
} else if (viewSize) {
|
|
772
|
+
const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 80;
|
|
773
|
+
const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 80;
|
|
774
|
+
await cursor.actions.move({ x: safeX, y: safeY });
|
|
1002
775
|
}
|
|
1003
776
|
}
|
|
1004
|
-
if (!status.isWindow) {
|
|
1005
|
-
await moveMouseToRect(status.targetRect);
|
|
1006
|
-
}
|
|
1007
777
|
await page.mouse.wheel(0, deltaY);
|
|
1008
|
-
if (typeof status.scrollTargetIndex === "number") {
|
|
1009
|
-
const prev = scrollDeltas.get(status.scrollTargetIndex) || 0;
|
|
1010
|
-
scrollDeltas.set(status.scrollTargetIndex, prev + deltaY);
|
|
1011
|
-
}
|
|
1012
778
|
didScroll = true;
|
|
1013
|
-
await (0, import_delay2.default)(this.jitterMs(
|
|
779
|
+
await (0, import_delay2.default)(this.jitterMs(20 + Math.random() * 40, 0.2));
|
|
1014
780
|
}
|
|
1015
|
-
logger5.warn(`humanScroll | \
|
|
1016
|
-
return { element, didScroll
|
|
781
|
+
logger5.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
|
|
782
|
+
return { element, didScroll };
|
|
1017
783
|
} catch (error) {
|
|
1018
784
|
logger5.fail("humanScroll", error);
|
|
1019
785
|
throw error;
|
|
@@ -1023,7 +789,7 @@ var Humanize = {
|
|
|
1023
789
|
* 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
|
|
1024
790
|
*
|
|
1025
791
|
* @param {import('playwright').Page} page
|
|
1026
|
-
* @param {string|import('playwright').ElementHandle
|
|
792
|
+
* @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
|
|
1027
793
|
* @param {Object} [options]
|
|
1028
794
|
* @param {number} [options.reactionDelay=250] - 反应延迟基础值 (ms),实际 ±30% 抖动
|
|
1029
795
|
* @param {boolean} [options.throwOnMissing=true] - 元素不存在时是否抛出错误
|
|
@@ -1032,10 +798,8 @@ var Humanize = {
|
|
|
1032
798
|
async humanClick(page, target, options = {}) {
|
|
1033
799
|
const cursor = $GetCursor(page);
|
|
1034
800
|
const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
|
|
1035
|
-
const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target :
|
|
801
|
+
const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
|
|
1036
802
|
logger5.start("humanClick", `target=${targetDesc}`);
|
|
1037
|
-
let element = null;
|
|
1038
|
-
let owned = false;
|
|
1039
803
|
const restoreOnce = async () => {
|
|
1040
804
|
if (restoreOnce.restored) return;
|
|
1041
805
|
restoreOnce.restored = true;
|
|
@@ -1054,15 +818,18 @@ var Humanize = {
|
|
|
1054
818
|
logger5.success("humanClick", "Clicked current position");
|
|
1055
819
|
return true;
|
|
1056
820
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
821
|
+
let element;
|
|
822
|
+
if (typeof target === "string") {
|
|
823
|
+
element = await page.$(target);
|
|
824
|
+
if (!element) {
|
|
825
|
+
if (throwOnMissing) {
|
|
826
|
+
throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${target}`);
|
|
827
|
+
}
|
|
828
|
+
logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
|
|
829
|
+
return false;
|
|
1063
830
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
831
|
+
} else {
|
|
832
|
+
element = target;
|
|
1066
833
|
}
|
|
1067
834
|
if (scrollIfNeeded) {
|
|
1068
835
|
const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
|
|
@@ -1071,7 +838,6 @@ var Humanize = {
|
|
|
1071
838
|
const box = await element.boundingBox();
|
|
1072
839
|
if (!box) {
|
|
1073
840
|
await restoreOnce();
|
|
1074
|
-
await disposeElementHandle(element, owned);
|
|
1075
841
|
if (throwOnMissing) {
|
|
1076
842
|
throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
1077
843
|
}
|
|
@@ -1084,12 +850,10 @@ var Humanize = {
|
|
|
1084
850
|
await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
|
|
1085
851
|
await cursor.actions.click();
|
|
1086
852
|
await restoreOnce();
|
|
1087
|
-
await disposeElementHandle(element, owned);
|
|
1088
853
|
logger5.success("humanClick");
|
|
1089
854
|
return true;
|
|
1090
855
|
} catch (error) {
|
|
1091
856
|
await restoreOnce();
|
|
1092
|
-
await disposeElementHandle(element, owned);
|
|
1093
857
|
logger5.fail("humanClick", error);
|
|
1094
858
|
throw error;
|
|
1095
859
|
}
|
|
@@ -1226,6 +990,34 @@ var Humanize = {
|
|
|
1226
990
|
logger5.fail("warmUpBrowsing", error);
|
|
1227
991
|
throw error;
|
|
1228
992
|
}
|
|
993
|
+
},
|
|
994
|
+
/**
|
|
995
|
+
* 自然滚动 - 带惯性、减速效果和随机抖动
|
|
996
|
+
* @param {import('playwright').Page} page
|
|
997
|
+
* @param {'up' | 'down'} [direction='down'] - 滚动方向
|
|
998
|
+
* @param {number} [distance=300] - 总滚动距离基础值 (px),±15% 抖动
|
|
999
|
+
* @param {number} [baseSteps=5] - 分几步完成基础值,±1 随机
|
|
1000
|
+
*/
|
|
1001
|
+
async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
|
|
1002
|
+
const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
|
|
1003
|
+
const actualDistance = this.jitterMs(distance, 0.15);
|
|
1004
|
+
logger5.start("naturalScroll", `dir=${direction}, dist=${actualDistance}, steps=${steps}`);
|
|
1005
|
+
const sign = direction === "down" ? 1 : -1;
|
|
1006
|
+
const stepDistance = actualDistance / steps;
|
|
1007
|
+
try {
|
|
1008
|
+
for (let i = 0; i < steps; i++) {
|
|
1009
|
+
const factor = 1 - i / steps * 0.5;
|
|
1010
|
+
const jitter = 0.9 + Math.random() * 0.2;
|
|
1011
|
+
const scrollAmount = stepDistance * factor * sign * jitter;
|
|
1012
|
+
await page.mouse.wheel(0, scrollAmount);
|
|
1013
|
+
const baseDelay = 60 + i * 25;
|
|
1014
|
+
await (0, import_delay2.default)(this.jitterMs(baseDelay, 0.3));
|
|
1015
|
+
}
|
|
1016
|
+
logger5.success("naturalScroll");
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
logger5.fail("naturalScroll", error);
|
|
1019
|
+
throw error;
|
|
1020
|
+
}
|
|
1229
1021
|
}
|
|
1230
1022
|
};
|
|
1231
1023
|
|