@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.cjs
CHANGED
|
@@ -2148,7 +2148,7 @@ var Humanize = {
|
|
|
2148
2148
|
},
|
|
2149
2149
|
/**
|
|
2150
2150
|
* 初始化页面的 Ghost Cursor(必须在使用其他 cursor 相关方法前调用)
|
|
2151
|
-
*
|
|
2151
|
+
*
|
|
2152
2152
|
* @param {import('playwright').Page} page
|
|
2153
2153
|
* @returns {Promise<void>}
|
|
2154
2154
|
*/
|
|
@@ -2164,7 +2164,7 @@ var Humanize = {
|
|
|
2164
2164
|
},
|
|
2165
2165
|
/**
|
|
2166
2166
|
* 人类化鼠标移动 - 使用 ghost-cursor 移动到指定位置或元素
|
|
2167
|
-
*
|
|
2167
|
+
*
|
|
2168
2168
|
* @param {import('playwright').Page} page
|
|
2169
2169
|
* @param {string|{x: number, y: number}|import('playwright').ElementHandle} target - CSS选择器、坐标对象或元素句柄
|
|
2170
2170
|
*/
|
|
@@ -2206,7 +2206,7 @@ var Humanize = {
|
|
|
2206
2206
|
/**
|
|
2207
2207
|
* 渐进式滚动到元素可见(仅处理 Y 轴滚动)
|
|
2208
2208
|
* 返回 restore 方法,用于将滚动容器恢复到原位置
|
|
2209
|
-
*
|
|
2209
|
+
*
|
|
2210
2210
|
* @param {import('playwright').Page} page
|
|
2211
2211
|
* @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
|
|
2212
2212
|
* @param {Object} [options]
|
|
@@ -2277,7 +2277,7 @@ var Humanize = {
|
|
|
2277
2277
|
return { code: "VISIBLE", isFixed };
|
|
2278
2278
|
});
|
|
2279
2279
|
};
|
|
2280
|
-
const
|
|
2280
|
+
const getScrollableRect2 = async () => {
|
|
2281
2281
|
return await element.evaluate((el) => {
|
|
2282
2282
|
const isScrollable = (node) => {
|
|
2283
2283
|
const style = window.getComputedStyle(node);
|
|
@@ -2320,7 +2320,7 @@ var Humanize = {
|
|
|
2320
2320
|
if (status.code === "OBSTRUCTED" && status.obstruction) {
|
|
2321
2321
|
logger6.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
|
|
2322
2322
|
}
|
|
2323
|
-
const scrollRect = await
|
|
2323
|
+
const scrollRect = await getScrollableRect2();
|
|
2324
2324
|
if (!scrollRect && status.isFixed) {
|
|
2325
2325
|
logger6.warn("humanScroll | fixed \u5BB9\u5668\u5185\u4E14\u65E0\u53EF\u6EDA\u52A8\u7956\u5148\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
2326
2326
|
return { element, didScroll };
|
|
@@ -2367,7 +2367,7 @@ var Humanize = {
|
|
|
2367
2367
|
},
|
|
2368
2368
|
/**
|
|
2369
2369
|
* 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
|
|
2370
|
-
*
|
|
2370
|
+
*
|
|
2371
2371
|
* @param {import('playwright').Page} page
|
|
2372
2372
|
* @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
|
|
2373
2373
|
* @param {Object} [options]
|
|
@@ -2428,9 +2428,7 @@ var Humanize = {
|
|
|
2428
2428
|
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
|
|
2429
2429
|
await cursor.actions.move({ x, y });
|
|
2430
2430
|
await (0, import_delay.default)(this.jitterMs(reactionDelay, 0.4));
|
|
2431
|
-
await
|
|
2432
|
-
delay: this.jitterMs(45, 0.45)
|
|
2433
|
-
});
|
|
2431
|
+
await cursor.actions.click();
|
|
2434
2432
|
await restoreOnce();
|
|
2435
2433
|
logger6.success("humanClick");
|
|
2436
2434
|
return true;
|
|
@@ -2475,7 +2473,7 @@ var Humanize = {
|
|
|
2475
2473
|
* @param {import('playwright').Page} page
|
|
2476
2474
|
* @param {string} selector - 输入框选择器
|
|
2477
2475
|
* @param {string} text - 要输入的文本
|
|
2478
|
-
* @param {Object} [options]
|
|
2476
|
+
* @param {Object} [options]
|
|
2479
2477
|
* @param {number} [options.baseDelay=180] - 基础按键延迟 (ms),实际 ±40% 抖动
|
|
2480
2478
|
* @param {number} [options.pauseProbability=0.08] - 停顿概率 (0-1)
|
|
2481
2479
|
* @param {number} [options.pauseBase=800] - 停顿时长基础值 (ms),实际 ±50% 抖动
|
|
@@ -2637,16 +2635,43 @@ var logger7 = createInternalLogger("Humanize.Mobile");
|
|
|
2637
2635
|
var initializedPages = /* @__PURE__ */ new WeakSet();
|
|
2638
2636
|
var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
2639
2637
|
var resolveViewport = (page) => page?.viewportSize?.() || { width: 390, height: 844 };
|
|
2638
|
+
var describeTarget = (target) => {
|
|
2639
|
+
if (target == null) return "Current Position";
|
|
2640
|
+
return typeof target === "string" ? target : "ElementHandle";
|
|
2641
|
+
};
|
|
2642
|
+
var clipBoxToViewport = (box, viewport) => {
|
|
2643
|
+
if (!box || !viewport) return box;
|
|
2644
|
+
const left = clamp(box.x, 0, viewport.width);
|
|
2645
|
+
const top = clamp(box.y, 0, viewport.height);
|
|
2646
|
+
const right = clamp(box.x + box.width, 0, viewport.width);
|
|
2647
|
+
const bottom = clamp(box.y + box.height, 0, viewport.height);
|
|
2648
|
+
if (right - left > 1 && bottom - top > 1) {
|
|
2649
|
+
return {
|
|
2650
|
+
x: left,
|
|
2651
|
+
y: top,
|
|
2652
|
+
width: right - left,
|
|
2653
|
+
height: bottom - top
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
return box;
|
|
2657
|
+
};
|
|
2640
2658
|
var checkElementVisibility = async (element) => {
|
|
2641
2659
|
return element.evaluate((el) => {
|
|
2660
|
+
const targetStyle = window.getComputedStyle(el);
|
|
2661
|
+
if (!targetStyle || targetStyle.display === "none" || targetStyle.visibility === "hidden" || targetStyle.visibility === "collapse") {
|
|
2662
|
+
return { code: "NOT_INTERACTABLE", reason: "\u5143\u7D20\u4E0D\u53EF\u89C1", direction: "down" };
|
|
2663
|
+
}
|
|
2642
2664
|
const rect = el.getBoundingClientRect();
|
|
2643
2665
|
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
2644
|
-
return { code: "ZERO_DIMENSIONS", direction: "down" };
|
|
2666
|
+
return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6", direction: "down" };
|
|
2645
2667
|
}
|
|
2646
2668
|
const viewW = window.innerWidth;
|
|
2647
2669
|
const viewH = window.innerHeight;
|
|
2648
|
-
const
|
|
2649
|
-
|
|
2670
|
+
const centerY = rect.top + rect.height / 2;
|
|
2671
|
+
let clipLeft = 0;
|
|
2672
|
+
let clipRight = viewW;
|
|
2673
|
+
let clipTop = 0;
|
|
2674
|
+
let clipBottom = viewH;
|
|
2650
2675
|
let isFixed = false;
|
|
2651
2676
|
for (let node = el; node && node !== document.body; node = node.parentElement) {
|
|
2652
2677
|
const style = window.getComputedStyle(node);
|
|
@@ -2655,20 +2680,101 @@ var checkElementVisibility = async (element) => {
|
|
|
2655
2680
|
break;
|
|
2656
2681
|
}
|
|
2657
2682
|
}
|
|
2658
|
-
|
|
2683
|
+
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
2684
|
+
const style = window.getComputedStyle(node);
|
|
2685
|
+
if (!style) continue;
|
|
2686
|
+
const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
|
|
2687
|
+
const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
|
|
2688
|
+
if (!clipsX && !clipsY) continue;
|
|
2689
|
+
const nodeRect = node.getBoundingClientRect();
|
|
2690
|
+
if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
|
|
2691
|
+
if (clipsX) {
|
|
2692
|
+
clipLeft = Math.max(clipLeft, nodeRect.left);
|
|
2693
|
+
clipRight = Math.min(clipRight, nodeRect.right);
|
|
2694
|
+
}
|
|
2695
|
+
if (clipsY) {
|
|
2696
|
+
clipTop = Math.max(clipTop, nodeRect.top);
|
|
2697
|
+
clipBottom = Math.min(clipBottom, nodeRect.bottom);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
|
|
2701
|
+
const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
|
|
2702
|
+
const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
|
|
2703
|
+
const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
|
|
2704
|
+
const visibleWidth = visibleRight - visibleLeft;
|
|
2705
|
+
const visibleHeight = visibleBottom - visibleTop;
|
|
2706
|
+
const cx = visibleLeft + visibleWidth / 2;
|
|
2707
|
+
const cy = visibleTop + visibleHeight / 2;
|
|
2708
|
+
if (visibleWidth <= 1 || visibleHeight <= 1) {
|
|
2659
2709
|
return {
|
|
2660
2710
|
code: "OUT_OF_VIEWPORT",
|
|
2661
|
-
|
|
2662
|
-
|
|
2711
|
+
reason: "\u4E0D\u5728\u89C6\u53E3\u5185",
|
|
2712
|
+
direction: centerY < clipTop ? "up" : "down",
|
|
2713
|
+
cy: centerY,
|
|
2663
2714
|
viewH,
|
|
2664
2715
|
isFixed
|
|
2665
2716
|
};
|
|
2666
2717
|
}
|
|
2667
|
-
const
|
|
2668
|
-
|
|
2718
|
+
const interactiveSelector = [
|
|
2719
|
+
"button",
|
|
2720
|
+
'[role="button"]',
|
|
2721
|
+
"a[href]",
|
|
2722
|
+
"label",
|
|
2723
|
+
"input",
|
|
2724
|
+
"textarea",
|
|
2725
|
+
"select",
|
|
2726
|
+
"summary",
|
|
2727
|
+
'[contenteditable="true"]',
|
|
2728
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
2729
|
+
].join(",");
|
|
2730
|
+
const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
|
|
2731
|
+
const sameInteractiveTarget = (pointElement) => {
|
|
2732
|
+
if (!pointElement) return false;
|
|
2733
|
+
if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
|
|
2734
|
+
return true;
|
|
2735
|
+
}
|
|
2736
|
+
if (!targetInteractive || typeof pointElement.closest !== "function") {
|
|
2737
|
+
return false;
|
|
2738
|
+
}
|
|
2739
|
+
return pointElement.closest(interactiveSelector) === targetInteractive;
|
|
2740
|
+
};
|
|
2741
|
+
const describeElement = (node) => {
|
|
2742
|
+
if (!node) return null;
|
|
2743
|
+
const className = typeof node.className === "string" ? node.className : node.className && typeof node.className.baseVal === "string" ? node.className.baseVal : "";
|
|
2744
|
+
const rect2 = node.getBoundingClientRect?.();
|
|
2745
|
+
const style = window.getComputedStyle(node);
|
|
2746
|
+
return {
|
|
2747
|
+
tag: node.tagName,
|
|
2748
|
+
id: node.id || "",
|
|
2749
|
+
className,
|
|
2750
|
+
isFixed: Boolean(style && (style.position === "fixed" || style.position === "sticky")),
|
|
2751
|
+
top: rect2 ? rect2.top : null,
|
|
2752
|
+
bottom: rect2 ? rect2.bottom : null,
|
|
2753
|
+
left: rect2 ? rect2.left : null,
|
|
2754
|
+
right: rect2 ? rect2.right : null
|
|
2755
|
+
};
|
|
2756
|
+
};
|
|
2757
|
+
const samplePoints = [
|
|
2758
|
+
{ x: cx, y: cy },
|
|
2759
|
+
{ x: visibleLeft + Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
|
|
2760
|
+
{ x: visibleRight - Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
|
|
2761
|
+
{ x: cx, y: visibleTop + Math.min(8, Math.max(1, visibleHeight * 0.25)) },
|
|
2762
|
+
{ x: cx, y: visibleBottom - Math.min(8, Math.max(1, visibleHeight * 0.25)) }
|
|
2763
|
+
];
|
|
2764
|
+
let obstruction = null;
|
|
2765
|
+
for (const point of samplePoints) {
|
|
2766
|
+
const pointElement = document.elementFromPoint(point.x, point.y);
|
|
2767
|
+
if (sameInteractiveTarget(pointElement)) {
|
|
2768
|
+
return { code: "VISIBLE", isFixed };
|
|
2769
|
+
}
|
|
2770
|
+
obstruction = obstruction || describeElement(pointElement);
|
|
2771
|
+
}
|
|
2772
|
+
if (obstruction) {
|
|
2669
2773
|
return {
|
|
2670
2774
|
code: "OBSTRUCTED",
|
|
2775
|
+
reason: "\u88AB\u906E\u6321",
|
|
2671
2776
|
direction: cy > viewH / 2 ? "down" : "up",
|
|
2777
|
+
obstruction,
|
|
2672
2778
|
cy,
|
|
2673
2779
|
viewH,
|
|
2674
2780
|
isFixed
|
|
@@ -2677,17 +2783,185 @@ var checkElementVisibility = async (element) => {
|
|
|
2677
2783
|
return { code: "VISIBLE", isFixed };
|
|
2678
2784
|
});
|
|
2679
2785
|
};
|
|
2786
|
+
var resolveSafeTapPoint = async (element) => {
|
|
2787
|
+
return element.evaluate((el) => {
|
|
2788
|
+
const rect = el.getBoundingClientRect();
|
|
2789
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
2790
|
+
return null;
|
|
2791
|
+
}
|
|
2792
|
+
const viewW = window.innerWidth;
|
|
2793
|
+
const viewH = window.innerHeight;
|
|
2794
|
+
let clipLeft = 0;
|
|
2795
|
+
let clipRight = viewW;
|
|
2796
|
+
let clipTop = 0;
|
|
2797
|
+
let clipBottom = viewH;
|
|
2798
|
+
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
2799
|
+
const style = window.getComputedStyle(node);
|
|
2800
|
+
if (!style) continue;
|
|
2801
|
+
const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
|
|
2802
|
+
const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
|
|
2803
|
+
if (!clipsX && !clipsY) continue;
|
|
2804
|
+
const nodeRect = node.getBoundingClientRect();
|
|
2805
|
+
if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
|
|
2806
|
+
if (clipsX) {
|
|
2807
|
+
clipLeft = Math.max(clipLeft, nodeRect.left);
|
|
2808
|
+
clipRight = Math.min(clipRight, nodeRect.right);
|
|
2809
|
+
}
|
|
2810
|
+
if (clipsY) {
|
|
2811
|
+
clipTop = Math.max(clipTop, nodeRect.top);
|
|
2812
|
+
clipBottom = Math.min(clipBottom, nodeRect.bottom);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
|
|
2816
|
+
const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
|
|
2817
|
+
const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
|
|
2818
|
+
const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
|
|
2819
|
+
const visibleWidth = visibleRight - visibleLeft;
|
|
2820
|
+
const visibleHeight = visibleBottom - visibleTop;
|
|
2821
|
+
if (visibleWidth <= 1 || visibleHeight <= 1) {
|
|
2822
|
+
return null;
|
|
2823
|
+
}
|
|
2824
|
+
const interactiveSelector = [
|
|
2825
|
+
"button",
|
|
2826
|
+
'[role="button"]',
|
|
2827
|
+
"a[href]",
|
|
2828
|
+
"label",
|
|
2829
|
+
"input",
|
|
2830
|
+
"textarea",
|
|
2831
|
+
"select",
|
|
2832
|
+
"summary",
|
|
2833
|
+
'[contenteditable="true"]',
|
|
2834
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
2835
|
+
].join(",");
|
|
2836
|
+
const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
|
|
2837
|
+
const sameInteractiveTarget = (pointElement) => {
|
|
2838
|
+
if (!pointElement) return false;
|
|
2839
|
+
if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
|
|
2840
|
+
return true;
|
|
2841
|
+
}
|
|
2842
|
+
if (!targetInteractive || typeof pointElement.closest !== "function") {
|
|
2843
|
+
return false;
|
|
2844
|
+
}
|
|
2845
|
+
return pointElement.closest(interactiveSelector) === targetInteractive;
|
|
2846
|
+
};
|
|
2847
|
+
const cx = visibleLeft + visibleWidth / 2;
|
|
2848
|
+
const cy = visibleTop + visibleHeight / 2;
|
|
2849
|
+
const smallX = Math.min(12, Math.max(2, visibleWidth * 0.18));
|
|
2850
|
+
const smallY = Math.min(12, Math.max(2, visibleHeight * 0.18));
|
|
2851
|
+
const points = [
|
|
2852
|
+
{ x: cx, y: cy },
|
|
2853
|
+
{ x: visibleLeft + smallX, y: visibleTop + smallY },
|
|
2854
|
+
{ x: visibleRight - smallX, y: visibleTop + smallY },
|
|
2855
|
+
{ x: visibleLeft + smallX, y: visibleBottom - smallY },
|
|
2856
|
+
{ x: visibleRight - smallX, y: visibleBottom - smallY },
|
|
2857
|
+
{ x: cx, y: visibleTop + Math.min(10, Math.max(2, visibleHeight * 0.2)) },
|
|
2858
|
+
{ x: cx, y: visibleBottom - Math.min(10, Math.max(2, visibleHeight * 0.2)) },
|
|
2859
|
+
{ x: visibleLeft + Math.min(10, Math.max(2, visibleWidth * 0.2)), y: cy },
|
|
2860
|
+
{ x: visibleRight - Math.min(10, Math.max(2, visibleWidth * 0.2)), y: cy }
|
|
2861
|
+
];
|
|
2862
|
+
const safePoints = points.filter((point) => {
|
|
2863
|
+
const pointElement = document.elementFromPoint(point.x, point.y);
|
|
2864
|
+
return sameInteractiveTarget(pointElement);
|
|
2865
|
+
});
|
|
2866
|
+
const candidates = safePoints.length ? safePoints : points;
|
|
2867
|
+
const chosen = candidates[Math.floor(Math.random() * candidates.length)];
|
|
2868
|
+
if (!chosen) return null;
|
|
2869
|
+
return {
|
|
2870
|
+
x: chosen.x,
|
|
2871
|
+
y: chosen.y
|
|
2872
|
+
};
|
|
2873
|
+
});
|
|
2874
|
+
};
|
|
2875
|
+
var getScrollableRect = async (element) => {
|
|
2876
|
+
return element.evaluate((el) => {
|
|
2877
|
+
const isScrollable = (node) => {
|
|
2878
|
+
const style = window.getComputedStyle(node);
|
|
2879
|
+
if (!style) return false;
|
|
2880
|
+
const overflowY = style.overflowY;
|
|
2881
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
2882
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
2883
|
+
};
|
|
2884
|
+
let current = el;
|
|
2885
|
+
while (current && current !== document.body) {
|
|
2886
|
+
if (isScrollable(current)) {
|
|
2887
|
+
const rect = current.getBoundingClientRect();
|
|
2888
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
2889
|
+
return {
|
|
2890
|
+
x: rect.x,
|
|
2891
|
+
y: rect.y,
|
|
2892
|
+
width: rect.width,
|
|
2893
|
+
height: rect.height
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
current = current.parentElement;
|
|
2898
|
+
}
|
|
2899
|
+
return null;
|
|
2900
|
+
});
|
|
2901
|
+
};
|
|
2902
|
+
var scrollScrollableAncestor = async (element, deltaY) => {
|
|
2903
|
+
return element.evaluate((el, amount) => {
|
|
2904
|
+
const isScrollable = (node) => {
|
|
2905
|
+
const style = window.getComputedStyle(node);
|
|
2906
|
+
if (!style) return false;
|
|
2907
|
+
const overflowY = style.overflowY;
|
|
2908
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
2909
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
2910
|
+
};
|
|
2911
|
+
let current = el;
|
|
2912
|
+
while (current && current !== document.body) {
|
|
2913
|
+
if (isScrollable(current)) {
|
|
2914
|
+
const beforeTop2 = current.scrollTop;
|
|
2915
|
+
current.scrollTop = beforeTop2 + amount;
|
|
2916
|
+
return {
|
|
2917
|
+
scroller: true,
|
|
2918
|
+
moved: current.scrollTop !== beforeTop2,
|
|
2919
|
+
scrollTop: current.scrollTop
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
current = current.parentElement;
|
|
2923
|
+
}
|
|
2924
|
+
const beforeTop = window.scrollY;
|
|
2925
|
+
window.scrollBy(0, amount);
|
|
2926
|
+
return {
|
|
2927
|
+
scroller: null,
|
|
2928
|
+
moved: window.scrollY !== beforeTop,
|
|
2929
|
+
scrollTop: window.scrollY
|
|
2930
|
+
};
|
|
2931
|
+
}, deltaY);
|
|
2932
|
+
};
|
|
2680
2933
|
var dispatchTouchSwipe = async (page, deltaY, options = {}) => {
|
|
2681
2934
|
const viewport = resolveViewport(page);
|
|
2682
|
-
const
|
|
2935
|
+
const rawRect = options.rect || null;
|
|
2936
|
+
const rect = rawRect ? {
|
|
2937
|
+
x: clamp(rawRect.x, 0, viewport.width),
|
|
2938
|
+
y: clamp(rawRect.y, 0, viewport.height),
|
|
2939
|
+
width: clamp(rawRect.width, 0, viewport.width),
|
|
2940
|
+
height: clamp(rawRect.height, 0, viewport.height)
|
|
2941
|
+
} : null;
|
|
2942
|
+
const area = rect && rect.width > 24 && rect.height > 48 ? {
|
|
2943
|
+
left: rect.x,
|
|
2944
|
+
right: Math.min(viewport.width, rect.x + rect.width),
|
|
2945
|
+
top: rect.y,
|
|
2946
|
+
bottom: Math.min(viewport.height, rect.y + rect.height)
|
|
2947
|
+
} : {
|
|
2948
|
+
left: 0,
|
|
2949
|
+
right: viewport.width,
|
|
2950
|
+
top: 0,
|
|
2951
|
+
bottom: viewport.height
|
|
2952
|
+
};
|
|
2953
|
+
const areaWidth = Math.max(1, area.right - area.left);
|
|
2954
|
+
const areaHeight = Math.max(1, area.bottom - area.top);
|
|
2955
|
+
const maxDistance = rect ? Math.max(60, areaHeight * 0.72) : Math.max(120, viewport.height * 0.72);
|
|
2956
|
+
const distance = clamp(Math.abs(deltaY), Math.min(80, maxDistance), maxDistance);
|
|
2683
2957
|
const direction = deltaY >= 0 ? 1 : -1;
|
|
2684
2958
|
const startX = clamp(
|
|
2685
|
-
|
|
2959
|
+
area.left + areaWidth * (0.45 + Math.random() * 0.1),
|
|
2686
2960
|
24,
|
|
2687
2961
|
viewport.width - 24
|
|
2688
2962
|
);
|
|
2689
|
-
const startY = direction > 0 ? clamp(
|
|
2690
|
-
const endY = clamp(startY - direction * distance,
|
|
2963
|
+
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);
|
|
2964
|
+
const endY = clamp(startY - direction * distance, Math.max(16, area.top + 12), Math.min(viewport.height - 16, area.bottom - 12));
|
|
2691
2965
|
const steps = Math.max(4, Math.round(Number(options.steps || 6)));
|
|
2692
2966
|
const durationMs = jitterMs(options.durationMs || 320, 0.35);
|
|
2693
2967
|
let client = null;
|
|
@@ -2790,8 +3064,11 @@ var MobileHumanize = {
|
|
|
2790
3064
|
maxDurationMs = maxSteps * 280 + 1200,
|
|
2791
3065
|
throwOnMissing = false
|
|
2792
3066
|
} = options;
|
|
3067
|
+
const targetDesc = describeTarget(target);
|
|
3068
|
+
logger7.start("humanScroll", `target=${targetDesc}`);
|
|
2793
3069
|
const element = await resolveElement(page, target, { throwOnMissing });
|
|
2794
3070
|
if (!element) {
|
|
3071
|
+
logger7.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${targetDesc}`);
|
|
2795
3072
|
return { element: null, didScroll: false, restore: null };
|
|
2796
3073
|
}
|
|
2797
3074
|
const startTime = Date.now();
|
|
@@ -2799,17 +3076,124 @@ var MobileHumanize = {
|
|
|
2799
3076
|
for (let i = 0; i < maxSteps; i += 1) {
|
|
2800
3077
|
const status = await checkElementVisibility(element);
|
|
2801
3078
|
if (status.code === "VISIBLE") {
|
|
3079
|
+
if (status.isFixed) {
|
|
3080
|
+
logger7.info("humanScroll | fixed/sticky \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
3081
|
+
} else {
|
|
3082
|
+
logger7.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
|
|
3083
|
+
}
|
|
3084
|
+
logger7.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
3085
|
+
return { element, didScroll, restore: null };
|
|
3086
|
+
}
|
|
3087
|
+
if (status.code === "ZERO_DIMENSIONS" || status.code === "NOT_INTERACTABLE") {
|
|
3088
|
+
logger7.warn(`humanScroll | \u5143\u7D20\u4E0D\u53EF\u6EDA\u52A8\u81F3\u53EF\u70B9\u51FB\u72B6\u6001: ${status.reason || status.code}`);
|
|
3089
|
+
return { element, didScroll, restore: null };
|
|
3090
|
+
}
|
|
3091
|
+
const scrollRect = await getScrollableRect(element);
|
|
3092
|
+
if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
|
|
3093
|
+
logger7.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
|
|
2802
3094
|
return { element, didScroll, restore: null };
|
|
2803
3095
|
}
|
|
2804
3096
|
if (Date.now() - startTime > maxDurationMs) {
|
|
2805
|
-
logger7.warn(`humanScroll | mobile timeout (${maxDurationMs}ms)`);
|
|
3097
|
+
logger7.warn(`humanScroll | mobile timeout (${maxDurationMs}ms, status=${status.code}, direction=${status.direction || "unknown"}, fixed=${Boolean(status.isFixed)})`);
|
|
2806
3098
|
return { element, didScroll, restore: null };
|
|
2807
3099
|
}
|
|
2808
|
-
const
|
|
2809
|
-
const
|
|
2810
|
-
|
|
3100
|
+
const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
|
|
3101
|
+
const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
|
|
3102
|
+
logger7.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason || status.code} ${status.direction ? `(${status.direction})` : ""}`);
|
|
3103
|
+
const distance = stepMin + Math.random() * Math.max(1, stepMax - stepMin);
|
|
3104
|
+
let deltaY = status.direction === "up" ? -distance : distance;
|
|
3105
|
+
if (status.code === "OBSTRUCTED") {
|
|
3106
|
+
if (status.obstruction?.isFixed && status.obstruction.top != null) {
|
|
3107
|
+
const obstructionMiddle = (Number(status.obstruction.top || 0) + Number(status.obstruction.bottom || status.obstruction.top || 0)) / 2;
|
|
3108
|
+
const visibleMiddle = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
3109
|
+
deltaY = obstructionMiddle < visibleMiddle ? -distance : distance;
|
|
3110
|
+
} else {
|
|
3111
|
+
const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
3112
|
+
deltaY = status.cy > halfY ? distance : -distance;
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
const beforeWindowState = scrollRect ? await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) : null;
|
|
3116
|
+
const beforeState = scrollRect ? await element.evaluate((el) => {
|
|
3117
|
+
const isScrollable = (node) => {
|
|
3118
|
+
const style = window.getComputedStyle(node);
|
|
3119
|
+
if (!style) return false;
|
|
3120
|
+
const overflowY = style.overflowY;
|
|
3121
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3122
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
3123
|
+
};
|
|
3124
|
+
let current = el;
|
|
3125
|
+
while (current && current !== document.body) {
|
|
3126
|
+
if (isScrollable(current)) {
|
|
3127
|
+
return {
|
|
3128
|
+
kind: "element",
|
|
3129
|
+
top: current.scrollTop,
|
|
3130
|
+
left: current.scrollLeft
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
current = current.parentElement;
|
|
3134
|
+
}
|
|
3135
|
+
return { kind: "window", top: window.scrollY, left: window.scrollX };
|
|
3136
|
+
}) : null;
|
|
3137
|
+
await dispatchTouchSwipe(page, deltaY, { rect: scrollRect });
|
|
3138
|
+
if (scrollRect && beforeWindowState) {
|
|
3139
|
+
const afterWindowState = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
|
|
3140
|
+
if (Math.abs(afterWindowState.x - beforeWindowState.x) > 2 || Math.abs(afterWindowState.y - beforeWindowState.y) > 2) {
|
|
3141
|
+
await page.evaluate((state) => window.scrollTo(state.x, state.y), beforeWindowState);
|
|
3142
|
+
logger7.debug(`humanScroll | \u7A97\u53E3\u6EDA\u52A8\u56DE\u6536 from=${Math.round(afterWindowState.y)} to=${Math.round(beforeWindowState.y)}`);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
if (scrollRect && beforeState) {
|
|
3146
|
+
const afterState = await element.evaluate((el) => {
|
|
3147
|
+
const isScrollable = (node) => {
|
|
3148
|
+
const style = window.getComputedStyle(node);
|
|
3149
|
+
if (!style) return false;
|
|
3150
|
+
const overflowY = style.overflowY;
|
|
3151
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3152
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
3153
|
+
};
|
|
3154
|
+
let current = el;
|
|
3155
|
+
while (current && current !== document.body) {
|
|
3156
|
+
if (isScrollable(current)) {
|
|
3157
|
+
return {
|
|
3158
|
+
kind: "element",
|
|
3159
|
+
top: current.scrollTop,
|
|
3160
|
+
left: current.scrollLeft
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
current = current.parentElement;
|
|
3164
|
+
}
|
|
3165
|
+
return { kind: "window", top: window.scrollY, left: window.scrollX };
|
|
3166
|
+
});
|
|
3167
|
+
const topDelta = Number(afterState.top || 0) - Number(beforeState.top || 0);
|
|
3168
|
+
const leftDelta = Number(afterState.left || 0) - Number(beforeState.left || 0);
|
|
3169
|
+
const expectedDelta = Number(deltaY || 0);
|
|
3170
|
+
const moved = beforeState.kind !== afterState.kind || Math.abs(topDelta) > 2 || Math.abs(leftDelta) > 2;
|
|
3171
|
+
if (!moved) {
|
|
3172
|
+
const fallback = await scrollScrollableAncestor(element, deltaY);
|
|
3173
|
+
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)}`);
|
|
3174
|
+
} 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)) {
|
|
3175
|
+
const residualDelta = expectedDelta - topDelta;
|
|
3176
|
+
if (Math.sign(residualDelta) === Math.sign(expectedDelta) && Math.abs(residualDelta) > 24) {
|
|
3177
|
+
const fallback = await scrollScrollableAncestor(element, residualDelta);
|
|
3178
|
+
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)}`);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
2811
3182
|
didScroll = true;
|
|
2812
3183
|
}
|
|
3184
|
+
try {
|
|
3185
|
+
await element.scrollIntoViewIfNeeded?.();
|
|
3186
|
+
await waitJitter(80, 0.3);
|
|
3187
|
+
const finalStatus = await checkElementVisibility(element);
|
|
3188
|
+
if (finalStatus.code === "VISIBLE") {
|
|
3189
|
+
logger7.info("humanScroll | \u539F\u751F scrollIntoViewIfNeeded \u515C\u5E95\u6210\u529F");
|
|
3190
|
+
logger7.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
3191
|
+
return { element, didScroll: true, restore: null };
|
|
3192
|
+
}
|
|
3193
|
+
} catch (fallbackError) {
|
|
3194
|
+
logger7.debug(`humanScroll | native fallback failed: ${fallbackError?.message || fallbackError}`);
|
|
3195
|
+
}
|
|
3196
|
+
logger7.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
|
|
2813
3197
|
return { element, didScroll, restore: null };
|
|
2814
3198
|
},
|
|
2815
3199
|
async humanClick(page, target, options = {}) {
|
|
@@ -2818,29 +3202,51 @@ var MobileHumanize = {
|
|
|
2818
3202
|
throwOnMissing = true,
|
|
2819
3203
|
scrollIfNeeded = true
|
|
2820
3204
|
} = options;
|
|
2821
|
-
|
|
2822
|
-
|
|
3205
|
+
const targetDesc = describeTarget(target);
|
|
3206
|
+
logger7.start("humanClick", `target=${targetDesc}`);
|
|
3207
|
+
try {
|
|
3208
|
+
if (target == null) {
|
|
3209
|
+
const viewport = resolveViewport(page);
|
|
3210
|
+
await waitJitter(reactionDelay, 0.45);
|
|
3211
|
+
await tapPoint(page, {
|
|
3212
|
+
x: viewport.width * (0.45 + Math.random() * 0.1),
|
|
3213
|
+
y: viewport.height * (0.48 + Math.random() * 0.12)
|
|
3214
|
+
});
|
|
3215
|
+
logger7.success("humanClick", "Tapped current position");
|
|
3216
|
+
return true;
|
|
3217
|
+
}
|
|
3218
|
+
const element = await resolveElement(page, target, { throwOnMissing });
|
|
3219
|
+
if (!element) {
|
|
3220
|
+
logger7.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
|
|
3221
|
+
return false;
|
|
3222
|
+
}
|
|
3223
|
+
if (scrollIfNeeded) {
|
|
3224
|
+
await MobileHumanize.humanScroll(page, element, { throwOnMissing });
|
|
3225
|
+
}
|
|
3226
|
+
const status = await checkElementVisibility(element).catch(() => null);
|
|
3227
|
+
if (status && (status.code === "ZERO_DIMENSIONS" || status.code === "NOT_INTERACTABLE")) {
|
|
3228
|
+
const message = `\u5143\u7D20\u4E0D\u53EF\u70B9\u51FB: ${status.reason || status.code}`;
|
|
3229
|
+
if (throwOnMissing) throw new Error(message);
|
|
3230
|
+
logger7.warn(`humanClick: ${message}\uFF0C\u8DF3\u8FC7\u70B9\u51FB`);
|
|
3231
|
+
return false;
|
|
3232
|
+
}
|
|
3233
|
+
const box = await element.boundingBox();
|
|
3234
|
+
if (!box) {
|
|
3235
|
+
if (throwOnMissing) throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
3236
|
+
logger7.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
|
|
3237
|
+
return false;
|
|
3238
|
+
}
|
|
2823
3239
|
await waitJitter(reactionDelay, 0.45);
|
|
2824
|
-
await
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
3240
|
+
const safePoint = await resolveSafeTapPoint(element).catch(() => null);
|
|
3241
|
+
const tapTarget = safePoint || randomPointInBox(clipBoxToViewport(box, resolveViewport(page)), 0.2);
|
|
3242
|
+
await tapPoint(page, tapTarget);
|
|
3243
|
+
await waitJitter(120, 0.35);
|
|
3244
|
+
logger7.success("humanClick");
|
|
2828
3245
|
return true;
|
|
3246
|
+
} catch (error) {
|
|
3247
|
+
logger7.fail("humanClick", error);
|
|
3248
|
+
throw error;
|
|
2829
3249
|
}
|
|
2830
|
-
const element = await resolveElement(page, target, { throwOnMissing });
|
|
2831
|
-
if (!element) return false;
|
|
2832
|
-
if (scrollIfNeeded) {
|
|
2833
|
-
await MobileHumanize.humanScroll(page, element, { throwOnMissing });
|
|
2834
|
-
}
|
|
2835
|
-
const box = await element.boundingBox();
|
|
2836
|
-
if (!box) {
|
|
2837
|
-
if (throwOnMissing) throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
2838
|
-
return false;
|
|
2839
|
-
}
|
|
2840
|
-
await waitJitter(reactionDelay, 0.45);
|
|
2841
|
-
await tapPoint(page, randomPointInBox(box, 0.2));
|
|
2842
|
-
await waitJitter(120, 0.35);
|
|
2843
|
-
return true;
|
|
2844
3250
|
},
|
|
2845
3251
|
async randomSleep(baseMs, jitterPercent = 0.3) {
|
|
2846
3252
|
await waitJitter(baseMs, jitterPercent);
|