@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.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 getScrollableRect = async () => {
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 getScrollableRect();
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 page.mouse.click(x, y, {
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 cx = rect.left + rect.width / 2;
2621
- const cy = rect.top + rect.height / 2;
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
- if ((isFixed ? cy < 0 || cy > viewH : cy < 12 || cy > viewH - 12) || cx < 0 || cx > viewW) {
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
- direction: cy < viewH / 2 ? "up" : "down",
2634
- cy,
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 pointElement = document.elementFromPoint(cx, cy);
2640
- if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
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 distance = clamp(Math.abs(deltaY), 80, Math.max(120, viewport.height * 0.72));
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
- viewport.width * (0.45 + Math.random() * 0.1),
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(viewport.height * (0.72 + Math.random() * 0.1), 80, viewport.height - 32) : clamp(viewport.height * (0.28 + Math.random() * 0.1), 32, viewport.height - 80);
2662
- const endY = clamp(startY - direction * distance, 24, viewport.height - 24);
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 distance = minStep + Math.random() * Math.max(1, maxStep - minStep);
2781
- const deltaY = status.direction === "up" ? -distance : distance;
2782
- await dispatchTouchSwipe(page, deltaY);
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
- if (target == null) {
2794
- const viewport = resolveViewport(page);
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 tapPoint(page, {
2797
- x: viewport.width * (0.45 + Math.random() * 0.1),
2798
- y: viewport.height * (0.48 + Math.random() * 0.12)
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);