@skrillex1224/playwright-toolkit 2.1.225 → 2.1.226
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 +452 -46
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +452 -46
- package/dist/index.js.map +3 -3
- package/index.d.ts +4 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2120,7 +2120,7 @@ var Humanize = {
|
|
|
2120
2120
|
},
|
|
2121
2121
|
/**
|
|
2122
2122
|
* 初始化页面的 Ghost Cursor(必须在使用其他 cursor 相关方法前调用)
|
|
2123
|
-
*
|
|
2123
|
+
*
|
|
2124
2124
|
* @param {import('playwright').Page} page
|
|
2125
2125
|
* @returns {Promise<void>}
|
|
2126
2126
|
*/
|
|
@@ -2136,7 +2136,7 @@ var Humanize = {
|
|
|
2136
2136
|
},
|
|
2137
2137
|
/**
|
|
2138
2138
|
* 人类化鼠标移动 - 使用 ghost-cursor 移动到指定位置或元素
|
|
2139
|
-
*
|
|
2139
|
+
*
|
|
2140
2140
|
* @param {import('playwright').Page} page
|
|
2141
2141
|
* @param {string|{x: number, y: number}|import('playwright').ElementHandle} target - CSS选择器、坐标对象或元素句柄
|
|
2142
2142
|
*/
|
|
@@ -2178,7 +2178,7 @@ var Humanize = {
|
|
|
2178
2178
|
/**
|
|
2179
2179
|
* 渐进式滚动到元素可见(仅处理 Y 轴滚动)
|
|
2180
2180
|
* 返回 restore 方法,用于将滚动容器恢复到原位置
|
|
2181
|
-
*
|
|
2181
|
+
*
|
|
2182
2182
|
* @param {import('playwright').Page} page
|
|
2183
2183
|
* @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
|
|
2184
2184
|
* @param {Object} [options]
|
|
@@ -2249,7 +2249,7 @@ var Humanize = {
|
|
|
2249
2249
|
return { code: "VISIBLE", isFixed };
|
|
2250
2250
|
});
|
|
2251
2251
|
};
|
|
2252
|
-
const
|
|
2252
|
+
const getScrollableRect2 = async () => {
|
|
2253
2253
|
return await element.evaluate((el) => {
|
|
2254
2254
|
const isScrollable = (node) => {
|
|
2255
2255
|
const style = window.getComputedStyle(node);
|
|
@@ -2292,7 +2292,7 @@ var Humanize = {
|
|
|
2292
2292
|
if (status.code === "OBSTRUCTED" && status.obstruction) {
|
|
2293
2293
|
logger6.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
|
|
2294
2294
|
}
|
|
2295
|
-
const scrollRect = await
|
|
2295
|
+
const scrollRect = await getScrollableRect2();
|
|
2296
2296
|
if (!scrollRect && status.isFixed) {
|
|
2297
2297
|
logger6.warn("humanScroll | fixed \u5BB9\u5668\u5185\u4E14\u65E0\u53EF\u6EDA\u52A8\u7956\u5148\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
2298
2298
|
return { element, didScroll };
|
|
@@ -2339,7 +2339,7 @@ var Humanize = {
|
|
|
2339
2339
|
},
|
|
2340
2340
|
/**
|
|
2341
2341
|
* 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
|
|
2342
|
-
*
|
|
2342
|
+
*
|
|
2343
2343
|
* @param {import('playwright').Page} page
|
|
2344
2344
|
* @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
|
|
2345
2345
|
* @param {Object} [options]
|
|
@@ -2400,9 +2400,7 @@ var Humanize = {
|
|
|
2400
2400
|
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
|
|
2401
2401
|
await cursor.actions.move({ x, y });
|
|
2402
2402
|
await delay(this.jitterMs(reactionDelay, 0.4));
|
|
2403
|
-
await
|
|
2404
|
-
delay: this.jitterMs(45, 0.45)
|
|
2405
|
-
});
|
|
2403
|
+
await cursor.actions.click();
|
|
2406
2404
|
await restoreOnce();
|
|
2407
2405
|
logger6.success("humanClick");
|
|
2408
2406
|
return true;
|
|
@@ -2447,7 +2445,7 @@ var Humanize = {
|
|
|
2447
2445
|
* @param {import('playwright').Page} page
|
|
2448
2446
|
* @param {string} selector - 输入框选择器
|
|
2449
2447
|
* @param {string} text - 要输入的文本
|
|
2450
|
-
* @param {Object} [options]
|
|
2448
|
+
* @param {Object} [options]
|
|
2451
2449
|
* @param {number} [options.baseDelay=180] - 基础按键延迟 (ms),实际 ±40% 抖动
|
|
2452
2450
|
* @param {number} [options.pauseProbability=0.08] - 停顿概率 (0-1)
|
|
2453
2451
|
* @param {number} [options.pauseBase=800] - 停顿时长基础值 (ms),实际 ±50% 抖动
|
|
@@ -2609,16 +2607,43 @@ var logger7 = createInternalLogger("Humanize.Mobile");
|
|
|
2609
2607
|
var initializedPages = /* @__PURE__ */ new WeakSet();
|
|
2610
2608
|
var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
2611
2609
|
var resolveViewport = (page) => page?.viewportSize?.() || { width: 390, height: 844 };
|
|
2610
|
+
var describeTarget = (target) => {
|
|
2611
|
+
if (target == null) return "Current Position";
|
|
2612
|
+
return typeof target === "string" ? target : "ElementHandle";
|
|
2613
|
+
};
|
|
2614
|
+
var clipBoxToViewport = (box, viewport) => {
|
|
2615
|
+
if (!box || !viewport) return box;
|
|
2616
|
+
const left = clamp(box.x, 0, viewport.width);
|
|
2617
|
+
const top = clamp(box.y, 0, viewport.height);
|
|
2618
|
+
const right = clamp(box.x + box.width, 0, viewport.width);
|
|
2619
|
+
const bottom = clamp(box.y + box.height, 0, viewport.height);
|
|
2620
|
+
if (right - left > 1 && bottom - top > 1) {
|
|
2621
|
+
return {
|
|
2622
|
+
x: left,
|
|
2623
|
+
y: top,
|
|
2624
|
+
width: right - left,
|
|
2625
|
+
height: bottom - top
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
return box;
|
|
2629
|
+
};
|
|
2612
2630
|
var checkElementVisibility = async (element) => {
|
|
2613
2631
|
return element.evaluate((el) => {
|
|
2632
|
+
const targetStyle = window.getComputedStyle(el);
|
|
2633
|
+
if (!targetStyle || targetStyle.display === "none" || targetStyle.visibility === "hidden" || targetStyle.visibility === "collapse") {
|
|
2634
|
+
return { code: "NOT_INTERACTABLE", reason: "\u5143\u7D20\u4E0D\u53EF\u89C1", direction: "down" };
|
|
2635
|
+
}
|
|
2614
2636
|
const rect = el.getBoundingClientRect();
|
|
2615
2637
|
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
2616
|
-
return { code: "ZERO_DIMENSIONS", direction: "down" };
|
|
2638
|
+
return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6", direction: "down" };
|
|
2617
2639
|
}
|
|
2618
2640
|
const viewW = window.innerWidth;
|
|
2619
2641
|
const viewH = window.innerHeight;
|
|
2620
|
-
const
|
|
2621
|
-
|
|
2642
|
+
const centerY = rect.top + rect.height / 2;
|
|
2643
|
+
let clipLeft = 0;
|
|
2644
|
+
let clipRight = viewW;
|
|
2645
|
+
let clipTop = 0;
|
|
2646
|
+
let clipBottom = viewH;
|
|
2622
2647
|
let isFixed = false;
|
|
2623
2648
|
for (let node = el; node && node !== document.body; node = node.parentElement) {
|
|
2624
2649
|
const style = window.getComputedStyle(node);
|
|
@@ -2627,20 +2652,101 @@ var checkElementVisibility = async (element) => {
|
|
|
2627
2652
|
break;
|
|
2628
2653
|
}
|
|
2629
2654
|
}
|
|
2630
|
-
|
|
2655
|
+
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
2656
|
+
const style = window.getComputedStyle(node);
|
|
2657
|
+
if (!style) continue;
|
|
2658
|
+
const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
|
|
2659
|
+
const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
|
|
2660
|
+
if (!clipsX && !clipsY) continue;
|
|
2661
|
+
const nodeRect = node.getBoundingClientRect();
|
|
2662
|
+
if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
|
|
2663
|
+
if (clipsX) {
|
|
2664
|
+
clipLeft = Math.max(clipLeft, nodeRect.left);
|
|
2665
|
+
clipRight = Math.min(clipRight, nodeRect.right);
|
|
2666
|
+
}
|
|
2667
|
+
if (clipsY) {
|
|
2668
|
+
clipTop = Math.max(clipTop, nodeRect.top);
|
|
2669
|
+
clipBottom = Math.min(clipBottom, nodeRect.bottom);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
|
|
2673
|
+
const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
|
|
2674
|
+
const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
|
|
2675
|
+
const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
|
|
2676
|
+
const visibleWidth = visibleRight - visibleLeft;
|
|
2677
|
+
const visibleHeight = visibleBottom - visibleTop;
|
|
2678
|
+
const cx = visibleLeft + visibleWidth / 2;
|
|
2679
|
+
const cy = visibleTop + visibleHeight / 2;
|
|
2680
|
+
if (visibleWidth <= 1 || visibleHeight <= 1) {
|
|
2631
2681
|
return {
|
|
2632
2682
|
code: "OUT_OF_VIEWPORT",
|
|
2633
|
-
|
|
2634
|
-
|
|
2683
|
+
reason: "\u4E0D\u5728\u89C6\u53E3\u5185",
|
|
2684
|
+
direction: centerY < clipTop ? "up" : "down",
|
|
2685
|
+
cy: centerY,
|
|
2635
2686
|
viewH,
|
|
2636
2687
|
isFixed
|
|
2637
2688
|
};
|
|
2638
2689
|
}
|
|
2639
|
-
const
|
|
2640
|
-
|
|
2690
|
+
const interactiveSelector = [
|
|
2691
|
+
"button",
|
|
2692
|
+
'[role="button"]',
|
|
2693
|
+
"a[href]",
|
|
2694
|
+
"label",
|
|
2695
|
+
"input",
|
|
2696
|
+
"textarea",
|
|
2697
|
+
"select",
|
|
2698
|
+
"summary",
|
|
2699
|
+
'[contenteditable="true"]',
|
|
2700
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
2701
|
+
].join(",");
|
|
2702
|
+
const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
|
|
2703
|
+
const sameInteractiveTarget = (pointElement) => {
|
|
2704
|
+
if (!pointElement) return false;
|
|
2705
|
+
if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
|
|
2706
|
+
return true;
|
|
2707
|
+
}
|
|
2708
|
+
if (!targetInteractive || typeof pointElement.closest !== "function") {
|
|
2709
|
+
return false;
|
|
2710
|
+
}
|
|
2711
|
+
return pointElement.closest(interactiveSelector) === targetInteractive;
|
|
2712
|
+
};
|
|
2713
|
+
const describeElement = (node) => {
|
|
2714
|
+
if (!node) return null;
|
|
2715
|
+
const className = typeof node.className === "string" ? node.className : node.className && typeof node.className.baseVal === "string" ? node.className.baseVal : "";
|
|
2716
|
+
const rect2 = node.getBoundingClientRect?.();
|
|
2717
|
+
const style = window.getComputedStyle(node);
|
|
2718
|
+
return {
|
|
2719
|
+
tag: node.tagName,
|
|
2720
|
+
id: node.id || "",
|
|
2721
|
+
className,
|
|
2722
|
+
isFixed: Boolean(style && (style.position === "fixed" || style.position === "sticky")),
|
|
2723
|
+
top: rect2 ? rect2.top : null,
|
|
2724
|
+
bottom: rect2 ? rect2.bottom : null,
|
|
2725
|
+
left: rect2 ? rect2.left : null,
|
|
2726
|
+
right: rect2 ? rect2.right : null
|
|
2727
|
+
};
|
|
2728
|
+
};
|
|
2729
|
+
const samplePoints = [
|
|
2730
|
+
{ x: cx, y: cy },
|
|
2731
|
+
{ x: visibleLeft + Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
|
|
2732
|
+
{ x: visibleRight - Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
|
|
2733
|
+
{ x: cx, y: visibleTop + Math.min(8, Math.max(1, visibleHeight * 0.25)) },
|
|
2734
|
+
{ x: cx, y: visibleBottom - Math.min(8, Math.max(1, visibleHeight * 0.25)) }
|
|
2735
|
+
];
|
|
2736
|
+
let obstruction = null;
|
|
2737
|
+
for (const point of samplePoints) {
|
|
2738
|
+
const pointElement = document.elementFromPoint(point.x, point.y);
|
|
2739
|
+
if (sameInteractiveTarget(pointElement)) {
|
|
2740
|
+
return { code: "VISIBLE", isFixed };
|
|
2741
|
+
}
|
|
2742
|
+
obstruction = obstruction || describeElement(pointElement);
|
|
2743
|
+
}
|
|
2744
|
+
if (obstruction) {
|
|
2641
2745
|
return {
|
|
2642
2746
|
code: "OBSTRUCTED",
|
|
2747
|
+
reason: "\u88AB\u906E\u6321",
|
|
2643
2748
|
direction: cy > viewH / 2 ? "down" : "up",
|
|
2749
|
+
obstruction,
|
|
2644
2750
|
cy,
|
|
2645
2751
|
viewH,
|
|
2646
2752
|
isFixed
|
|
@@ -2649,17 +2755,185 @@ var checkElementVisibility = async (element) => {
|
|
|
2649
2755
|
return { code: "VISIBLE", isFixed };
|
|
2650
2756
|
});
|
|
2651
2757
|
};
|
|
2758
|
+
var resolveSafeTapPoint = async (element) => {
|
|
2759
|
+
return element.evaluate((el) => {
|
|
2760
|
+
const rect = el.getBoundingClientRect();
|
|
2761
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
2762
|
+
return null;
|
|
2763
|
+
}
|
|
2764
|
+
const viewW = window.innerWidth;
|
|
2765
|
+
const viewH = window.innerHeight;
|
|
2766
|
+
let clipLeft = 0;
|
|
2767
|
+
let clipRight = viewW;
|
|
2768
|
+
let clipTop = 0;
|
|
2769
|
+
let clipBottom = viewH;
|
|
2770
|
+
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
2771
|
+
const style = window.getComputedStyle(node);
|
|
2772
|
+
if (!style) continue;
|
|
2773
|
+
const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
|
|
2774
|
+
const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
|
|
2775
|
+
if (!clipsX && !clipsY) continue;
|
|
2776
|
+
const nodeRect = node.getBoundingClientRect();
|
|
2777
|
+
if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
|
|
2778
|
+
if (clipsX) {
|
|
2779
|
+
clipLeft = Math.max(clipLeft, nodeRect.left);
|
|
2780
|
+
clipRight = Math.min(clipRight, nodeRect.right);
|
|
2781
|
+
}
|
|
2782
|
+
if (clipsY) {
|
|
2783
|
+
clipTop = Math.max(clipTop, nodeRect.top);
|
|
2784
|
+
clipBottom = Math.min(clipBottom, nodeRect.bottom);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
|
|
2788
|
+
const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
|
|
2789
|
+
const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
|
|
2790
|
+
const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
|
|
2791
|
+
const visibleWidth = visibleRight - visibleLeft;
|
|
2792
|
+
const visibleHeight = visibleBottom - visibleTop;
|
|
2793
|
+
if (visibleWidth <= 1 || visibleHeight <= 1) {
|
|
2794
|
+
return null;
|
|
2795
|
+
}
|
|
2796
|
+
const interactiveSelector = [
|
|
2797
|
+
"button",
|
|
2798
|
+
'[role="button"]',
|
|
2799
|
+
"a[href]",
|
|
2800
|
+
"label",
|
|
2801
|
+
"input",
|
|
2802
|
+
"textarea",
|
|
2803
|
+
"select",
|
|
2804
|
+
"summary",
|
|
2805
|
+
'[contenteditable="true"]',
|
|
2806
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
2807
|
+
].join(",");
|
|
2808
|
+
const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
|
|
2809
|
+
const sameInteractiveTarget = (pointElement) => {
|
|
2810
|
+
if (!pointElement) return false;
|
|
2811
|
+
if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
|
|
2812
|
+
return true;
|
|
2813
|
+
}
|
|
2814
|
+
if (!targetInteractive || typeof pointElement.closest !== "function") {
|
|
2815
|
+
return false;
|
|
2816
|
+
}
|
|
2817
|
+
return pointElement.closest(interactiveSelector) === targetInteractive;
|
|
2818
|
+
};
|
|
2819
|
+
const cx = visibleLeft + visibleWidth / 2;
|
|
2820
|
+
const cy = visibleTop + visibleHeight / 2;
|
|
2821
|
+
const smallX = Math.min(12, Math.max(2, visibleWidth * 0.18));
|
|
2822
|
+
const smallY = Math.min(12, Math.max(2, visibleHeight * 0.18));
|
|
2823
|
+
const points = [
|
|
2824
|
+
{ x: cx, y: cy },
|
|
2825
|
+
{ x: visibleLeft + smallX, y: visibleTop + smallY },
|
|
2826
|
+
{ x: visibleRight - smallX, y: visibleTop + smallY },
|
|
2827
|
+
{ x: visibleLeft + smallX, y: visibleBottom - smallY },
|
|
2828
|
+
{ x: visibleRight - smallX, y: visibleBottom - smallY },
|
|
2829
|
+
{ x: cx, y: visibleTop + Math.min(10, Math.max(2, visibleHeight * 0.2)) },
|
|
2830
|
+
{ x: cx, y: visibleBottom - Math.min(10, Math.max(2, visibleHeight * 0.2)) },
|
|
2831
|
+
{ x: visibleLeft + Math.min(10, Math.max(2, visibleWidth * 0.2)), y: cy },
|
|
2832
|
+
{ x: visibleRight - Math.min(10, Math.max(2, visibleWidth * 0.2)), y: cy }
|
|
2833
|
+
];
|
|
2834
|
+
const safePoints = points.filter((point) => {
|
|
2835
|
+
const pointElement = document.elementFromPoint(point.x, point.y);
|
|
2836
|
+
return sameInteractiveTarget(pointElement);
|
|
2837
|
+
});
|
|
2838
|
+
const candidates = safePoints.length ? safePoints : points;
|
|
2839
|
+
const chosen = candidates[Math.floor(Math.random() * candidates.length)];
|
|
2840
|
+
if (!chosen) return null;
|
|
2841
|
+
return {
|
|
2842
|
+
x: chosen.x,
|
|
2843
|
+
y: chosen.y
|
|
2844
|
+
};
|
|
2845
|
+
});
|
|
2846
|
+
};
|
|
2847
|
+
var getScrollableRect = async (element) => {
|
|
2848
|
+
return element.evaluate((el) => {
|
|
2849
|
+
const isScrollable = (node) => {
|
|
2850
|
+
const style = window.getComputedStyle(node);
|
|
2851
|
+
if (!style) return false;
|
|
2852
|
+
const overflowY = style.overflowY;
|
|
2853
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
2854
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
2855
|
+
};
|
|
2856
|
+
let current = el;
|
|
2857
|
+
while (current && current !== document.body) {
|
|
2858
|
+
if (isScrollable(current)) {
|
|
2859
|
+
const rect = current.getBoundingClientRect();
|
|
2860
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
2861
|
+
return {
|
|
2862
|
+
x: rect.x,
|
|
2863
|
+
y: rect.y,
|
|
2864
|
+
width: rect.width,
|
|
2865
|
+
height: rect.height
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
current = current.parentElement;
|
|
2870
|
+
}
|
|
2871
|
+
return null;
|
|
2872
|
+
});
|
|
2873
|
+
};
|
|
2874
|
+
var scrollScrollableAncestor = async (element, deltaY) => {
|
|
2875
|
+
return element.evaluate((el, amount) => {
|
|
2876
|
+
const isScrollable = (node) => {
|
|
2877
|
+
const style = window.getComputedStyle(node);
|
|
2878
|
+
if (!style) return false;
|
|
2879
|
+
const overflowY = style.overflowY;
|
|
2880
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
2881
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
2882
|
+
};
|
|
2883
|
+
let current = el;
|
|
2884
|
+
while (current && current !== document.body) {
|
|
2885
|
+
if (isScrollable(current)) {
|
|
2886
|
+
const beforeTop2 = current.scrollTop;
|
|
2887
|
+
current.scrollTop = beforeTop2 + amount;
|
|
2888
|
+
return {
|
|
2889
|
+
scroller: true,
|
|
2890
|
+
moved: current.scrollTop !== beforeTop2,
|
|
2891
|
+
scrollTop: current.scrollTop
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
current = current.parentElement;
|
|
2895
|
+
}
|
|
2896
|
+
const beforeTop = window.scrollY;
|
|
2897
|
+
window.scrollBy(0, amount);
|
|
2898
|
+
return {
|
|
2899
|
+
scroller: null,
|
|
2900
|
+
moved: window.scrollY !== beforeTop,
|
|
2901
|
+
scrollTop: window.scrollY
|
|
2902
|
+
};
|
|
2903
|
+
}, deltaY);
|
|
2904
|
+
};
|
|
2652
2905
|
var dispatchTouchSwipe = async (page, deltaY, options = {}) => {
|
|
2653
2906
|
const viewport = resolveViewport(page);
|
|
2654
|
-
const
|
|
2907
|
+
const rawRect = options.rect || null;
|
|
2908
|
+
const rect = rawRect ? {
|
|
2909
|
+
x: clamp(rawRect.x, 0, viewport.width),
|
|
2910
|
+
y: clamp(rawRect.y, 0, viewport.height),
|
|
2911
|
+
width: clamp(rawRect.width, 0, viewport.width),
|
|
2912
|
+
height: clamp(rawRect.height, 0, viewport.height)
|
|
2913
|
+
} : null;
|
|
2914
|
+
const area = rect && rect.width > 24 && rect.height > 48 ? {
|
|
2915
|
+
left: rect.x,
|
|
2916
|
+
right: Math.min(viewport.width, rect.x + rect.width),
|
|
2917
|
+
top: rect.y,
|
|
2918
|
+
bottom: Math.min(viewport.height, rect.y + rect.height)
|
|
2919
|
+
} : {
|
|
2920
|
+
left: 0,
|
|
2921
|
+
right: viewport.width,
|
|
2922
|
+
top: 0,
|
|
2923
|
+
bottom: viewport.height
|
|
2924
|
+
};
|
|
2925
|
+
const areaWidth = Math.max(1, area.right - area.left);
|
|
2926
|
+
const areaHeight = Math.max(1, area.bottom - area.top);
|
|
2927
|
+
const maxDistance = rect ? Math.max(60, areaHeight * 0.72) : Math.max(120, viewport.height * 0.72);
|
|
2928
|
+
const distance = clamp(Math.abs(deltaY), Math.min(80, maxDistance), maxDistance);
|
|
2655
2929
|
const direction = deltaY >= 0 ? 1 : -1;
|
|
2656
2930
|
const startX = clamp(
|
|
2657
|
-
|
|
2931
|
+
area.left + areaWidth * (0.45 + Math.random() * 0.1),
|
|
2658
2932
|
24,
|
|
2659
2933
|
viewport.width - 24
|
|
2660
2934
|
);
|
|
2661
|
-
const startY = direction > 0 ? clamp(
|
|
2662
|
-
const endY = clamp(startY - direction * distance,
|
|
2935
|
+
const startY = direction > 0 ? clamp(area.top + areaHeight * (0.72 + Math.random() * 0.1), area.top + 24, area.bottom - 16) : clamp(area.top + areaHeight * (0.28 + Math.random() * 0.1), area.top + 16, area.bottom - 24);
|
|
2936
|
+
const endY = clamp(startY - direction * distance, Math.max(16, area.top + 12), Math.min(viewport.height - 16, area.bottom - 12));
|
|
2663
2937
|
const steps = Math.max(4, Math.round(Number(options.steps || 6)));
|
|
2664
2938
|
const durationMs = jitterMs(options.durationMs || 320, 0.35);
|
|
2665
2939
|
let client = null;
|
|
@@ -2762,8 +3036,11 @@ var MobileHumanize = {
|
|
|
2762
3036
|
maxDurationMs = maxSteps * 280 + 1200,
|
|
2763
3037
|
throwOnMissing = false
|
|
2764
3038
|
} = options;
|
|
3039
|
+
const targetDesc = describeTarget(target);
|
|
3040
|
+
logger7.start("humanScroll", `target=${targetDesc}`);
|
|
2765
3041
|
const element = await resolveElement(page, target, { throwOnMissing });
|
|
2766
3042
|
if (!element) {
|
|
3043
|
+
logger7.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${targetDesc}`);
|
|
2767
3044
|
return { element: null, didScroll: false, restore: null };
|
|
2768
3045
|
}
|
|
2769
3046
|
const startTime = Date.now();
|
|
@@ -2771,17 +3048,124 @@ var MobileHumanize = {
|
|
|
2771
3048
|
for (let i = 0; i < maxSteps; i += 1) {
|
|
2772
3049
|
const status = await checkElementVisibility(element);
|
|
2773
3050
|
if (status.code === "VISIBLE") {
|
|
3051
|
+
if (status.isFixed) {
|
|
3052
|
+
logger7.info("humanScroll | fixed/sticky \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
3053
|
+
} else {
|
|
3054
|
+
logger7.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
|
|
3055
|
+
}
|
|
3056
|
+
logger7.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
3057
|
+
return { element, didScroll, restore: null };
|
|
3058
|
+
}
|
|
3059
|
+
if (status.code === "ZERO_DIMENSIONS" || status.code === "NOT_INTERACTABLE") {
|
|
3060
|
+
logger7.warn(`humanScroll | \u5143\u7D20\u4E0D\u53EF\u6EDA\u52A8\u81F3\u53EF\u70B9\u51FB\u72B6\u6001: ${status.reason || status.code}`);
|
|
3061
|
+
return { element, didScroll, restore: null };
|
|
3062
|
+
}
|
|
3063
|
+
const scrollRect = await getScrollableRect(element);
|
|
3064
|
+
if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
|
|
3065
|
+
logger7.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
|
|
2774
3066
|
return { element, didScroll, restore: null };
|
|
2775
3067
|
}
|
|
2776
3068
|
if (Date.now() - startTime > maxDurationMs) {
|
|
2777
|
-
logger7.warn(`humanScroll | mobile timeout (${maxDurationMs}ms)`);
|
|
3069
|
+
logger7.warn(`humanScroll | mobile timeout (${maxDurationMs}ms, status=${status.code}, direction=${status.direction || "unknown"}, fixed=${Boolean(status.isFixed)})`);
|
|
2778
3070
|
return { element, didScroll, restore: null };
|
|
2779
3071
|
}
|
|
2780
|
-
const
|
|
2781
|
-
const
|
|
2782
|
-
|
|
3072
|
+
const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
|
|
3073
|
+
const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
|
|
3074
|
+
logger7.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason || status.code} ${status.direction ? `(${status.direction})` : ""}`);
|
|
3075
|
+
const distance = stepMin + Math.random() * Math.max(1, stepMax - stepMin);
|
|
3076
|
+
let deltaY = status.direction === "up" ? -distance : distance;
|
|
3077
|
+
if (status.code === "OBSTRUCTED") {
|
|
3078
|
+
if (status.obstruction?.isFixed && status.obstruction.top != null) {
|
|
3079
|
+
const obstructionMiddle = (Number(status.obstruction.top || 0) + Number(status.obstruction.bottom || status.obstruction.top || 0)) / 2;
|
|
3080
|
+
const visibleMiddle = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
3081
|
+
deltaY = obstructionMiddle < visibleMiddle ? -distance : distance;
|
|
3082
|
+
} else {
|
|
3083
|
+
const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
3084
|
+
deltaY = status.cy > halfY ? distance : -distance;
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
const beforeWindowState = scrollRect ? await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) : null;
|
|
3088
|
+
const beforeState = scrollRect ? await element.evaluate((el) => {
|
|
3089
|
+
const isScrollable = (node) => {
|
|
3090
|
+
const style = window.getComputedStyle(node);
|
|
3091
|
+
if (!style) return false;
|
|
3092
|
+
const overflowY = style.overflowY;
|
|
3093
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3094
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
3095
|
+
};
|
|
3096
|
+
let current = el;
|
|
3097
|
+
while (current && current !== document.body) {
|
|
3098
|
+
if (isScrollable(current)) {
|
|
3099
|
+
return {
|
|
3100
|
+
kind: "element",
|
|
3101
|
+
top: current.scrollTop,
|
|
3102
|
+
left: current.scrollLeft
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
current = current.parentElement;
|
|
3106
|
+
}
|
|
3107
|
+
return { kind: "window", top: window.scrollY, left: window.scrollX };
|
|
3108
|
+
}) : null;
|
|
3109
|
+
await dispatchTouchSwipe(page, deltaY, { rect: scrollRect });
|
|
3110
|
+
if (scrollRect && beforeWindowState) {
|
|
3111
|
+
const afterWindowState = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
|
|
3112
|
+
if (Math.abs(afterWindowState.x - beforeWindowState.x) > 2 || Math.abs(afterWindowState.y - beforeWindowState.y) > 2) {
|
|
3113
|
+
await page.evaluate((state) => window.scrollTo(state.x, state.y), beforeWindowState);
|
|
3114
|
+
logger7.debug(`humanScroll | \u7A97\u53E3\u6EDA\u52A8\u56DE\u6536 from=${Math.round(afterWindowState.y)} to=${Math.round(beforeWindowState.y)}`);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
if (scrollRect && beforeState) {
|
|
3118
|
+
const afterState = await element.evaluate((el) => {
|
|
3119
|
+
const isScrollable = (node) => {
|
|
3120
|
+
const style = window.getComputedStyle(node);
|
|
3121
|
+
if (!style) return false;
|
|
3122
|
+
const overflowY = style.overflowY;
|
|
3123
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3124
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
3125
|
+
};
|
|
3126
|
+
let current = el;
|
|
3127
|
+
while (current && current !== document.body) {
|
|
3128
|
+
if (isScrollable(current)) {
|
|
3129
|
+
return {
|
|
3130
|
+
kind: "element",
|
|
3131
|
+
top: current.scrollTop,
|
|
3132
|
+
left: current.scrollLeft
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
current = current.parentElement;
|
|
3136
|
+
}
|
|
3137
|
+
return { kind: "window", top: window.scrollY, left: window.scrollX };
|
|
3138
|
+
});
|
|
3139
|
+
const topDelta = Number(afterState.top || 0) - Number(beforeState.top || 0);
|
|
3140
|
+
const leftDelta = Number(afterState.left || 0) - Number(beforeState.left || 0);
|
|
3141
|
+
const expectedDelta = Number(deltaY || 0);
|
|
3142
|
+
const moved = beforeState.kind !== afterState.kind || Math.abs(topDelta) > 2 || Math.abs(leftDelta) > 2;
|
|
3143
|
+
if (!moved) {
|
|
3144
|
+
const fallback = await scrollScrollableAncestor(element, deltaY);
|
|
3145
|
+
logger7.debug(`humanScroll | \u5BB9\u5668\u89E6\u6478\u65E0\u6548\uFF0C\u76F4\u63A5\u6EDA\u52A8 fallback=${fallback.scroller ? "ancestor" : "window"} top=${Math.round(fallback.scrollTop || 0)}`);
|
|
3146
|
+
} else if (beforeState.kind === afterState.kind && Math.abs(expectedDelta) > 24 && Math.sign(topDelta || expectedDelta) === Math.sign(expectedDelta) && Math.abs(topDelta) < Math.min(Math.abs(expectedDelta) * 0.45, 96)) {
|
|
3147
|
+
const residualDelta = expectedDelta - topDelta;
|
|
3148
|
+
if (Math.sign(residualDelta) === Math.sign(expectedDelta) && Math.abs(residualDelta) > 24) {
|
|
3149
|
+
const fallback = await scrollScrollableAncestor(element, residualDelta);
|
|
3150
|
+
logger7.debug(`humanScroll | \u5BB9\u5668\u89E6\u6478\u8DDD\u79BB\u4E0D\u8DB3\uFF0C\u8865\u507F\u6EDA\u52A8 fallback=${fallback.scroller ? "ancestor" : "window"} top=${Math.round(fallback.scrollTop || 0)}`);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
2783
3154
|
didScroll = true;
|
|
2784
3155
|
}
|
|
3156
|
+
try {
|
|
3157
|
+
await element.scrollIntoViewIfNeeded?.();
|
|
3158
|
+
await waitJitter(80, 0.3);
|
|
3159
|
+
const finalStatus = await checkElementVisibility(element);
|
|
3160
|
+
if (finalStatus.code === "VISIBLE") {
|
|
3161
|
+
logger7.info("humanScroll | \u539F\u751F scrollIntoViewIfNeeded \u515C\u5E95\u6210\u529F");
|
|
3162
|
+
logger7.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
3163
|
+
return { element, didScroll: true, restore: null };
|
|
3164
|
+
}
|
|
3165
|
+
} catch (fallbackError) {
|
|
3166
|
+
logger7.debug(`humanScroll | native fallback failed: ${fallbackError?.message || fallbackError}`);
|
|
3167
|
+
}
|
|
3168
|
+
logger7.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
|
|
2785
3169
|
return { element, didScroll, restore: null };
|
|
2786
3170
|
},
|
|
2787
3171
|
async humanClick(page, target, options = {}) {
|
|
@@ -2790,29 +3174,51 @@ var MobileHumanize = {
|
|
|
2790
3174
|
throwOnMissing = true,
|
|
2791
3175
|
scrollIfNeeded = true
|
|
2792
3176
|
} = options;
|
|
2793
|
-
|
|
2794
|
-
|
|
3177
|
+
const targetDesc = describeTarget(target);
|
|
3178
|
+
logger7.start("humanClick", `target=${targetDesc}`);
|
|
3179
|
+
try {
|
|
3180
|
+
if (target == null) {
|
|
3181
|
+
const viewport = resolveViewport(page);
|
|
3182
|
+
await waitJitter(reactionDelay, 0.45);
|
|
3183
|
+
await tapPoint(page, {
|
|
3184
|
+
x: viewport.width * (0.45 + Math.random() * 0.1),
|
|
3185
|
+
y: viewport.height * (0.48 + Math.random() * 0.12)
|
|
3186
|
+
});
|
|
3187
|
+
logger7.success("humanClick", "Tapped current position");
|
|
3188
|
+
return true;
|
|
3189
|
+
}
|
|
3190
|
+
const element = await resolveElement(page, target, { throwOnMissing });
|
|
3191
|
+
if (!element) {
|
|
3192
|
+
logger7.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
|
|
3193
|
+
return false;
|
|
3194
|
+
}
|
|
3195
|
+
if (scrollIfNeeded) {
|
|
3196
|
+
await MobileHumanize.humanScroll(page, element, { throwOnMissing });
|
|
3197
|
+
}
|
|
3198
|
+
const status = await checkElementVisibility(element).catch(() => null);
|
|
3199
|
+
if (status && (status.code === "ZERO_DIMENSIONS" || status.code === "NOT_INTERACTABLE")) {
|
|
3200
|
+
const message = `\u5143\u7D20\u4E0D\u53EF\u70B9\u51FB: ${status.reason || status.code}`;
|
|
3201
|
+
if (throwOnMissing) throw new Error(message);
|
|
3202
|
+
logger7.warn(`humanClick: ${message}\uFF0C\u8DF3\u8FC7\u70B9\u51FB`);
|
|
3203
|
+
return false;
|
|
3204
|
+
}
|
|
3205
|
+
const box = await element.boundingBox();
|
|
3206
|
+
if (!box) {
|
|
3207
|
+
if (throwOnMissing) throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
3208
|
+
logger7.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
|
|
3209
|
+
return false;
|
|
3210
|
+
}
|
|
2795
3211
|
await waitJitter(reactionDelay, 0.45);
|
|
2796
|
-
await
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3212
|
+
const safePoint = await resolveSafeTapPoint(element).catch(() => null);
|
|
3213
|
+
const tapTarget = safePoint || randomPointInBox(clipBoxToViewport(box, resolveViewport(page)), 0.2);
|
|
3214
|
+
await tapPoint(page, tapTarget);
|
|
3215
|
+
await waitJitter(120, 0.35);
|
|
3216
|
+
logger7.success("humanClick");
|
|
2800
3217
|
return true;
|
|
3218
|
+
} catch (error) {
|
|
3219
|
+
logger7.fail("humanClick", error);
|
|
3220
|
+
throw error;
|
|
2801
3221
|
}
|
|
2802
|
-
const element = await resolveElement(page, target, { throwOnMissing });
|
|
2803
|
-
if (!element) return false;
|
|
2804
|
-
if (scrollIfNeeded) {
|
|
2805
|
-
await MobileHumanize.humanScroll(page, element, { throwOnMissing });
|
|
2806
|
-
}
|
|
2807
|
-
const box = await element.boundingBox();
|
|
2808
|
-
if (!box) {
|
|
2809
|
-
if (throwOnMissing) throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
2810
|
-
return false;
|
|
2811
|
-
}
|
|
2812
|
-
await waitJitter(reactionDelay, 0.45);
|
|
2813
|
-
await tapPoint(page, randomPointInBox(box, 0.2));
|
|
2814
|
-
await waitJitter(120, 0.35);
|
|
2815
|
-
return true;
|
|
2816
3222
|
},
|
|
2817
3223
|
async randomSleep(baseMs, jitterPercent = 0.3) {
|
|
2818
3224
|
await waitJitter(baseMs, jitterPercent);
|