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