@skrillex1224/playwright-toolkit 2.1.225 → 2.1.227
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/README.md +2 -0
- package/dist/index.cjs +721 -48
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +721 -48
- package/dist/index.js.map +4 -4
- package/index.d.ts +43 -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% 抖动
|
|
@@ -2487,6 +2485,43 @@ var Humanize = {
|
|
|
2487
2485
|
throw error;
|
|
2488
2486
|
}
|
|
2489
2487
|
},
|
|
2488
|
+
/**
|
|
2489
|
+
* 人类化按键 - 模拟用户在当前焦点或指定目标上按下一次键。
|
|
2490
|
+
* @param {import('playwright').Page} page
|
|
2491
|
+
* @param {string|import('playwright').ElementHandle|import('playwright').Locator} targetOrKey - 目标或按键
|
|
2492
|
+
* @param {string|Object} [maybeKey] - 按键或选项
|
|
2493
|
+
* @param {Object} [options]
|
|
2494
|
+
*/
|
|
2495
|
+
async humanPress(page, targetOrKey, maybeKey, options = {}) {
|
|
2496
|
+
const hasTarget = typeof maybeKey === "string";
|
|
2497
|
+
const key = hasTarget ? maybeKey : targetOrKey;
|
|
2498
|
+
const pressOptions = hasTarget ? options : maybeKey || options;
|
|
2499
|
+
const {
|
|
2500
|
+
reactionDelay = 180,
|
|
2501
|
+
holdDelay = 45,
|
|
2502
|
+
focusDelay = 180,
|
|
2503
|
+
scrollIfNeeded = true,
|
|
2504
|
+
throwOnMissing = true,
|
|
2505
|
+
keyboardOptions = {}
|
|
2506
|
+
} = pressOptions || {};
|
|
2507
|
+
const targetDesc = hasTarget ? typeof targetOrKey === "string" ? targetOrKey : "ElementHandle" : "current focus";
|
|
2508
|
+
logger6.start("humanPress", `key=${key}, target=${targetDesc}`);
|
|
2509
|
+
try {
|
|
2510
|
+
if (hasTarget) {
|
|
2511
|
+
await this.humanClick(page, targetOrKey, { reactionDelay: focusDelay, scrollIfNeeded, throwOnMissing });
|
|
2512
|
+
}
|
|
2513
|
+
await delay(this.jitterMs(reactionDelay, 0.45));
|
|
2514
|
+
await page.keyboard.press(key, {
|
|
2515
|
+
...keyboardOptions,
|
|
2516
|
+
delay: this.jitterMs(holdDelay, 0.5)
|
|
2517
|
+
});
|
|
2518
|
+
logger6.success("humanPress");
|
|
2519
|
+
return true;
|
|
2520
|
+
} catch (error) {
|
|
2521
|
+
logger6.fail("humanPress", error);
|
|
2522
|
+
throw error;
|
|
2523
|
+
}
|
|
2524
|
+
},
|
|
2490
2525
|
/**
|
|
2491
2526
|
* 人类化清空输入框 - 模拟人类删除文本的行为
|
|
2492
2527
|
* @param {import('playwright').Page} page
|
|
@@ -2575,6 +2610,141 @@ var Humanize = {
|
|
|
2575
2610
|
}
|
|
2576
2611
|
};
|
|
2577
2612
|
|
|
2613
|
+
// src/internals/humanize/machine.js
|
|
2614
|
+
var resolveDeviceFromPage = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
|
|
2615
|
+
var isPoint = (value) => value && typeof value === "object" && Number.isFinite(Number(value.x)) && Number.isFinite(Number(value.y));
|
|
2616
|
+
var resolveNativeTarget = (page, target) => {
|
|
2617
|
+
if (!target) return null;
|
|
2618
|
+
if (typeof target === "string") {
|
|
2619
|
+
return page.locator(target);
|
|
2620
|
+
}
|
|
2621
|
+
return target;
|
|
2622
|
+
};
|
|
2623
|
+
var normalizeSelectorOptions = (options = {}) => ({
|
|
2624
|
+
targetParent: Boolean(options.targetParent ?? options.parent ?? false),
|
|
2625
|
+
pick: options.pick === "last" || options.last ? "last" : "first",
|
|
2626
|
+
visibleOnly: options.visibleOnly !== false,
|
|
2627
|
+
fallbackDomClick: Boolean(options.fallbackDomClick),
|
|
2628
|
+
forceMouse: Boolean(options.forceMouse)
|
|
2629
|
+
});
|
|
2630
|
+
var MachineHumanize = {
|
|
2631
|
+
async clickPoint(page, pointOrX, maybeY, options = {}) {
|
|
2632
|
+
const clickOptions = isPoint(pointOrX) && maybeY && typeof maybeY === "object" ? maybeY : options;
|
|
2633
|
+
const point = isPoint(pointOrX) ? pointOrX : { x: pointOrX, y: maybeY };
|
|
2634
|
+
const x = Number(point.x);
|
|
2635
|
+
const y = Number(point.y);
|
|
2636
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
2637
|
+
throw new Error(`Invalid click point: ${JSON.stringify(point)}`);
|
|
2638
|
+
}
|
|
2639
|
+
const resolvedDevice = resolveDeviceFromPage(page);
|
|
2640
|
+
if (resolvedDevice === Device.Mobile && !clickOptions.forceMouse && page.touchscreen && typeof page.touchscreen.tap === "function") {
|
|
2641
|
+
await page.touchscreen.tap(x, y);
|
|
2642
|
+
return true;
|
|
2643
|
+
}
|
|
2644
|
+
await page.mouse.click(x, y, clickOptions.mouseOptions || {});
|
|
2645
|
+
return true;
|
|
2646
|
+
},
|
|
2647
|
+
async click(page, target, options = {}) {
|
|
2648
|
+
if (isPoint(target)) {
|
|
2649
|
+
return MachineHumanize.clickPoint(page, target, void 0, options);
|
|
2650
|
+
}
|
|
2651
|
+
const nativeTarget = resolveNativeTarget(page, target);
|
|
2652
|
+
if (!nativeTarget || typeof nativeTarget.click !== "function") {
|
|
2653
|
+
throw new Error("Machine click target does not expose click()");
|
|
2654
|
+
}
|
|
2655
|
+
await nativeTarget.click(options.clickOptions || options);
|
|
2656
|
+
return true;
|
|
2657
|
+
},
|
|
2658
|
+
async focus(page, target, options = {}) {
|
|
2659
|
+
const nativeTarget = resolveNativeTarget(page, target);
|
|
2660
|
+
if (!nativeTarget || typeof nativeTarget.focus !== "function") {
|
|
2661
|
+
throw new Error("Machine focus target does not expose focus()");
|
|
2662
|
+
}
|
|
2663
|
+
await nativeTarget.focus(options);
|
|
2664
|
+
return true;
|
|
2665
|
+
},
|
|
2666
|
+
async fill(page, target, value, options = {}) {
|
|
2667
|
+
const nativeTarget = resolveNativeTarget(page, target);
|
|
2668
|
+
if (!nativeTarget || typeof nativeTarget.fill !== "function") {
|
|
2669
|
+
throw new Error("Machine fill target does not expose fill()");
|
|
2670
|
+
}
|
|
2671
|
+
await nativeTarget.fill(value, options);
|
|
2672
|
+
return true;
|
|
2673
|
+
},
|
|
2674
|
+
async keyboardType(page, text, options = {}) {
|
|
2675
|
+
await page.keyboard.type(text, options);
|
|
2676
|
+
return true;
|
|
2677
|
+
},
|
|
2678
|
+
async type(page, targetOrText, maybeText, options = {}) {
|
|
2679
|
+
if (maybeText == null || typeof maybeText === "object" && !Array.isArray(maybeText)) {
|
|
2680
|
+
return MachineHumanize.keyboardType(page, targetOrText, maybeText || options);
|
|
2681
|
+
}
|
|
2682
|
+
const nativeTarget = resolveNativeTarget(page, targetOrText);
|
|
2683
|
+
if (!nativeTarget || typeof nativeTarget.type !== "function") {
|
|
2684
|
+
throw new Error("Machine type target does not expose type()");
|
|
2685
|
+
}
|
|
2686
|
+
await nativeTarget.type(maybeText, options);
|
|
2687
|
+
return true;
|
|
2688
|
+
},
|
|
2689
|
+
async selectorCenterPoint(page, selector, options = {}) {
|
|
2690
|
+
const normalizedOptions = normalizeSelectorOptions(options);
|
|
2691
|
+
return page.evaluate(({ innerSelector, innerOptions }) => {
|
|
2692
|
+
const nodes = Array.from(document.querySelectorAll(innerSelector));
|
|
2693
|
+
const ordered = innerOptions.pick === "last" ? nodes.reverse() : nodes;
|
|
2694
|
+
let rect = null;
|
|
2695
|
+
for (const node of ordered) {
|
|
2696
|
+
if (!node) continue;
|
|
2697
|
+
const target = innerOptions.targetParent ? node.parentElement || node : node;
|
|
2698
|
+
rect = target.getBoundingClientRect?.() || null;
|
|
2699
|
+
const isVisible = Boolean(rect && rect.width > 0 && rect.height > 0);
|
|
2700
|
+
if (innerOptions.visibleOnly && !isVisible) continue;
|
|
2701
|
+
break;
|
|
2702
|
+
}
|
|
2703
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return null;
|
|
2704
|
+
return {
|
|
2705
|
+
x: rect.left + rect.width / 2,
|
|
2706
|
+
y: rect.top + rect.height / 2
|
|
2707
|
+
};
|
|
2708
|
+
}, { innerSelector: selector, innerOptions: normalizedOptions });
|
|
2709
|
+
},
|
|
2710
|
+
async domClick(page, selector, options = {}) {
|
|
2711
|
+
const normalizedOptions = normalizeSelectorOptions(options);
|
|
2712
|
+
return page.evaluate(({ innerSelector, innerOptions }) => {
|
|
2713
|
+
const nodes = Array.from(document.querySelectorAll(innerSelector));
|
|
2714
|
+
const ordered = innerOptions.pick === "last" ? nodes.reverse() : nodes;
|
|
2715
|
+
let target = null;
|
|
2716
|
+
for (const node of ordered) {
|
|
2717
|
+
if (!node) continue;
|
|
2718
|
+
const candidate = innerOptions.targetParent ? node.parentElement || node : node;
|
|
2719
|
+
const rect = candidate.getBoundingClientRect?.() || null;
|
|
2720
|
+
const isVisible = Boolean(rect && rect.width > 0 && rect.height > 0);
|
|
2721
|
+
if (innerOptions.visibleOnly && !isVisible) continue;
|
|
2722
|
+
target = candidate;
|
|
2723
|
+
break;
|
|
2724
|
+
}
|
|
2725
|
+
if (!target || typeof target.click !== "function") return false;
|
|
2726
|
+
target.click();
|
|
2727
|
+
return true;
|
|
2728
|
+
}, { innerSelector: selector, innerOptions: normalizedOptions });
|
|
2729
|
+
},
|
|
2730
|
+
async clickSelectorCenter(page, selector, options = {}) {
|
|
2731
|
+
const normalizedOptions = normalizeSelectorOptions(options);
|
|
2732
|
+
try {
|
|
2733
|
+
const point = await MachineHumanize.selectorCenterPoint(page, selector, normalizedOptions);
|
|
2734
|
+
if (point) {
|
|
2735
|
+
await MachineHumanize.clickPoint(page, point, void 0, normalizedOptions);
|
|
2736
|
+
return true;
|
|
2737
|
+
}
|
|
2738
|
+
} catch (error) {
|
|
2739
|
+
if (!normalizedOptions.fallbackDomClick) throw error;
|
|
2740
|
+
}
|
|
2741
|
+
if (normalizedOptions.fallbackDomClick) {
|
|
2742
|
+
return MachineHumanize.domClick(page, selector, normalizedOptions);
|
|
2743
|
+
}
|
|
2744
|
+
return false;
|
|
2745
|
+
}
|
|
2746
|
+
};
|
|
2747
|
+
|
|
2578
2748
|
// src/internals/humanize/shared.js
|
|
2579
2749
|
import delay2 from "delay";
|
|
2580
2750
|
var jitterMs = (base, jitterPercent = 0.3) => {
|
|
@@ -2609,16 +2779,43 @@ var logger7 = createInternalLogger("Humanize.Mobile");
|
|
|
2609
2779
|
var initializedPages = /* @__PURE__ */ new WeakSet();
|
|
2610
2780
|
var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
2611
2781
|
var resolveViewport = (page) => page?.viewportSize?.() || { width: 390, height: 844 };
|
|
2782
|
+
var describeTarget = (target) => {
|
|
2783
|
+
if (target == null) return "Current Position";
|
|
2784
|
+
return typeof target === "string" ? target : "ElementHandle";
|
|
2785
|
+
};
|
|
2786
|
+
var clipBoxToViewport = (box, viewport) => {
|
|
2787
|
+
if (!box || !viewport) return box;
|
|
2788
|
+
const left = clamp(box.x, 0, viewport.width);
|
|
2789
|
+
const top = clamp(box.y, 0, viewport.height);
|
|
2790
|
+
const right = clamp(box.x + box.width, 0, viewport.width);
|
|
2791
|
+
const bottom = clamp(box.y + box.height, 0, viewport.height);
|
|
2792
|
+
if (right - left > 1 && bottom - top > 1) {
|
|
2793
|
+
return {
|
|
2794
|
+
x: left,
|
|
2795
|
+
y: top,
|
|
2796
|
+
width: right - left,
|
|
2797
|
+
height: bottom - top
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
return box;
|
|
2801
|
+
};
|
|
2612
2802
|
var checkElementVisibility = async (element) => {
|
|
2613
2803
|
return element.evaluate((el) => {
|
|
2804
|
+
const targetStyle = window.getComputedStyle(el);
|
|
2805
|
+
if (!targetStyle || targetStyle.display === "none" || targetStyle.visibility === "hidden" || targetStyle.visibility === "collapse") {
|
|
2806
|
+
return { code: "NOT_INTERACTABLE", reason: "\u5143\u7D20\u4E0D\u53EF\u89C1", direction: "down" };
|
|
2807
|
+
}
|
|
2614
2808
|
const rect = el.getBoundingClientRect();
|
|
2615
2809
|
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
2616
|
-
return { code: "ZERO_DIMENSIONS", direction: "down" };
|
|
2810
|
+
return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6", direction: "down" };
|
|
2617
2811
|
}
|
|
2618
2812
|
const viewW = window.innerWidth;
|
|
2619
2813
|
const viewH = window.innerHeight;
|
|
2620
|
-
const
|
|
2621
|
-
|
|
2814
|
+
const centerY = rect.top + rect.height / 2;
|
|
2815
|
+
let clipLeft = 0;
|
|
2816
|
+
let clipRight = viewW;
|
|
2817
|
+
let clipTop = 0;
|
|
2818
|
+
let clipBottom = viewH;
|
|
2622
2819
|
let isFixed = false;
|
|
2623
2820
|
for (let node = el; node && node !== document.body; node = node.parentElement) {
|
|
2624
2821
|
const style = window.getComputedStyle(node);
|
|
@@ -2627,20 +2824,101 @@ var checkElementVisibility = async (element) => {
|
|
|
2627
2824
|
break;
|
|
2628
2825
|
}
|
|
2629
2826
|
}
|
|
2630
|
-
|
|
2827
|
+
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
2828
|
+
const style = window.getComputedStyle(node);
|
|
2829
|
+
if (!style) continue;
|
|
2830
|
+
const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
|
|
2831
|
+
const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
|
|
2832
|
+
if (!clipsX && !clipsY) continue;
|
|
2833
|
+
const nodeRect = node.getBoundingClientRect();
|
|
2834
|
+
if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
|
|
2835
|
+
if (clipsX) {
|
|
2836
|
+
clipLeft = Math.max(clipLeft, nodeRect.left);
|
|
2837
|
+
clipRight = Math.min(clipRight, nodeRect.right);
|
|
2838
|
+
}
|
|
2839
|
+
if (clipsY) {
|
|
2840
|
+
clipTop = Math.max(clipTop, nodeRect.top);
|
|
2841
|
+
clipBottom = Math.min(clipBottom, nodeRect.bottom);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
|
|
2845
|
+
const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
|
|
2846
|
+
const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
|
|
2847
|
+
const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
|
|
2848
|
+
const visibleWidth = visibleRight - visibleLeft;
|
|
2849
|
+
const visibleHeight = visibleBottom - visibleTop;
|
|
2850
|
+
const cx = visibleLeft + visibleWidth / 2;
|
|
2851
|
+
const cy = visibleTop + visibleHeight / 2;
|
|
2852
|
+
if (visibleWidth <= 1 || visibleHeight <= 1) {
|
|
2631
2853
|
return {
|
|
2632
2854
|
code: "OUT_OF_VIEWPORT",
|
|
2633
|
-
|
|
2634
|
-
|
|
2855
|
+
reason: "\u4E0D\u5728\u89C6\u53E3\u5185",
|
|
2856
|
+
direction: centerY < clipTop ? "up" : "down",
|
|
2857
|
+
cy: centerY,
|
|
2635
2858
|
viewH,
|
|
2636
2859
|
isFixed
|
|
2637
2860
|
};
|
|
2638
2861
|
}
|
|
2639
|
-
const
|
|
2640
|
-
|
|
2862
|
+
const interactiveSelector = [
|
|
2863
|
+
"button",
|
|
2864
|
+
'[role="button"]',
|
|
2865
|
+
"a[href]",
|
|
2866
|
+
"label",
|
|
2867
|
+
"input",
|
|
2868
|
+
"textarea",
|
|
2869
|
+
"select",
|
|
2870
|
+
"summary",
|
|
2871
|
+
'[contenteditable="true"]',
|
|
2872
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
2873
|
+
].join(",");
|
|
2874
|
+
const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
|
|
2875
|
+
const sameInteractiveTarget = (pointElement) => {
|
|
2876
|
+
if (!pointElement) return false;
|
|
2877
|
+
if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
|
|
2878
|
+
return true;
|
|
2879
|
+
}
|
|
2880
|
+
if (!targetInteractive || typeof pointElement.closest !== "function") {
|
|
2881
|
+
return false;
|
|
2882
|
+
}
|
|
2883
|
+
return pointElement.closest(interactiveSelector) === targetInteractive;
|
|
2884
|
+
};
|
|
2885
|
+
const describeElement = (node) => {
|
|
2886
|
+
if (!node) return null;
|
|
2887
|
+
const className = typeof node.className === "string" ? node.className : node.className && typeof node.className.baseVal === "string" ? node.className.baseVal : "";
|
|
2888
|
+
const rect2 = node.getBoundingClientRect?.();
|
|
2889
|
+
const style = window.getComputedStyle(node);
|
|
2890
|
+
return {
|
|
2891
|
+
tag: node.tagName,
|
|
2892
|
+
id: node.id || "",
|
|
2893
|
+
className,
|
|
2894
|
+
isFixed: Boolean(style && (style.position === "fixed" || style.position === "sticky")),
|
|
2895
|
+
top: rect2 ? rect2.top : null,
|
|
2896
|
+
bottom: rect2 ? rect2.bottom : null,
|
|
2897
|
+
left: rect2 ? rect2.left : null,
|
|
2898
|
+
right: rect2 ? rect2.right : null
|
|
2899
|
+
};
|
|
2900
|
+
};
|
|
2901
|
+
const samplePoints = [
|
|
2902
|
+
{ x: cx, y: cy },
|
|
2903
|
+
{ x: visibleLeft + Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
|
|
2904
|
+
{ x: visibleRight - Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
|
|
2905
|
+
{ x: cx, y: visibleTop + Math.min(8, Math.max(1, visibleHeight * 0.25)) },
|
|
2906
|
+
{ x: cx, y: visibleBottom - Math.min(8, Math.max(1, visibleHeight * 0.25)) }
|
|
2907
|
+
];
|
|
2908
|
+
let obstruction = null;
|
|
2909
|
+
for (const point of samplePoints) {
|
|
2910
|
+
const pointElement = document.elementFromPoint(point.x, point.y);
|
|
2911
|
+
if (sameInteractiveTarget(pointElement)) {
|
|
2912
|
+
return { code: "VISIBLE", isFixed };
|
|
2913
|
+
}
|
|
2914
|
+
obstruction = obstruction || describeElement(pointElement);
|
|
2915
|
+
}
|
|
2916
|
+
if (obstruction) {
|
|
2641
2917
|
return {
|
|
2642
2918
|
code: "OBSTRUCTED",
|
|
2919
|
+
reason: "\u88AB\u906E\u6321",
|
|
2643
2920
|
direction: cy > viewH / 2 ? "down" : "up",
|
|
2921
|
+
obstruction,
|
|
2644
2922
|
cy,
|
|
2645
2923
|
viewH,
|
|
2646
2924
|
isFixed
|
|
@@ -2649,17 +2927,228 @@ var checkElementVisibility = async (element) => {
|
|
|
2649
2927
|
return { code: "VISIBLE", isFixed };
|
|
2650
2928
|
});
|
|
2651
2929
|
};
|
|
2930
|
+
var resolveSafeTapPoint = async (element) => {
|
|
2931
|
+
return element.evaluate((el) => {
|
|
2932
|
+
const rect = el.getBoundingClientRect();
|
|
2933
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
2934
|
+
return null;
|
|
2935
|
+
}
|
|
2936
|
+
const viewW = window.innerWidth;
|
|
2937
|
+
const viewH = window.innerHeight;
|
|
2938
|
+
let clipLeft = 0;
|
|
2939
|
+
let clipRight = viewW;
|
|
2940
|
+
let clipTop = 0;
|
|
2941
|
+
let clipBottom = viewH;
|
|
2942
|
+
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
2943
|
+
const style = window.getComputedStyle(node);
|
|
2944
|
+
if (!style) continue;
|
|
2945
|
+
const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
|
|
2946
|
+
const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
|
|
2947
|
+
if (!clipsX && !clipsY) continue;
|
|
2948
|
+
const nodeRect = node.getBoundingClientRect();
|
|
2949
|
+
if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
|
|
2950
|
+
if (clipsX) {
|
|
2951
|
+
clipLeft = Math.max(clipLeft, nodeRect.left);
|
|
2952
|
+
clipRight = Math.min(clipRight, nodeRect.right);
|
|
2953
|
+
}
|
|
2954
|
+
if (clipsY) {
|
|
2955
|
+
clipTop = Math.max(clipTop, nodeRect.top);
|
|
2956
|
+
clipBottom = Math.min(clipBottom, nodeRect.bottom);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
|
|
2960
|
+
const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
|
|
2961
|
+
const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
|
|
2962
|
+
const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
|
|
2963
|
+
const visibleWidth = visibleRight - visibleLeft;
|
|
2964
|
+
const visibleHeight = visibleBottom - visibleTop;
|
|
2965
|
+
if (visibleWidth <= 1 || visibleHeight <= 1) {
|
|
2966
|
+
return null;
|
|
2967
|
+
}
|
|
2968
|
+
const interactiveSelector = [
|
|
2969
|
+
"button",
|
|
2970
|
+
'[role="button"]',
|
|
2971
|
+
"a[href]",
|
|
2972
|
+
"label",
|
|
2973
|
+
"input",
|
|
2974
|
+
"textarea",
|
|
2975
|
+
"select",
|
|
2976
|
+
"summary",
|
|
2977
|
+
'[contenteditable="true"]',
|
|
2978
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
2979
|
+
].join(",");
|
|
2980
|
+
const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
|
|
2981
|
+
const sameInteractiveTarget = (pointElement) => {
|
|
2982
|
+
if (!pointElement) return false;
|
|
2983
|
+
if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
|
|
2984
|
+
return true;
|
|
2985
|
+
}
|
|
2986
|
+
if (!targetInteractive || typeof pointElement.closest !== "function") {
|
|
2987
|
+
return false;
|
|
2988
|
+
}
|
|
2989
|
+
return pointElement.closest(interactiveSelector) === targetInteractive;
|
|
2990
|
+
};
|
|
2991
|
+
const cx = visibleLeft + visibleWidth / 2;
|
|
2992
|
+
const cy = visibleTop + visibleHeight / 2;
|
|
2993
|
+
const smallX = Math.min(12, Math.max(2, visibleWidth * 0.18));
|
|
2994
|
+
const smallY = Math.min(12, Math.max(2, visibleHeight * 0.18));
|
|
2995
|
+
const points = [
|
|
2996
|
+
{ x: cx, y: cy },
|
|
2997
|
+
{ x: visibleLeft + smallX, y: visibleTop + smallY },
|
|
2998
|
+
{ x: visibleRight - smallX, y: visibleTop + smallY },
|
|
2999
|
+
{ x: visibleLeft + smallX, y: visibleBottom - smallY },
|
|
3000
|
+
{ x: visibleRight - smallX, y: visibleBottom - smallY },
|
|
3001
|
+
{ x: cx, y: visibleTop + Math.min(10, Math.max(2, visibleHeight * 0.2)) },
|
|
3002
|
+
{ x: cx, y: visibleBottom - Math.min(10, Math.max(2, visibleHeight * 0.2)) },
|
|
3003
|
+
{ x: visibleLeft + Math.min(10, Math.max(2, visibleWidth * 0.2)), y: cy },
|
|
3004
|
+
{ x: visibleRight - Math.min(10, Math.max(2, visibleWidth * 0.2)), y: cy }
|
|
3005
|
+
];
|
|
3006
|
+
const safePoints = points.filter((point) => {
|
|
3007
|
+
const pointElement = document.elementFromPoint(point.x, point.y);
|
|
3008
|
+
return sameInteractiveTarget(pointElement);
|
|
3009
|
+
});
|
|
3010
|
+
const candidates = safePoints.length ? safePoints : points;
|
|
3011
|
+
const chosen = candidates[Math.floor(Math.random() * candidates.length)];
|
|
3012
|
+
if (!chosen) return null;
|
|
3013
|
+
return {
|
|
3014
|
+
x: chosen.x,
|
|
3015
|
+
y: chosen.y
|
|
3016
|
+
};
|
|
3017
|
+
});
|
|
3018
|
+
};
|
|
3019
|
+
var getScrollableRect = async (element) => {
|
|
3020
|
+
return element.evaluate((el) => {
|
|
3021
|
+
const isScrollable = (node) => {
|
|
3022
|
+
const style = window.getComputedStyle(node);
|
|
3023
|
+
if (!style) return false;
|
|
3024
|
+
const overflowY = style.overflowY;
|
|
3025
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3026
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
3027
|
+
};
|
|
3028
|
+
let current = el;
|
|
3029
|
+
while (current && current !== document.body) {
|
|
3030
|
+
if (isScrollable(current)) {
|
|
3031
|
+
const rect = current.getBoundingClientRect();
|
|
3032
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
3033
|
+
return {
|
|
3034
|
+
x: rect.x,
|
|
3035
|
+
y: rect.y,
|
|
3036
|
+
width: rect.width,
|
|
3037
|
+
height: rect.height
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
current = current.parentElement;
|
|
3042
|
+
}
|
|
3043
|
+
return null;
|
|
3044
|
+
});
|
|
3045
|
+
};
|
|
3046
|
+
var scrollScrollableAncestor = async (element, deltaY) => {
|
|
3047
|
+
return element.evaluate((el, amount) => {
|
|
3048
|
+
const isScrollable = (node) => {
|
|
3049
|
+
const style = window.getComputedStyle(node);
|
|
3050
|
+
if (!style) return false;
|
|
3051
|
+
const overflowY = style.overflowY;
|
|
3052
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3053
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
3054
|
+
};
|
|
3055
|
+
let current = el;
|
|
3056
|
+
while (current && current !== document.body) {
|
|
3057
|
+
if (isScrollable(current)) {
|
|
3058
|
+
const beforeTop2 = current.scrollTop;
|
|
3059
|
+
current.scrollTop = beforeTop2 + amount;
|
|
3060
|
+
return {
|
|
3061
|
+
scroller: true,
|
|
3062
|
+
moved: current.scrollTop !== beforeTop2,
|
|
3063
|
+
scrollTop: current.scrollTop
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
current = current.parentElement;
|
|
3067
|
+
}
|
|
3068
|
+
const beforeTop = window.scrollY;
|
|
3069
|
+
window.scrollBy(0, amount);
|
|
3070
|
+
return {
|
|
3071
|
+
scroller: null,
|
|
3072
|
+
moved: window.scrollY !== beforeTop,
|
|
3073
|
+
scrollTop: window.scrollY
|
|
3074
|
+
};
|
|
3075
|
+
}, deltaY);
|
|
3076
|
+
};
|
|
3077
|
+
var scrollAwayFromObstruction = async (element, status) => {
|
|
3078
|
+
return element.evaluate((el, innerStatus) => {
|
|
3079
|
+
const isScrollable = (node) => {
|
|
3080
|
+
const style = window.getComputedStyle(node);
|
|
3081
|
+
if (!style) return false;
|
|
3082
|
+
const overflowY = style.overflowY;
|
|
3083
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3084
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
3085
|
+
};
|
|
3086
|
+
let scroller = el;
|
|
3087
|
+
while (scroller && scroller !== document.body) {
|
|
3088
|
+
if (isScrollable(scroller)) break;
|
|
3089
|
+
scroller = scroller.parentElement;
|
|
3090
|
+
}
|
|
3091
|
+
if (!scroller || scroller === document.body) {
|
|
3092
|
+
return { moved: false, scrollTop: 0, deltaY: 0 };
|
|
3093
|
+
}
|
|
3094
|
+
const rect = el.getBoundingClientRect();
|
|
3095
|
+
const scrollerRect = scroller.getBoundingClientRect();
|
|
3096
|
+
const obstruction = innerStatus?.obstruction || {};
|
|
3097
|
+
const obstructionTop = Number(obstruction.top);
|
|
3098
|
+
const obstructionBottom = Number(obstruction.bottom);
|
|
3099
|
+
const obstructionMiddle = Number.isFinite(obstructionTop) && Number.isFinite(obstructionBottom) ? (obstructionTop + obstructionBottom) / 2 : scrollerRect.top + scrollerRect.height / 2;
|
|
3100
|
+
const scrollerMiddle = scrollerRect.top + scrollerRect.height / 2;
|
|
3101
|
+
const padding = 18;
|
|
3102
|
+
let deltaY = 0;
|
|
3103
|
+
if (Number.isFinite(obstructionTop) && rect.bottom > obstructionTop && obstructionTop >= scrollerRect.top && obstructionTop <= scrollerRect.bottom && obstructionMiddle >= scrollerMiddle) {
|
|
3104
|
+
deltaY = rect.bottom - obstructionTop + padding;
|
|
3105
|
+
} else if (Number.isFinite(obstructionBottom) && rect.top < obstructionBottom && obstructionBottom >= scrollerRect.top && obstructionBottom <= scrollerRect.bottom && obstructionMiddle < scrollerMiddle) {
|
|
3106
|
+
deltaY = rect.top - obstructionBottom - padding;
|
|
3107
|
+
} else {
|
|
3108
|
+
const fallbackDistance = Math.max(48, Math.min(180, scrollerRect.height * 0.45));
|
|
3109
|
+
deltaY = obstructionMiddle >= scrollerMiddle ? fallbackDistance : -fallbackDistance;
|
|
3110
|
+
}
|
|
3111
|
+
const beforeTop = scroller.scrollTop;
|
|
3112
|
+
scroller.scrollTop = beforeTop + deltaY;
|
|
3113
|
+
return {
|
|
3114
|
+
moved: Math.abs(scroller.scrollTop - beforeTop) > 2,
|
|
3115
|
+
scrollTop: scroller.scrollTop,
|
|
3116
|
+
deltaY
|
|
3117
|
+
};
|
|
3118
|
+
}, status);
|
|
3119
|
+
};
|
|
2652
3120
|
var dispatchTouchSwipe = async (page, deltaY, options = {}) => {
|
|
2653
3121
|
const viewport = resolveViewport(page);
|
|
2654
|
-
const
|
|
3122
|
+
const rawRect = options.rect || null;
|
|
3123
|
+
const rect = rawRect ? {
|
|
3124
|
+
x: clamp(rawRect.x, 0, viewport.width),
|
|
3125
|
+
y: clamp(rawRect.y, 0, viewport.height),
|
|
3126
|
+
width: clamp(rawRect.width, 0, viewport.width),
|
|
3127
|
+
height: clamp(rawRect.height, 0, viewport.height)
|
|
3128
|
+
} : null;
|
|
3129
|
+
const area = rect && rect.width > 24 && rect.height > 48 ? {
|
|
3130
|
+
left: rect.x,
|
|
3131
|
+
right: Math.min(viewport.width, rect.x + rect.width),
|
|
3132
|
+
top: rect.y,
|
|
3133
|
+
bottom: Math.min(viewport.height, rect.y + rect.height)
|
|
3134
|
+
} : {
|
|
3135
|
+
left: 0,
|
|
3136
|
+
right: viewport.width,
|
|
3137
|
+
top: 0,
|
|
3138
|
+
bottom: viewport.height
|
|
3139
|
+
};
|
|
3140
|
+
const areaWidth = Math.max(1, area.right - area.left);
|
|
3141
|
+
const areaHeight = Math.max(1, area.bottom - area.top);
|
|
3142
|
+
const maxDistance = rect ? Math.max(60, areaHeight * 0.72) : Math.max(120, viewport.height * 0.72);
|
|
3143
|
+
const distance = clamp(Math.abs(deltaY), Math.min(80, maxDistance), maxDistance);
|
|
2655
3144
|
const direction = deltaY >= 0 ? 1 : -1;
|
|
2656
3145
|
const startX = clamp(
|
|
2657
|
-
|
|
3146
|
+
area.left + areaWidth * (0.45 + Math.random() * 0.1),
|
|
2658
3147
|
24,
|
|
2659
3148
|
viewport.width - 24
|
|
2660
3149
|
);
|
|
2661
|
-
const startY = direction > 0 ? clamp(
|
|
2662
|
-
const endY = clamp(startY - direction * distance,
|
|
3150
|
+
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);
|
|
3151
|
+
const endY = clamp(startY - direction * distance, Math.max(16, area.top + 12), Math.min(viewport.height - 16, area.bottom - 12));
|
|
2663
3152
|
const steps = Math.max(4, Math.round(Number(options.steps || 6)));
|
|
2664
3153
|
const durationMs = jitterMs(options.durationMs || 320, 0.35);
|
|
2665
3154
|
let client = null;
|
|
@@ -2762,8 +3251,11 @@ var MobileHumanize = {
|
|
|
2762
3251
|
maxDurationMs = maxSteps * 280 + 1200,
|
|
2763
3252
|
throwOnMissing = false
|
|
2764
3253
|
} = options;
|
|
3254
|
+
const targetDesc = describeTarget(target);
|
|
3255
|
+
logger7.start("humanScroll", `target=${targetDesc}`);
|
|
2765
3256
|
const element = await resolveElement(page, target, { throwOnMissing });
|
|
2766
3257
|
if (!element) {
|
|
3258
|
+
logger7.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${targetDesc}`);
|
|
2767
3259
|
return { element: null, didScroll: false, restore: null };
|
|
2768
3260
|
}
|
|
2769
3261
|
const startTime = Date.now();
|
|
@@ -2771,17 +3263,133 @@ var MobileHumanize = {
|
|
|
2771
3263
|
for (let i = 0; i < maxSteps; i += 1) {
|
|
2772
3264
|
const status = await checkElementVisibility(element);
|
|
2773
3265
|
if (status.code === "VISIBLE") {
|
|
3266
|
+
if (status.isFixed) {
|
|
3267
|
+
logger7.info("humanScroll | fixed/sticky \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
3268
|
+
} else {
|
|
3269
|
+
logger7.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
|
|
3270
|
+
}
|
|
3271
|
+
logger7.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
3272
|
+
return { element, didScroll, restore: null };
|
|
3273
|
+
}
|
|
3274
|
+
if (status.code === "ZERO_DIMENSIONS" || status.code === "NOT_INTERACTABLE") {
|
|
3275
|
+
logger7.warn(`humanScroll | \u5143\u7D20\u4E0D\u53EF\u6EDA\u52A8\u81F3\u53EF\u70B9\u51FB\u72B6\u6001: ${status.reason || status.code}`);
|
|
2774
3276
|
return { element, didScroll, restore: null };
|
|
2775
3277
|
}
|
|
3278
|
+
const scrollRect = await getScrollableRect(element);
|
|
3279
|
+
if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
|
|
3280
|
+
logger7.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
|
|
3281
|
+
return { element, didScroll, restore: null };
|
|
3282
|
+
}
|
|
3283
|
+
if (scrollRect && status.code === "OBSTRUCTED" && status.obstruction?.isFixed) {
|
|
3284
|
+
const moved = await scrollAwayFromObstruction(element, status);
|
|
3285
|
+
if (moved.moved) {
|
|
3286
|
+
logger7.debug(`humanScroll | sticky/fixed \u906E\u6321\u8865\u507F\u6EDA\u52A8 top=${Math.round(moved.scrollTop || 0)}`);
|
|
3287
|
+
await waitJitter(90, 0.3);
|
|
3288
|
+
didScroll = true;
|
|
3289
|
+
continue;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
2776
3292
|
if (Date.now() - startTime > maxDurationMs) {
|
|
2777
|
-
logger7.warn(`humanScroll | mobile timeout (${maxDurationMs}ms)`);
|
|
3293
|
+
logger7.warn(`humanScroll | mobile timeout (${maxDurationMs}ms, status=${status.code}, direction=${status.direction || "unknown"}, fixed=${Boolean(status.isFixed)})`);
|
|
2778
3294
|
return { element, didScroll, restore: null };
|
|
2779
3295
|
}
|
|
2780
|
-
const
|
|
2781
|
-
const
|
|
2782
|
-
|
|
3296
|
+
const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
|
|
3297
|
+
const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
|
|
3298
|
+
logger7.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason || status.code} ${status.direction ? `(${status.direction})` : ""}`);
|
|
3299
|
+
const distance = stepMin + Math.random() * Math.max(1, stepMax - stepMin);
|
|
3300
|
+
let deltaY = status.direction === "up" ? -distance : distance;
|
|
3301
|
+
if (status.code === "OBSTRUCTED") {
|
|
3302
|
+
if (status.obstruction?.isFixed && status.obstruction.top != null) {
|
|
3303
|
+
const obstructionMiddle = (Number(status.obstruction.top || 0) + Number(status.obstruction.bottom || status.obstruction.top || 0)) / 2;
|
|
3304
|
+
const visibleMiddle = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
3305
|
+
deltaY = obstructionMiddle < visibleMiddle ? -distance : distance;
|
|
3306
|
+
} else {
|
|
3307
|
+
const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
3308
|
+
deltaY = status.cy > halfY ? distance : -distance;
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
const beforeWindowState = scrollRect ? await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) : null;
|
|
3312
|
+
const beforeState = scrollRect ? await element.evaluate((el) => {
|
|
3313
|
+
const isScrollable = (node) => {
|
|
3314
|
+
const style = window.getComputedStyle(node);
|
|
3315
|
+
if (!style) return false;
|
|
3316
|
+
const overflowY = style.overflowY;
|
|
3317
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3318
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
3319
|
+
};
|
|
3320
|
+
let current = el;
|
|
3321
|
+
while (current && current !== document.body) {
|
|
3322
|
+
if (isScrollable(current)) {
|
|
3323
|
+
return {
|
|
3324
|
+
kind: "element",
|
|
3325
|
+
top: current.scrollTop,
|
|
3326
|
+
left: current.scrollLeft
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
current = current.parentElement;
|
|
3330
|
+
}
|
|
3331
|
+
return { kind: "window", top: window.scrollY, left: window.scrollX };
|
|
3332
|
+
}) : null;
|
|
3333
|
+
await dispatchTouchSwipe(page, deltaY, { rect: scrollRect });
|
|
3334
|
+
if (scrollRect && beforeWindowState) {
|
|
3335
|
+
const afterWindowState = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
|
|
3336
|
+
if (Math.abs(afterWindowState.x - beforeWindowState.x) > 2 || Math.abs(afterWindowState.y - beforeWindowState.y) > 2) {
|
|
3337
|
+
await page.evaluate((state) => window.scrollTo(state.x, state.y), beforeWindowState);
|
|
3338
|
+
logger7.debug(`humanScroll | \u7A97\u53E3\u6EDA\u52A8\u56DE\u6536 from=${Math.round(afterWindowState.y)} to=${Math.round(beforeWindowState.y)}`);
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
if (scrollRect && beforeState) {
|
|
3342
|
+
const afterState = await element.evaluate((el) => {
|
|
3343
|
+
const isScrollable = (node) => {
|
|
3344
|
+
const style = window.getComputedStyle(node);
|
|
3345
|
+
if (!style) return false;
|
|
3346
|
+
const overflowY = style.overflowY;
|
|
3347
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3348
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
3349
|
+
};
|
|
3350
|
+
let current = el;
|
|
3351
|
+
while (current && current !== document.body) {
|
|
3352
|
+
if (isScrollable(current)) {
|
|
3353
|
+
return {
|
|
3354
|
+
kind: "element",
|
|
3355
|
+
top: current.scrollTop,
|
|
3356
|
+
left: current.scrollLeft
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
current = current.parentElement;
|
|
3360
|
+
}
|
|
3361
|
+
return { kind: "window", top: window.scrollY, left: window.scrollX };
|
|
3362
|
+
});
|
|
3363
|
+
const topDelta = Number(afterState.top || 0) - Number(beforeState.top || 0);
|
|
3364
|
+
const leftDelta = Number(afterState.left || 0) - Number(beforeState.left || 0);
|
|
3365
|
+
const expectedDelta = Number(deltaY || 0);
|
|
3366
|
+
const moved = beforeState.kind !== afterState.kind || Math.abs(topDelta) > 2 || Math.abs(leftDelta) > 2;
|
|
3367
|
+
if (!moved) {
|
|
3368
|
+
const fallback = await scrollScrollableAncestor(element, deltaY);
|
|
3369
|
+
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)}`);
|
|
3370
|
+
} 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)) {
|
|
3371
|
+
const residualDelta = expectedDelta - topDelta;
|
|
3372
|
+
if (Math.sign(residualDelta) === Math.sign(expectedDelta) && Math.abs(residualDelta) > 24) {
|
|
3373
|
+
const fallback = await scrollScrollableAncestor(element, residualDelta);
|
|
3374
|
+
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)}`);
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
2783
3378
|
didScroll = true;
|
|
2784
3379
|
}
|
|
3380
|
+
try {
|
|
3381
|
+
await element.scrollIntoViewIfNeeded?.();
|
|
3382
|
+
await waitJitter(80, 0.3);
|
|
3383
|
+
const finalStatus = await checkElementVisibility(element);
|
|
3384
|
+
if (finalStatus.code === "VISIBLE") {
|
|
3385
|
+
logger7.info("humanScroll | \u539F\u751F scrollIntoViewIfNeeded \u515C\u5E95\u6210\u529F");
|
|
3386
|
+
logger7.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
3387
|
+
return { element, didScroll: true, restore: null };
|
|
3388
|
+
}
|
|
3389
|
+
} catch (fallbackError) {
|
|
3390
|
+
logger7.debug(`humanScroll | native fallback failed: ${fallbackError?.message || fallbackError}`);
|
|
3391
|
+
}
|
|
3392
|
+
logger7.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
|
|
2785
3393
|
return { element, didScroll, restore: null };
|
|
2786
3394
|
},
|
|
2787
3395
|
async humanClick(page, target, options = {}) {
|
|
@@ -2790,29 +3398,51 @@ var MobileHumanize = {
|
|
|
2790
3398
|
throwOnMissing = true,
|
|
2791
3399
|
scrollIfNeeded = true
|
|
2792
3400
|
} = options;
|
|
2793
|
-
|
|
2794
|
-
|
|
3401
|
+
const targetDesc = describeTarget(target);
|
|
3402
|
+
logger7.start("humanClick", `target=${targetDesc}`);
|
|
3403
|
+
try {
|
|
3404
|
+
if (target == null) {
|
|
3405
|
+
const viewport = resolveViewport(page);
|
|
3406
|
+
await waitJitter(reactionDelay, 0.45);
|
|
3407
|
+
await tapPoint(page, {
|
|
3408
|
+
x: viewport.width * (0.45 + Math.random() * 0.1),
|
|
3409
|
+
y: viewport.height * (0.48 + Math.random() * 0.12)
|
|
3410
|
+
});
|
|
3411
|
+
logger7.success("humanClick", "Tapped current position");
|
|
3412
|
+
return true;
|
|
3413
|
+
}
|
|
3414
|
+
const element = await resolveElement(page, target, { throwOnMissing });
|
|
3415
|
+
if (!element) {
|
|
3416
|
+
logger7.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
|
|
3417
|
+
return false;
|
|
3418
|
+
}
|
|
3419
|
+
if (scrollIfNeeded) {
|
|
3420
|
+
await MobileHumanize.humanScroll(page, element, { throwOnMissing });
|
|
3421
|
+
}
|
|
3422
|
+
const status = await checkElementVisibility(element).catch(() => null);
|
|
3423
|
+
if (status && status.code !== "VISIBLE") {
|
|
3424
|
+
const message = `\u5143\u7D20\u4E0D\u53EF\u70B9\u51FB: ${status.reason || status.code}`;
|
|
3425
|
+
if (throwOnMissing) throw new Error(message);
|
|
3426
|
+
logger7.warn(`humanClick: ${message}\uFF0C\u8DF3\u8FC7\u70B9\u51FB`);
|
|
3427
|
+
return false;
|
|
3428
|
+
}
|
|
3429
|
+
const box = await element.boundingBox();
|
|
3430
|
+
if (!box) {
|
|
3431
|
+
if (throwOnMissing) throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
3432
|
+
logger7.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
|
|
3433
|
+
return false;
|
|
3434
|
+
}
|
|
2795
3435
|
await waitJitter(reactionDelay, 0.45);
|
|
2796
|
-
await
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3436
|
+
const safePoint = await resolveSafeTapPoint(element).catch(() => null);
|
|
3437
|
+
const tapTarget = safePoint || randomPointInBox(clipBoxToViewport(box, resolveViewport(page)), 0.2);
|
|
3438
|
+
await tapPoint(page, tapTarget);
|
|
3439
|
+
await waitJitter(120, 0.35);
|
|
3440
|
+
logger7.success("humanClick");
|
|
2800
3441
|
return true;
|
|
3442
|
+
} catch (error) {
|
|
3443
|
+
logger7.fail("humanClick", error);
|
|
3444
|
+
throw error;
|
|
2801
3445
|
}
|
|
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
3446
|
},
|
|
2817
3447
|
async randomSleep(baseMs, jitterPercent = 0.3) {
|
|
2818
3448
|
await waitJitter(baseMs, jitterPercent);
|
|
@@ -2850,6 +3480,40 @@ var MobileHumanize = {
|
|
|
2850
3480
|
}
|
|
2851
3481
|
}
|
|
2852
3482
|
},
|
|
3483
|
+
async humanPress(page, targetOrKey, maybeKey, options = {}) {
|
|
3484
|
+
const hasTarget = typeof maybeKey === "string";
|
|
3485
|
+
const key = hasTarget ? maybeKey : targetOrKey;
|
|
3486
|
+
const pressOptions = hasTarget ? options : maybeKey || options;
|
|
3487
|
+
const {
|
|
3488
|
+
reactionDelay = 170,
|
|
3489
|
+
holdDelay = 42,
|
|
3490
|
+
focusDelay = 180,
|
|
3491
|
+
scrollIfNeeded = true,
|
|
3492
|
+
throwOnMissing = true,
|
|
3493
|
+
keyboardOptions = {}
|
|
3494
|
+
} = pressOptions || {};
|
|
3495
|
+
const targetDesc = hasTarget ? describeTarget(targetOrKey) : "current focus";
|
|
3496
|
+
logger7.start("humanPress", `key=${key}, target=${targetDesc}`);
|
|
3497
|
+
try {
|
|
3498
|
+
if (hasTarget) {
|
|
3499
|
+
await MobileHumanize.humanClick(page, targetOrKey, {
|
|
3500
|
+
reactionDelay: focusDelay,
|
|
3501
|
+
scrollIfNeeded,
|
|
3502
|
+
throwOnMissing
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3505
|
+
await waitJitter(reactionDelay, 0.45);
|
|
3506
|
+
await page.keyboard.press(key, {
|
|
3507
|
+
...keyboardOptions,
|
|
3508
|
+
delay: jitterMs(holdDelay, 0.5)
|
|
3509
|
+
});
|
|
3510
|
+
logger7.success("humanPress");
|
|
3511
|
+
return true;
|
|
3512
|
+
} catch (error) {
|
|
3513
|
+
logger7.fail("humanPress", error);
|
|
3514
|
+
throw error;
|
|
3515
|
+
}
|
|
3516
|
+
},
|
|
2853
3517
|
async humanClear(page, selector) {
|
|
2854
3518
|
const locator = page.locator(selector);
|
|
2855
3519
|
await MobileHumanize.humanClick(page, locator, { scrollIfNeeded: true });
|
|
@@ -2900,15 +3564,18 @@ var MobileHumanize = {
|
|
|
2900
3564
|
};
|
|
2901
3565
|
|
|
2902
3566
|
// src/humanize.js
|
|
2903
|
-
var
|
|
3567
|
+
var resolveDeviceFromPage2 = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
|
|
2904
3568
|
var resolveDelegate = (page) => {
|
|
2905
|
-
return
|
|
3569
|
+
return resolveDeviceFromPage2(page) === Device.Mobile ? MobileHumanize : Humanize;
|
|
2906
3570
|
};
|
|
2907
3571
|
var callDelegate = (method, page, args) => {
|
|
2908
3572
|
const delegate = resolveDelegate(page);
|
|
2909
3573
|
return delegate[method](page, ...args);
|
|
2910
3574
|
};
|
|
2911
3575
|
var Humanize2 = {
|
|
3576
|
+
// M = Machine: native/mechanical operations for compatibility paths.
|
|
3577
|
+
// These APIs intentionally do not humanize timing, motion, or intent.
|
|
3578
|
+
M: MachineHumanize,
|
|
2912
3579
|
jitterMs(base, jitterPercent = 0.3) {
|
|
2913
3580
|
return Humanize.jitterMs(base, jitterPercent);
|
|
2914
3581
|
},
|
|
@@ -2937,6 +3604,12 @@ var Humanize2 = {
|
|
|
2937
3604
|
humanType(page, selector, text, options = {}) {
|
|
2938
3605
|
return callDelegate("humanType", page, [selector, text, options]);
|
|
2939
3606
|
},
|
|
3607
|
+
humanPress(page, targetOrKey, maybeKey, options = {}) {
|
|
3608
|
+
if (typeof maybeKey === "string") {
|
|
3609
|
+
return callDelegate("humanPress", page, [targetOrKey, maybeKey, options]);
|
|
3610
|
+
}
|
|
3611
|
+
return callDelegate("humanPress", page, [targetOrKey, maybeKey || options]);
|
|
3612
|
+
},
|
|
2940
3613
|
humanClear(page, selector) {
|
|
2941
3614
|
return callDelegate("humanClear", page, [selector]);
|
|
2942
3615
|
},
|