@skrillex1224/playwright-toolkit 2.1.286 → 3.0.1

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
@@ -66,7 +66,7 @@ var Device = Object.freeze({
66
66
  });
67
67
  var Mode = Object.freeze({
68
68
  Default: "default",
69
- CloakBrowser: "cloakbrowser"
69
+ Cloak: "cloak"
70
70
  });
71
71
  var normalizeDevice = (value, fallback = Device.Desktop) => {
72
72
  const normalizedFallback = String(fallback || "").trim().toLowerCase() === Device.Mobile ? Device.Mobile : Device.Desktop;
@@ -76,9 +76,9 @@ var normalizeDevice = (value, fallback = Device.Desktop) => {
76
76
  return normalizedFallback;
77
77
  };
78
78
  var normalizeMode = (value, fallback = Mode.Default) => {
79
- const normalizedFallback = String(fallback || "").trim().toLowerCase() === Mode.CloakBrowser ? Mode.CloakBrowser : Mode.Default;
79
+ const normalizedFallback = String(fallback || "").trim().toLowerCase() === Mode.Cloak ? Mode.Cloak : Mode.Default;
80
80
  const raw = String(value || "").trim().toLowerCase();
81
- if (raw === Mode.CloakBrowser) return Mode.CloakBrowser;
81
+ if (raw === Mode.Cloak) return Mode.Cloak;
82
82
  if (raw === Mode.Default) return Mode.Default;
83
83
  return normalizedFallback;
84
84
  };
@@ -2509,77 +2509,60 @@ var setToolkitMode = (mode = Mode.Default) => ToolkitContext.setMode(mode);
2509
2509
  var resolveModeStrategy = (strategies = {}, mode = getToolkitMode(), fallbackMode = Mode.Default) => {
2510
2510
  const normalizedStrategies = normalizeStrategies(strategies);
2511
2511
  const normalizedMode = normalizeMode(mode, fallbackMode);
2512
- const delegate = normalizedStrategies[normalizedMode] ?? normalizedStrategies[fallbackMode] ?? Object.values(normalizedStrategies).find(Boolean) ?? null;
2512
+ const strategy = normalizedStrategies[normalizedMode] ?? normalizedStrategies[fallbackMode] ?? Object.values(normalizedStrategies).find(Boolean) ?? null;
2513
2513
  return {
2514
2514
  mode: normalizedMode,
2515
- delegate
2515
+ strategy
2516
2516
  };
2517
2517
  };
2518
2518
 
2519
- // src/internals/delegate.js
2520
- var normalizeArrayMethodDefinition = (definition) => typeof definition === "string" ? { name: definition, enumerable: true } : {
2521
- name: String(definition?.name || "").trim(),
2522
- enumerable: definition?.enumerable !== false
2523
- };
2524
- var normalizeMethodDefinitions = (methods = {}) => {
2525
- if (Array.isArray(methods)) {
2526
- return methods.map(normalizeArrayMethodDefinition).filter((method) => method.name);
2527
- }
2528
- if (!methods || typeof methods !== "object") {
2529
- return [];
2530
- }
2531
- return Object.entries(methods).map(([name, definition]) => ({
2532
- name: String(name || "").trim(),
2533
- enumerable: definition?.enumerable !== false
2534
- })).filter((method) => method.name);
2535
- };
2536
- var normalizeDelegateResolution = (resolution) => {
2537
- if (resolution && typeof resolution === "object" && ("delegate" in resolution || "label" in resolution)) {
2538
- return {
2539
- delegate: resolution.delegate ?? null,
2540
- label: String(resolution.label || "current delegate").trim() || "current delegate"
2541
- };
2519
+ // src/internals/reflect.js
2520
+ var normalizeStrategies2 = (strategies) => strategies && typeof strategies === "object" ? strategies : {};
2521
+ var collectFunctionNames = (strategies = []) => {
2522
+ const names = /* @__PURE__ */ new Set();
2523
+ for (const strategy of strategies) {
2524
+ if (!strategy || typeof strategy !== "object") continue;
2525
+ for (const name of Reflect.ownKeys(strategy)) {
2526
+ if (typeof name === "string" && typeof strategy[name] === "function") {
2527
+ names.add(name);
2528
+ }
2529
+ }
2542
2530
  }
2543
- return {
2544
- delegate: resolution,
2545
- label: "current delegate"
2546
- };
2531
+ return names;
2547
2532
  };
2548
- var createMethodDescriptor = (namespace, methodName, enumerable, resolveDelegate) => ({
2549
- enumerable,
2533
+ var methodDescriptor = (namespace, name, resolveTarget) => ({
2534
+ enumerable: true,
2535
+ configurable: true,
2536
+ writable: true,
2550
2537
  value: (...args) => {
2551
- const { delegate, label } = normalizeDelegateResolution(resolveDelegate(methodName, args));
2552
- if (typeof delegate?.[methodName] !== "function") {
2553
- throw new Error(`${namespace}.${methodName} is not available in ${label}`);
2538
+ const { mode, strategy } = resolveTarget(args);
2539
+ const method = strategy?.[name];
2540
+ if (typeof method !== "function") {
2541
+ throw new Error(`${namespace}.${name} is not available in ${mode} mode`);
2554
2542
  }
2555
- return delegate[methodName](...args);
2543
+ return method.apply(strategy, args);
2556
2544
  }
2557
2545
  });
2558
- var withDelegatedProperties = (target = {}, {
2559
- namespace = "Delegate",
2560
- methods = {},
2561
- resolveDelegate = () => null
2562
- } = {}) => {
2563
- const descriptors = Object.fromEntries(
2564
- normalizeMethodDefinitions(methods).map((method) => [
2565
- method.name,
2566
- createMethodDescriptor(namespace, method.name, method.enumerable, resolveDelegate)
2567
- ])
2568
- );
2569
- return Object.defineProperties(target, descriptors);
2570
- };
2571
- var createDelegatedFacade = (namespace, strategies = {}, methods = {}) => {
2572
- return withDelegatedProperties({}, {
2573
- namespace,
2574
- methods,
2575
- resolveDelegate: () => {
2576
- const { mode, delegate } = resolveModeStrategy(strategies);
2577
- return {
2578
- delegate,
2579
- label: `${mode} mode`
2580
- };
2581
- }
2582
- });
2546
+ var withModeReflect = (namespace, strategies = {}) => {
2547
+ const normalizedStrategies = normalizeStrategies2(strategies);
2548
+ const baseStrategy = normalizedStrategies.default ?? Object.values(normalizedStrategies).find(Boolean);
2549
+ const names = collectFunctionNames([baseStrategy]);
2550
+ const descriptors = {};
2551
+ for (const name of names) {
2552
+ descriptors[name] = methodDescriptor(namespace, name, () => resolveModeStrategy(normalizedStrategies));
2553
+ }
2554
+ return Object.defineProperties({}, descriptors);
2555
+ };
2556
+ var withPageReflect = (namespace, resolveStrategy, strategies = []) => {
2557
+ const names = collectFunctionNames(strategies);
2558
+ const descriptors = {};
2559
+ for (const name of names) {
2560
+ descriptors[name] = methodDescriptor(namespace, name, ([page]) => ({
2561
+ mode: "page",
2562
+ strategy: resolveStrategy(page)
2563
+ }));
2564
+ }
2565
+ return Object.defineProperties({}, descriptors);
2583
2566
  };
2584
2567
 
2585
2568
  // src/internals/anti-cheat/default.js
@@ -2654,8 +2637,8 @@ var DefaultAntiCheat = {
2654
2637
  }
2655
2638
  };
2656
2639
 
2657
- // src/internals/anti-cheat/cloakbrowser.js
2658
- var CLOAK_BROWSER_BASE_CONFIG = Object.freeze({
2640
+ // src/internals/anti-cheat/cloak.js
2641
+ var CLOAK_BASE_CONFIG = Object.freeze({
2659
2642
  locale: "",
2660
2643
  acceptLanguage: "",
2661
2644
  timezoneId: "",
@@ -2663,12 +2646,12 @@ var CLOAK_BROWSER_BASE_CONFIG = Object.freeze({
2663
2646
  geolocation: null
2664
2647
  });
2665
2648
  var normalizeHeaders = (headers) => headers && typeof headers === "object" ? headers : {};
2666
- var CloakBrowserAntiCheat = {
2649
+ var CloakAntiCheat = {
2667
2650
  /**
2668
- * CloakBrowser 自身会负责浏览器指纹,toolkit 在该模式下尽量不再注入额外反检测配置。
2651
+ * Cloak 自身会负责浏览器指纹,toolkit 在该模式下尽量不再注入额外反检测配置。
2669
2652
  */
2670
2653
  getBaseConfig() {
2671
- return { ...CLOAK_BROWSER_BASE_CONFIG };
2654
+ return { ...CLOAK_BASE_CONFIG };
2672
2655
  },
2673
2656
  getFingerprintGeneratorOptions() {
2674
2657
  return {};
@@ -2691,16 +2674,9 @@ var CloakBrowserAntiCheat = {
2691
2674
  // src/anti-cheat.js
2692
2675
  var antiCheatStrategies = {
2693
2676
  [Mode.Default]: DefaultAntiCheat,
2694
- [Mode.CloakBrowser]: CloakBrowserAntiCheat
2695
- };
2696
- var antiCheatFacadeMethods = Object.freeze({
2697
- getBaseConfig: { enumerable: true },
2698
- getFingerprintGeneratorOptions: { enumerable: true },
2699
- getLaunchArgs: { enumerable: true },
2700
- getTlsFingerprintOptions: { enumerable: true },
2701
- applyLocaleHeaders: { enumerable: true }
2702
- });
2703
- var AntiCheat = createDelegatedFacade("AntiCheat", antiCheatStrategies, antiCheatFacadeMethods);
2677
+ [Mode.Cloak]: CloakAntiCheat
2678
+ };
2679
+ var AntiCheat = withModeReflect("AntiCheat", antiCheatStrategies);
2704
2680
 
2705
2681
  // src/device-input.js
2706
2682
  var resolveDeviceFromPage = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
@@ -3252,624 +3228,165 @@ var DeviceView = {
3252
3228
  }
3253
3229
  };
3254
3230
 
3255
- // src/internals/humanize/cloakbrowser.js
3256
- var import_delay3 = __toESM(require("delay"), 1);
3231
+ // src/internals/humanize/index.js
3232
+ var import_delay4 = __toESM(require("delay"), 1);
3257
3233
 
3258
- // src/internals/humanize/shared.js
3234
+ // src/internals/humanize/desktop.js
3259
3235
  var import_delay2 = __toESM(require("delay"), 1);
3260
- var jitterMs = (base, jitterPercent = 0.3) => {
3261
- const jitter = Number(base || 0) * Number(jitterPercent || 0) * (Math.random() * 2 - 1);
3262
- return Math.max(10, Math.round(Number(base || 0) + jitter));
3263
- };
3264
- var resolveElement = async (page, target, { throwOnMissing = true } = {}) => {
3265
- if (target == null) return null;
3266
- let element = target;
3267
- if (typeof target === "string") {
3268
- element = await page.$(target);
3269
- }
3270
- if (!element) {
3271
- if (throwOnMissing) {
3272
- throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${String(target)}`);
3273
- }
3274
- return null;
3275
- }
3276
- return element;
3277
- };
3278
- var waitJitter = (base, jitterPercent = 0.3) => (0, import_delay2.default)(jitterMs(base, jitterPercent));
3279
-
3280
- // src/internals/humanize/mobile.js
3281
- var logger5 = createInternalLogger("Humanize.Mobile");
3282
- var initializedPages = /* @__PURE__ */ new WeakSet();
3283
- var DEFAULT_TAP_TIMEOUT_MS = 2500;
3284
- var DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS = 1200;
3285
- var DEFAULT_ACTIVATE_FALLBACK_TIMEOUT_MS = 900;
3286
- var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
3287
- var resolveViewport = (page) => page?.viewportSize?.() || { width: 390, height: 844 };
3288
- var describeTarget = (target) => {
3289
- if (target == null) return "Current Position";
3290
- return typeof target === "string" ? target : "ElementHandle";
3291
- };
3292
- var clipBoxToViewport = (box, viewport) => {
3293
- if (!box || !viewport) return box;
3294
- const left = clamp(box.x, 0, viewport.width);
3295
- const top = clamp(box.y, 0, viewport.height);
3296
- const right = clamp(box.x + box.width, 0, viewport.width);
3297
- const bottom = clamp(box.y + box.height, 0, viewport.height);
3298
- if (right - left > 1 && bottom - top > 1) {
3299
- return {
3300
- x: left,
3301
- y: top,
3302
- width: right - left,
3303
- height: bottom - top
3304
- };
3305
- }
3306
- return box;
3307
- };
3308
- var centerPointInBox = (box) => ({
3309
- x: box.x + box.width / 2,
3310
- y: box.y + box.height / 2
3311
- });
3312
- var withTimeout = async (operation, timeoutMs, label) => {
3313
- const safeTimeoutMs = Math.max(50, Number(timeoutMs || 0));
3314
- let timeoutId = null;
3315
- try {
3316
- return await Promise.race([
3317
- Promise.resolve().then(operation),
3318
- new Promise((_, reject) => {
3319
- timeoutId = setTimeout(() => {
3320
- reject(new Error(`${label} timeout after ${safeTimeoutMs}ms`));
3321
- }, safeTimeoutMs);
3322
- })
3323
- ]);
3324
- } finally {
3325
- if (timeoutId) clearTimeout(timeoutId);
3236
+ var import_ghost_cursor_playwright = require("ghost-cursor-playwright");
3237
+ var logger5 = createInternalLogger("Humanize");
3238
+ var $CursorWeakMap = /* @__PURE__ */ new WeakMap();
3239
+ function $GetCursor(page) {
3240
+ const cursor = $CursorWeakMap.get(page);
3241
+ if (!cursor) {
3242
+ throw new Error("Cursor \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 Humanize.initializeCursor(page)");
3326
3243
  }
3327
- };
3328
- var checkElementVisibility = async (element) => {
3329
- return element.evaluate((el) => {
3330
- const targetStyle = window.getComputedStyle(el);
3331
- if (!targetStyle || targetStyle.display === "none" || targetStyle.visibility === "hidden" || targetStyle.visibility === "collapse") {
3332
- return { code: "NOT_INTERACTABLE", reason: "\u5143\u7D20\u4E0D\u53EF\u89C1", direction: "down" };
3333
- }
3334
- const rect = el.getBoundingClientRect();
3335
- if (!rect || rect.width <= 0 || rect.height <= 0) {
3336
- return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6", direction: "down" };
3244
+ return cursor;
3245
+ }
3246
+ var Humanize = {
3247
+ /**
3248
+ * 生成带抖动的毫秒数 - 基于基础值添加随机浮动 (±30% 默认)
3249
+ * @param {number} base - 基础延迟 (ms)
3250
+ * @param {number} [jitterPercent=0.3] - 抖动百分比 (0.3 = ±30%)
3251
+ * @returns {number} 抖动后的毫秒数
3252
+ */
3253
+ jitterMs(base, jitterPercent = 0.3) {
3254
+ const jitter = base * jitterPercent * (Math.random() * 2 - 1);
3255
+ return Math.max(10, Math.round(base + jitter));
3256
+ },
3257
+ /**
3258
+ * 初始化页面的 Ghost Cursor(必须在使用其他 cursor 相关方法前调用)
3259
+ *
3260
+ * @param {import('playwright').Page} page
3261
+ * @returns {Promise<void>}
3262
+ */
3263
+ async initializeCursor(page) {
3264
+ if ($CursorWeakMap.has(page)) {
3265
+ logger5.debug("initializeCursor: cursor already exists, skipping");
3266
+ return;
3337
3267
  }
3338
- const viewW = window.innerWidth;
3339
- const viewH = window.innerHeight;
3340
- const centerY = rect.top + rect.height / 2;
3341
- let clipLeft = 0;
3342
- let clipRight = viewW;
3343
- let clipTop = 0;
3344
- let clipBottom = viewH;
3345
- let isFixed = false;
3346
- let positioning = null;
3347
- for (let node = el; node && node !== document.body; node = node.parentElement) {
3348
- const style = window.getComputedStyle(node);
3349
- if (style && (style.position === "fixed" || style.position === "sticky")) {
3350
- isFixed = true;
3351
- positioning = style.position;
3352
- break;
3268
+ logger5.start("initializeCursor", "creating cursor");
3269
+ const cursor = await (0, import_ghost_cursor_playwright.createCursor)(page);
3270
+ $CursorWeakMap.set(page, cursor);
3271
+ logger5.success("initializeCursor", "cursor initialized");
3272
+ },
3273
+ /**
3274
+ * 人类化鼠标移动 - 使用 ghost-cursor 移动到指定位置或元素
3275
+ *
3276
+ * @param {import('playwright').Page} page
3277
+ * @param {string|{x: number, y: number}|import('playwright').ElementHandle} target - CSS选择器、坐标对象或元素句柄
3278
+ */
3279
+ async humanMove(page, target) {
3280
+ const cursor = $GetCursor(page);
3281
+ logger5.start("humanMove", `target=${typeof target === "string" ? target : "element/coords"}`);
3282
+ try {
3283
+ if (typeof target === "string") {
3284
+ const element = await page.$(target);
3285
+ if (!element) {
3286
+ logger5.warn(`humanMove: \u5143\u7D20\u4E0D\u5B58\u5728 ${target}`);
3287
+ return false;
3288
+ }
3289
+ const box = await element.boundingBox();
3290
+ if (!box) {
3291
+ logger5.warn(`humanMove: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E ${target}`);
3292
+ return false;
3293
+ }
3294
+ const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
3295
+ const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
3296
+ await cursor.actions.move({ x, y });
3297
+ } else if (target && typeof target.x === "number" && typeof target.y === "number") {
3298
+ await cursor.actions.move(target);
3299
+ } else if (target && typeof target.boundingBox === "function") {
3300
+ const box = await target.boundingBox();
3301
+ if (box) {
3302
+ const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
3303
+ const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
3304
+ await cursor.actions.move({ x, y });
3305
+ }
3353
3306
  }
3307
+ logger5.success("humanMove");
3308
+ return true;
3309
+ } catch (error) {
3310
+ logger5.fail("humanMove", error);
3311
+ throw error;
3354
3312
  }
3355
- for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
3356
- const style = window.getComputedStyle(node);
3357
- if (!style) continue;
3358
- const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
3359
- const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
3360
- if (!clipsX && !clipsY) continue;
3361
- const nodeRect = node.getBoundingClientRect();
3362
- if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
3363
- if (clipsX) {
3364
- clipLeft = Math.max(clipLeft, nodeRect.left);
3365
- clipRight = Math.min(clipRight, nodeRect.right);
3366
- }
3367
- if (clipsY) {
3368
- clipTop = Math.max(clipTop, nodeRect.top);
3369
- clipBottom = Math.min(clipBottom, nodeRect.bottom);
3313
+ },
3314
+ /**
3315
+ * 渐进式滚动到元素可见(仅处理 Y 轴滚动)
3316
+ * 返回 restore 方法,用于将滚动容器恢复到原位置
3317
+ *
3318
+ * @param {import('playwright').Page} page
3319
+ * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
3320
+ * @param {Object} [options]
3321
+ * @param {number} [options.maxSteps=20] - 最大滚动步数
3322
+ * @param {number} [options.minStep=260] - 单次滚动最小步长
3323
+ * @param {number} [options.maxStep=800] - 单次滚动最大步长
3324
+ * @param {number} [options.maxDurationMs] - 最长耗时上限 (默认随 maxSteps 估算)
3325
+ */
3326
+ async humanScroll(page, target, options = {}) {
3327
+ const {
3328
+ maxSteps = 20,
3329
+ minStep = 260,
3330
+ maxStep = 800,
3331
+ maxDurationMs = maxSteps * 220 + 800
3332
+ } = options;
3333
+ const targetDesc = typeof target === "string" ? target : "ElementHandle";
3334
+ logger5.start("humanScroll", `target=${targetDesc}`);
3335
+ let element;
3336
+ if (typeof target === "string") {
3337
+ element = await page.$(target);
3338
+ if (!element) {
3339
+ logger5.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
3340
+ return { element: null, didScroll: false };
3370
3341
  }
3342
+ } else {
3343
+ element = target;
3371
3344
  }
3372
- const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
3373
- const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
3374
- const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
3375
- const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
3376
- const visibleWidth = visibleRight - visibleLeft;
3377
- const visibleHeight = visibleBottom - visibleTop;
3378
- const cx = visibleLeft + visibleWidth / 2;
3379
- const cy = visibleTop + visibleHeight / 2;
3380
- if (visibleWidth <= 1 || visibleHeight <= 1) {
3381
- return {
3382
- code: "OUT_OF_VIEWPORT",
3383
- reason: "\u4E0D\u5728\u89C6\u53E3\u5185",
3384
- direction: centerY < clipTop ? "up" : "down",
3385
- cy: centerY,
3386
- viewH,
3387
- isFixed,
3388
- positioning
3389
- };
3390
- }
3391
- const isRootNode = (node) => !node || node === document || node === document.body || node === document.documentElement;
3392
- const commonAncestor = (a, b) => {
3393
- for (let current = a; current && !isRootNode(current); current = current.parentElement) {
3394
- if (current.contains(b)) return current;
3395
- }
3396
- return null;
3397
- };
3398
- const sameTapTarget = (pointElement) => {
3399
- if (!pointElement) return false;
3400
- if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
3401
- return true;
3402
- }
3403
- const common = commonAncestor(el, pointElement);
3404
- if (!common) return false;
3405
- const commonRect = common.getBoundingClientRect?.();
3406
- if (!commonRect || commonRect.width <= 0 || commonRect.height <= 0) return false;
3407
- const commonArea = commonRect.width * commonRect.height;
3408
- const targetArea = Math.max(1, rect.width * rect.height);
3409
- const maxSharedRegionArea = Math.max(targetArea * 12, 4096);
3410
- if (commonArea > maxSharedRegionArea) return false;
3411
- if (commonRect.width > Math.max(rect.width * 8, 120) || commonRect.height > Math.max(rect.height * 8, 120)) {
3412
- return false;
3413
- }
3414
- return common.contains(el) && common.contains(pointElement);
3415
- };
3416
- const describeElement = (node) => {
3417
- if (!node) return null;
3418
- const className = typeof node.className === "string" ? node.className : node.className && typeof node.className.baseVal === "string" ? node.className.baseVal : "";
3419
- const rect2 = node.getBoundingClientRect?.();
3420
- const style = window.getComputedStyle(node);
3421
- return {
3422
- tag: node.tagName,
3423
- id: node.id || "",
3424
- className,
3425
- isFixed: Boolean(style && (style.position === "fixed" || style.position === "sticky")),
3426
- positioning: style?.position || "",
3427
- top: rect2 ? rect2.top : null,
3428
- bottom: rect2 ? rect2.bottom : null,
3429
- left: rect2 ? rect2.left : null,
3430
- right: rect2 ? rect2.right : null
3431
- };
3432
- };
3433
- const samplePoints = [
3434
- { x: cx, y: cy },
3435
- { x: visibleLeft + Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
3436
- { x: visibleRight - Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
3437
- { x: cx, y: visibleTop + Math.min(8, Math.max(1, visibleHeight * 0.25)) },
3438
- { x: cx, y: visibleBottom - Math.min(8, Math.max(1, visibleHeight * 0.25)) }
3439
- ];
3440
- let obstruction = null;
3441
- for (const point of samplePoints) {
3442
- const pointElement = document.elementFromPoint(point.x, point.y);
3443
- if (sameTapTarget(pointElement)) {
3444
- return { code: "VISIBLE", isFixed, positioning };
3445
- }
3446
- obstruction = obstruction || describeElement(pointElement);
3447
- }
3448
- if (obstruction) {
3449
- return {
3450
- code: "OBSTRUCTED",
3451
- reason: "\u88AB\u906E\u6321",
3452
- direction: cy > viewH / 2 ? "down" : "up",
3453
- obstruction,
3454
- cy,
3455
- viewH,
3456
- isFixed,
3457
- positioning
3458
- };
3459
- }
3460
- return { code: "VISIBLE", isFixed, positioning };
3461
- });
3462
- };
3463
- var activateElementFallback = async (element, point = null, options = {}) => {
3464
- return element.evaluate((el, { innerOptions }) => {
3465
- const isEditable = (node) => {
3466
- if (!node || node.nodeType !== Node.ELEMENT_NODE) return false;
3467
- if (node.isContentEditable) return true;
3468
- if (node instanceof HTMLTextAreaElement) return !node.disabled && !node.readOnly;
3469
- if (node instanceof HTMLInputElement) {
3470
- return !node.disabled && !node.readOnly && typeof node.select === "function";
3471
- }
3472
- return false;
3473
- };
3474
- const findEditable = (node) => {
3475
- for (let current = node; current && current !== document.body; current = current.parentElement) {
3476
- if (isEditable(current)) return current;
3477
- }
3478
- return null;
3479
- };
3480
- const editable = findEditable(el);
3481
- if (editable && typeof editable.focus === "function") {
3482
- editable.focus({ preventScroll: true });
3483
- if (innerOptions.editableOnly) {
3484
- return { activated: true, method: "focus", tag: editable.tagName || "" };
3485
- }
3486
- }
3487
- if (innerOptions.editableOnly) {
3488
- return { activated: false, method: "none", tag: el?.tagName || "" };
3489
- }
3490
- if (typeof el.focus === "function") {
3491
- el.focus({ preventScroll: true });
3492
- }
3493
- if (typeof el.click === "function") {
3494
- el.click();
3495
- return { activated: true, method: "dom-click", tag: el.tagName || "" };
3496
- }
3497
- if (typeof el.dispatchEvent === "function") {
3498
- el.dispatchEvent(new MouseEvent("click", {
3499
- bubbles: true,
3500
- cancelable: true,
3501
- view: window
3502
- }));
3503
- return { activated: true, method: "dispatch-click", tag: el.tagName || "" };
3504
- }
3505
- return {
3506
- activated: Boolean(editable),
3507
- method: editable ? "focus" : "none",
3508
- tag: el?.tagName || ""
3509
- };
3510
- }, {
3511
- innerOptions: options || {}
3512
- });
3513
- };
3514
- var getScrollableRect = async (element) => {
3515
- return element.evaluate((el) => {
3516
- const isScrollable = (node) => {
3517
- const style = window.getComputedStyle(node);
3518
- if (!style) return false;
3519
- const overflowY = style.overflowY;
3520
- if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3521
- return node.scrollHeight > node.clientHeight + 1;
3522
- };
3523
- let current = el;
3524
- while (current && current !== document.body) {
3525
- if (isScrollable(current)) {
3526
- const rect = current.getBoundingClientRect();
3527
- if (rect && rect.width > 0 && rect.height > 0) {
3528
- return {
3529
- x: rect.x,
3530
- y: rect.y,
3531
- width: rect.width,
3532
- height: rect.height
3533
- };
3534
- }
3535
- }
3536
- current = current.parentElement;
3537
- }
3538
- return null;
3539
- });
3540
- };
3541
- var scrollScrollableAncestor = async (element, deltaY) => {
3542
- return element.evaluate((el, amount) => {
3543
- const isScrollable = (node) => {
3544
- const style = window.getComputedStyle(node);
3545
- if (!style) return false;
3546
- const overflowY = style.overflowY;
3547
- if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3548
- return node.scrollHeight > node.clientHeight + 1;
3549
- };
3550
- let current = el;
3551
- while (current && current !== document.body) {
3552
- if (isScrollable(current)) {
3553
- const beforeTop2 = current.scrollTop;
3554
- current.scrollTop = beforeTop2 + amount;
3555
- return {
3556
- scroller: true,
3557
- moved: current.scrollTop !== beforeTop2,
3558
- scrollTop: current.scrollTop
3559
- };
3560
- }
3561
- current = current.parentElement;
3562
- }
3563
- const beforeTop = window.scrollY;
3564
- window.scrollBy(0, amount);
3565
- return {
3566
- scroller: null,
3567
- moved: window.scrollY !== beforeTop,
3568
- scrollTop: window.scrollY
3569
- };
3570
- }, deltaY);
3571
- };
3572
- var scrollAwayFromObstruction = async (element, status) => {
3573
- return element.evaluate((el, innerStatus) => {
3574
- const isScrollable = (node) => {
3575
- const style = window.getComputedStyle(node);
3576
- if (!style) return false;
3577
- const overflowY = style.overflowY;
3578
- if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3579
- return node.scrollHeight > node.clientHeight + 1;
3580
- };
3581
- let scroller = el;
3582
- while (scroller && scroller !== document.body) {
3583
- if (isScrollable(scroller)) break;
3584
- scroller = scroller.parentElement;
3585
- }
3586
- if (!scroller || scroller === document.body) {
3587
- return { moved: false, scrollTop: 0, deltaY: 0 };
3588
- }
3589
- const rect = el.getBoundingClientRect();
3590
- const scrollerRect = scroller.getBoundingClientRect();
3591
- const obstruction = innerStatus?.obstruction || {};
3592
- const obstructionTop = Number(obstruction.top);
3593
- const obstructionBottom = Number(obstruction.bottom);
3594
- const obstructionMiddle = Number.isFinite(obstructionTop) && Number.isFinite(obstructionBottom) ? (obstructionTop + obstructionBottom) / 2 : scrollerRect.top + scrollerRect.height / 2;
3595
- const scrollerMiddle = scrollerRect.top + scrollerRect.height / 2;
3596
- const padding = 18;
3597
- let deltaY = 0;
3598
- if (Number.isFinite(obstructionTop) && rect.bottom > obstructionTop && obstructionTop >= scrollerRect.top && obstructionTop <= scrollerRect.bottom && obstructionMiddle >= scrollerMiddle) {
3599
- deltaY = rect.bottom - obstructionTop + padding;
3600
- } else if (Number.isFinite(obstructionBottom) && rect.top < obstructionBottom && obstructionBottom >= scrollerRect.top && obstructionBottom <= scrollerRect.bottom && obstructionMiddle < scrollerMiddle) {
3601
- deltaY = rect.top - obstructionBottom - padding;
3602
- } else {
3603
- const fallbackDistance = Math.max(48, Math.min(180, scrollerRect.height * 0.45));
3604
- deltaY = obstructionMiddle >= scrollerMiddle ? fallbackDistance : -fallbackDistance;
3605
- }
3606
- const beforeTop = scroller.scrollTop;
3607
- scroller.scrollTop = beforeTop + deltaY;
3608
- return {
3609
- moved: Math.abs(scroller.scrollTop - beforeTop) > 2,
3610
- scrollTop: scroller.scrollTop,
3611
- deltaY
3612
- };
3613
- }, status);
3614
- };
3615
- var getElementViewportSnapshot = async (element) => {
3616
- return element.evaluate((el) => {
3617
- const rect = el.getBoundingClientRect();
3618
- return {
3619
- top: rect.top,
3620
- bottom: rect.bottom,
3621
- left: rect.left,
3622
- right: rect.right,
3623
- width: rect.width,
3624
- height: rect.height,
3625
- scrollX: window.scrollX,
3626
- scrollY: window.scrollY
3627
- };
3628
- });
3629
- };
3630
- var isTargetImmobileAfterScroll = (before, after) => {
3631
- if (!before || !after) return false;
3632
- const rectDeltaY = Number(after.top || 0) - Number(before.top || 0);
3633
- const rectDeltaX = Number(after.left || 0) - Number(before.left || 0);
3634
- const scrollDeltaY = Number(after.scrollY || 0) - Number(before.scrollY || 0);
3635
- const scrollDeltaX = Number(after.scrollX || 0) - Number(before.scrollX || 0);
3636
- const rectMoved = Math.abs(rectDeltaY) > 3 || Math.abs(rectDeltaX) > 3;
3637
- const pageMoved = Math.abs(scrollDeltaY) > 3 || Math.abs(scrollDeltaX) > 3;
3638
- if (!rectMoved && !pageMoved) return true;
3639
- if (pageMoved && !rectMoved) return true;
3640
- if (Math.abs(scrollDeltaY) > 12 && Math.abs(rectDeltaY) < Math.min(12, Math.abs(scrollDeltaY) * 0.2)) {
3641
- return true;
3642
- }
3643
- return false;
3644
- };
3645
- var restoreWindowFromSnapshot = async (page, before, after) => {
3646
- if (!before || !after) return;
3647
- if (Math.abs(Number(after.scrollX || 0) - Number(before.scrollX || 0)) <= 2 && Math.abs(Number(after.scrollY || 0) - Number(before.scrollY || 0)) <= 2) {
3648
- return;
3649
- }
3650
- await page.evaluate(
3651
- (state2) => window.scrollTo(state2.x, state2.y),
3652
- { x: Number(before.scrollX || 0), y: Number(before.scrollY || 0) }
3653
- ).catch(() => {
3654
- });
3655
- };
3656
- var dispatchTouchSwipe = async (page, deltaY, options = {}) => {
3657
- const viewport = resolveViewport(page);
3658
- const rawRect = options.rect || null;
3659
- const rect = rawRect ? {
3660
- x: clamp(rawRect.x, 0, viewport.width),
3661
- y: clamp(rawRect.y, 0, viewport.height),
3662
- width: clamp(rawRect.width, 0, viewport.width),
3663
- height: clamp(rawRect.height, 0, viewport.height)
3664
- } : null;
3665
- const area = rect && rect.width > 24 && rect.height > 48 ? {
3666
- left: rect.x,
3667
- right: Math.min(viewport.width, rect.x + rect.width),
3668
- top: rect.y,
3669
- bottom: Math.min(viewport.height, rect.y + rect.height)
3670
- } : {
3671
- left: 0,
3672
- right: viewport.width,
3673
- top: 0,
3674
- bottom: viewport.height
3675
- };
3676
- const areaWidth = Math.max(1, area.right - area.left);
3677
- const areaHeight = Math.max(1, area.bottom - area.top);
3678
- const maxDistance = rect ? Math.max(60, areaHeight * 0.72) : Math.max(120, viewport.height * 0.72);
3679
- const distance = clamp(Math.abs(deltaY), Math.min(80, maxDistance), maxDistance);
3680
- const direction = deltaY >= 0 ? 1 : -1;
3681
- const startX = clamp(
3682
- area.left + areaWidth * (0.45 + Math.random() * 0.1),
3683
- 24,
3684
- viewport.width - 24
3685
- );
3686
- 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);
3687
- const endY = clamp(startY - direction * distance, Math.max(16, area.top + 12), Math.min(viewport.height - 16, area.bottom - 12));
3688
- const steps = Math.max(4, Math.round(Number(options.steps || 6)));
3689
- const durationMs = jitterMs(options.durationMs || 320, 0.35);
3690
- let client = null;
3691
- const scrollBefore = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })).catch(() => null);
3692
- try {
3693
- if (page.context && typeof page.context().newCDPSession === "function") {
3694
- client = await page.context().newCDPSession(page);
3695
- }
3696
- if (!client) throw new Error("CDP session unavailable");
3697
- await client.send("Input.synthesizeScrollGesture", {
3698
- x: startX,
3699
- y: startY,
3700
- yDistance: -direction * distance,
3701
- xOverscroll: (Math.random() - 0.5) * 4,
3702
- yOverscroll: (Math.random() - 0.5) * 8,
3703
- speed: 700 + Math.round(Math.random() * 350),
3704
- gestureSourceType: "touch"
3705
- });
3706
- await waitJitter(160, 0.35);
3707
- const scrollAfterSynth = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })).catch(() => null);
3708
- if (scrollBefore && scrollAfterSynth && (Math.abs(scrollAfterSynth.y - scrollBefore.y) > 2 || Math.abs(scrollAfterSynth.x - scrollBefore.x) > 2)) {
3709
- return true;
3710
- }
3711
- await client.send("Input.dispatchTouchEvent", {
3712
- type: "touchStart",
3713
- touchPoints: [{ x: startX, y: startY, id: 1 }]
3714
- });
3715
- for (let i = 1; i <= steps; i += 1) {
3716
- const progress = i / steps;
3717
- const eased = 1 - Math.pow(1 - progress, 2);
3718
- const x = startX + (Math.random() - 0.5) * 3;
3719
- const y = startY + (endY - startY) * eased + (Math.random() - 0.5) * 5;
3720
- await waitJitter(durationMs / steps, 0.25);
3721
- await client.send("Input.dispatchTouchEvent", {
3722
- type: "touchMove",
3723
- touchPoints: [{ x, y, id: 1 }]
3724
- });
3725
- }
3726
- await client.send("Input.dispatchTouchEvent", {
3727
- type: "touchEnd",
3728
- touchPoints: []
3729
- });
3730
- await waitJitter(120, 0.35);
3731
- const scrollAfterTouch = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })).catch(() => null);
3732
- if (scrollBefore && scrollAfterTouch && Math.abs(scrollAfterTouch.y - scrollBefore.y) <= 2 && Math.abs(scrollAfterTouch.x - scrollBefore.x) <= 2) {
3733
- await page.evaluate((amount) => window.scrollBy(0, amount), deltaY);
3734
- await waitJitter(120, 0.35);
3735
- }
3736
- return true;
3737
- } catch (error) {
3738
- logger5.debug(`touch swipe fallback: ${error?.message || error}`);
3739
- try {
3740
- await page.evaluate((amount) => window.scrollBy(0, amount), deltaY);
3741
- await waitJitter(120, 0.35);
3742
- return true;
3743
- } catch {
3744
- await page.mouse.wheel(0, deltaY);
3745
- await waitJitter(120, 0.35);
3746
- return true;
3747
- }
3748
- } finally {
3749
- if (client && typeof client.detach === "function") {
3750
- try {
3751
- await client.detach();
3752
- } catch {
3753
- }
3754
- }
3755
- }
3756
- };
3757
- var tapPoint = async (page, point, options = {}) => {
3758
- const {
3759
- timeoutMs = DEFAULT_TAP_TIMEOUT_MS,
3760
- mouseFallbackTimeoutMs = DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS,
3761
- allowMouseFallback = true
3762
- } = options;
3763
- if (page.touchscreen && typeof page.touchscreen.tap === "function") {
3764
- try {
3765
- await withTimeout(
3766
- () => page.touchscreen.tap(point.x, point.y),
3767
- timeoutMs,
3768
- "touchscreen.tap"
3769
- );
3770
- return { method: "touchscreen" };
3771
- } catch (error) {
3772
- logger5.warn(`tapPoint | touchscreen.tap \u5931\u8D25\u6216\u8D85\u65F6\uFF0C\u5C1D\u8BD5\u9F20\u6807\u515C\u5E95: ${error?.message || error}`);
3773
- if (!allowMouseFallback) throw error;
3774
- }
3775
- }
3776
- await withTimeout(
3777
- () => page.mouse.click(point.x, point.y),
3778
- mouseFallbackTimeoutMs,
3779
- "mouse.click fallback"
3780
- );
3781
- return { method: "mouse" };
3782
- };
3783
- var MobileHumanize = {
3784
- jitterMs,
3785
- async initializeCursor(page) {
3786
- if (initializedPages.has(page)) return;
3787
- initializedPages.add(page);
3788
- logger5.debug("initializeCursor: mobile mode uses touch gestures, cursor init skipped");
3789
- },
3790
- async humanMove(page, target) {
3791
- logger5.debug(`humanMove: mobile no-op target=${typeof target === "string" ? target : "element/coords"}`);
3792
- if (typeof target === "string" || target && typeof target.boundingBox === "function") {
3793
- const element = await resolveElement(page, target, { throwOnMissing: false });
3794
- if (!element) {
3795
- return false;
3796
- }
3797
- }
3798
- await waitJitter(80, 0.4);
3799
- return true;
3800
- },
3801
- async humanScroll(page, target, options = {}) {
3802
- const {
3803
- maxSteps = 20,
3804
- minStep = 180,
3805
- maxStep = 520,
3806
- maxDurationMs = maxSteps * 280 + 1200,
3807
- throwOnMissing = false
3808
- } = options;
3809
- const targetDesc = describeTarget(target);
3810
- logger5.start("humanScroll", `target=${targetDesc}`);
3811
- const element = await resolveElement(page, target, { throwOnMissing });
3812
- if (!element) {
3813
- logger5.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${targetDesc}`);
3814
- return { element: null, didScroll: false, restore: null };
3815
- }
3816
- const startTime = Date.now();
3345
+ const cursor = $GetCursor(page);
3817
3346
  let didScroll = false;
3818
- for (let i = 0; i < maxSteps; i += 1) {
3819
- const status = await checkElementVisibility(element);
3820
- if (status.code === "VISIBLE") {
3821
- if (status.isFixed) {
3822
- logger5.info("humanScroll | fixed/sticky \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
3823
- } else {
3824
- logger5.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
3825
- }
3826
- logger5.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
3827
- return { element, didScroll, restore: null };
3828
- }
3829
- if (status.code === "ZERO_DIMENSIONS" || status.code === "NOT_INTERACTABLE") {
3830
- logger5.warn(`humanScroll | \u5143\u7D20\u4E0D\u53EF\u6EDA\u52A8\u81F3\u53EF\u70B9\u51FB\u72B6\u6001: ${status.reason || status.code}`);
3831
- return { element, didScroll, restore: null };
3832
- }
3833
- const scrollRect = await getScrollableRect(element);
3834
- if (!scrollRect && status.isFixed && status.code === "OUT_OF_VIEWPORT") {
3835
- logger5.warn(`humanScroll | fixed/sticky \u76EE\u6807\u4E0D\u5728\u89C6\u53E3\u5185\uFF0C\u9875\u9762\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (direction=${status.direction || "unknown"})`);
3836
- return { element, didScroll, restore: null, unscrollable: true };
3837
- }
3838
- if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
3839
- logger5.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
3840
- return { element, didScroll, restore: null, unscrollable: true };
3841
- }
3842
- if (scrollRect && status.code === "OBSTRUCTED" && status.obstruction?.isFixed) {
3843
- const moved = await scrollAwayFromObstruction(element, status);
3844
- if (moved.moved) {
3845
- logger5.debug(`humanScroll | sticky/fixed \u906E\u6321\u8865\u507F\u6EDA\u52A8 top=${Math.round(moved.scrollTop || 0)}`);
3846
- await waitJitter(90, 0.3);
3847
- didScroll = true;
3848
- continue;
3347
+ const checkVisibility = async () => {
3348
+ return await element.evaluate((el) => {
3349
+ const rect = el.getBoundingClientRect();
3350
+ if (!rect || rect.width === 0 || rect.height === 0) {
3351
+ return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6" };
3849
3352
  }
3850
- }
3851
- if (Date.now() - startTime > maxDurationMs) {
3852
- logger5.warn(`humanScroll | mobile timeout (${maxDurationMs}ms, status=${status.code}, direction=${status.direction || "unknown"}, fixed=${Boolean(status.isFixed)})`);
3853
- return { element, didScroll, restore: null };
3854
- }
3855
- const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
3856
- const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
3857
- logger5.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason || status.code} ${status.direction ? `(${status.direction})` : ""}`);
3858
- const distance = stepMin + Math.random() * Math.max(1, stepMax - stepMin);
3859
- let deltaY = status.direction === "up" ? -distance : distance;
3860
- if (status.code === "OBSTRUCTED") {
3861
- if (status.obstruction?.isFixed && status.obstruction.top != null) {
3862
- const obstructionMiddle = (Number(status.obstruction.top || 0) + Number(status.obstruction.bottom || status.obstruction.top || 0)) / 2;
3863
- const visibleMiddle = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
3864
- deltaY = obstructionMiddle < visibleMiddle ? -distance : distance;
3865
- } else {
3866
- const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
3867
- deltaY = status.cy > halfY ? distance : -distance;
3353
+ const cx = rect.left + rect.width / 2;
3354
+ const cy = rect.top + rect.height / 2;
3355
+ const viewH = window.innerHeight;
3356
+ const viewW = window.innerWidth;
3357
+ let isFixed = false;
3358
+ for (let node = el; node && node !== document.body; node = node.parentElement) {
3359
+ const style = window.getComputedStyle(node);
3360
+ if (style && style.position === "fixed") {
3361
+ isFixed = true;
3362
+ break;
3363
+ }
3868
3364
  }
3869
- }
3870
- const beforeWindowState = scrollRect ? await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) : null;
3871
- const beforeElementSnapshot = await getElementViewportSnapshot(element).catch(() => null);
3872
- const beforeState = scrollRect ? await element.evaluate((el) => {
3365
+ if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
3366
+ const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
3367
+ return { code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy, viewH, isFixed };
3368
+ }
3369
+ const pointElement = document.elementFromPoint(cx, cy);
3370
+ if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
3371
+ return {
3372
+ code: "OBSTRUCTED",
3373
+ reason: "\u88AB\u906E\u6321",
3374
+ obstruction: {
3375
+ tag: pointElement.tagName,
3376
+ id: pointElement.id,
3377
+ className: pointElement.className
3378
+ },
3379
+ cy,
3380
+ // Return Center Y for smart direction calculation
3381
+ viewH,
3382
+ isFixed
3383
+ };
3384
+ }
3385
+ return { code: "VISIBLE", isFixed };
3386
+ });
3387
+ };
3388
+ const getScrollableRect2 = async () => {
3389
+ return await element.evaluate((el) => {
3873
3390
  const isScrollable = (node) => {
3874
3391
  const style = window.getComputedStyle(node);
3875
3392
  if (!style) return false;
@@ -3880,612 +3397,970 @@ var MobileHumanize = {
3880
3397
  let current = el;
3881
3398
  while (current && current !== document.body) {
3882
3399
  if (isScrollable(current)) {
3883
- return {
3884
- kind: "element",
3885
- top: current.scrollTop,
3886
- left: current.scrollLeft
3887
- };
3400
+ const rect = current.getBoundingClientRect();
3401
+ if (rect && rect.width > 0 && rect.height > 0) {
3402
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
3403
+ }
3888
3404
  }
3889
3405
  current = current.parentElement;
3890
3406
  }
3891
- return { kind: "window", top: window.scrollY, left: window.scrollX };
3892
- }) : null;
3893
- await dispatchTouchSwipe(page, deltaY, { rect: scrollRect });
3894
- if (scrollRect && beforeWindowState) {
3895
- const afterWindowState = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
3896
- if (Math.abs(afterWindowState.x - beforeWindowState.x) > 2 || Math.abs(afterWindowState.y - beforeWindowState.y) > 2) {
3897
- await page.evaluate((state2) => window.scrollTo(state2.x, state2.y), beforeWindowState);
3898
- logger5.debug(`humanScroll | \u7A97\u53E3\u6EDA\u52A8\u56DE\u6536 from=${Math.round(afterWindowState.y)} to=${Math.round(beforeWindowState.y)}`);
3407
+ return null;
3408
+ });
3409
+ };
3410
+ const startTime = Date.now();
3411
+ try {
3412
+ for (let i = 0; i < maxSteps; i++) {
3413
+ if (Date.now() - startTime > maxDurationMs) {
3414
+ logger5.warn(`humanScroll | \u8D85\u65F6\u4FDD\u62A4\u89E6\u53D1 (${maxDurationMs}ms)`);
3415
+ return { element, didScroll };
3899
3416
  }
3900
- }
3901
- let afterElementSnapshot = null;
3902
- const readAfterElementSnapshot = async () => {
3903
- if (!afterElementSnapshot) {
3904
- afterElementSnapshot = await getElementViewportSnapshot(element).catch(() => null);
3417
+ const status = await checkVisibility();
3418
+ if (status.code === "VISIBLE") {
3419
+ if (status.isFixed) {
3420
+ logger5.info("humanScroll | fixed \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
3421
+ } else {
3422
+ logger5.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
3423
+ }
3424
+ logger5.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
3425
+ return { element, didScroll };
3905
3426
  }
3906
- return afterElementSnapshot;
3907
- };
3908
- if (!scrollRect && beforeElementSnapshot) {
3909
- const afterSnapshot = await readAfterElementSnapshot();
3910
- if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
3911
- await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
3912
- logger5.warn(`humanScroll | \u76EE\u6807\u4E0D\u968F\u9875\u9762\u6EDA\u52A8\u79FB\u52A8\uFF0C\u9875\u9762\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (status=${status.code}, direction=${status.direction || "unknown"})`);
3913
- return { element, didScroll, restore: null, unscrollable: true };
3427
+ logger5.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${status.direction ? `(${status.direction})` : ""}`);
3428
+ if (status.code === "OBSTRUCTED" && status.obstruction) {
3429
+ logger5.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
3914
3430
  }
3915
- }
3916
- if (scrollRect && beforeState) {
3917
- const afterState = await element.evaluate((el) => {
3918
- const isScrollable = (node) => {
3919
- const style = window.getComputedStyle(node);
3920
- if (!style) return false;
3921
- const overflowY = style.overflowY;
3922
- if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3923
- return node.scrollHeight > node.clientHeight + 1;
3924
- };
3925
- let current = el;
3926
- while (current && current !== document.body) {
3927
- if (isScrollable(current)) {
3928
- return {
3929
- kind: "element",
3930
- top: current.scrollTop,
3931
- left: current.scrollLeft
3932
- };
3933
- }
3934
- current = current.parentElement;
3431
+ const scrollRect = await getScrollableRect2();
3432
+ if (!scrollRect && status.isFixed) {
3433
+ logger5.warn("humanScroll | fixed \u5BB9\u5668\u5185\u4E14\u65E0\u53EF\u6EDA\u52A8\u7956\u5148\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
3434
+ return { element, didScroll };
3435
+ }
3436
+ const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
3437
+ const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
3438
+ let deltaY = 0;
3439
+ if (status.code === "OUT_OF_VIEWPORT") {
3440
+ if (status.direction === "down") {
3441
+ deltaY = stepMin + Math.random() * (stepMax - stepMin);
3442
+ } else if (status.direction === "up") {
3443
+ deltaY = -(stepMin + Math.random() * (stepMax - stepMin));
3444
+ } else {
3445
+ deltaY = 100;
3935
3446
  }
3936
- return { kind: "window", top: window.scrollY, left: window.scrollX };
3937
- });
3938
- const topDelta = Number(afterState.top || 0) - Number(beforeState.top || 0);
3939
- const leftDelta = Number(afterState.left || 0) - Number(beforeState.left || 0);
3940
- const expectedDelta = Number(deltaY || 0);
3941
- const moved = beforeState.kind !== afterState.kind || Math.abs(topDelta) > 2 || Math.abs(leftDelta) > 2;
3942
- if (!moved) {
3943
- const fallback = await scrollScrollableAncestor(element, deltaY);
3944
- logger5.debug(`humanScroll | \u5BB9\u5668\u89E6\u6478\u65E0\u6548\uFF0C\u76F4\u63A5\u6EDA\u52A8 fallback=${fallback.scroller ? "ancestor" : "window"} top=${Math.round(fallback.scrollTop || 0)}`);
3945
- } 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)) {
3946
- const residualDelta = expectedDelta - topDelta;
3947
- if (Math.sign(residualDelta) === Math.sign(expectedDelta) && Math.abs(residualDelta) > 24) {
3948
- const fallback = await scrollScrollableAncestor(element, residualDelta);
3949
- logger5.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)}`);
3447
+ } else if (status.code === "OBSTRUCTED") {
3448
+ const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
3449
+ const isBottomHalf = status.cy > halfY;
3450
+ const direction = isBottomHalf ? 1 : -1;
3451
+ deltaY = direction * (stepMin + Math.random() * 50);
3452
+ }
3453
+ if (i === 0) {
3454
+ const viewSize = page.viewportSize();
3455
+ if (scrollRect) {
3456
+ const safeX = scrollRect.x + scrollRect.width * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.width * 0.4);
3457
+ const safeY = scrollRect.y + scrollRect.height * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.height * 0.4);
3458
+ await cursor.actions.move({ x: safeX, y: safeY });
3459
+ } else if (viewSize) {
3460
+ const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 80;
3461
+ const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 80;
3462
+ await cursor.actions.move({ x: safeX, y: safeY });
3463
+ }
3464
+ }
3465
+ await page.mouse.wheel(0, deltaY);
3466
+ didScroll = true;
3467
+ await (0, import_delay2.default)(this.jitterMs(20 + Math.random() * 40, 0.2));
3468
+ }
3469
+ logger5.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
3470
+ return { element, didScroll };
3471
+ } catch (error) {
3472
+ logger5.fail("humanScroll", error);
3473
+ throw error;
3474
+ }
3475
+ },
3476
+ /**
3477
+ * 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
3478
+ *
3479
+ * @param {import('playwright').Page} page
3480
+ * @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
3481
+ * @param {Object} [options]
3482
+ * @param {number} [options.reactionDelay=250] - 反应延迟基础值 (ms),实际 ±30% 抖动
3483
+ * @param {boolean} [options.throwOnMissing=true] - 元素不存在时是否抛出错误
3484
+ * @param {boolean} [options.scrollIfNeeded=true] - 元素不在视口时是否自动滚动
3485
+ */
3486
+ async humanClick(page, target, options = {}) {
3487
+ const cursor = $GetCursor(page);
3488
+ const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
3489
+ const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
3490
+ logger5.start("humanClick", `target=${targetDesc}`);
3491
+ const restoreOnce = async () => {
3492
+ if (restoreOnce.restored) return;
3493
+ restoreOnce.restored = true;
3494
+ if (typeof restoreOnce.do !== "function") return;
3495
+ try {
3496
+ await (0, import_delay2.default)(this.jitterMs(1e3));
3497
+ await restoreOnce.do();
3498
+ } catch (restoreError) {
3499
+ logger5.warn(`humanClick: \u6062\u590D\u6EDA\u52A8\u4F4D\u7F6E\u5931\u8D25: ${restoreError.message}`);
3500
+ }
3501
+ };
3502
+ try {
3503
+ if (target == null) {
3504
+ await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
3505
+ await cursor.actions.click();
3506
+ logger5.success("humanClick", "Clicked current position");
3507
+ return true;
3508
+ }
3509
+ let element;
3510
+ if (typeof target === "string") {
3511
+ element = await page.$(target);
3512
+ if (!element) {
3513
+ if (throwOnMissing) {
3514
+ throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${target}`);
3950
3515
  }
3516
+ logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
3517
+ return false;
3518
+ }
3519
+ } else {
3520
+ element = target;
3521
+ }
3522
+ if (scrollIfNeeded) {
3523
+ const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
3524
+ restoreOnce.do = didScroll && restore ? restoreFn : null;
3525
+ }
3526
+ const box = await element.boundingBox();
3527
+ if (!box) {
3528
+ await restoreOnce();
3529
+ if (throwOnMissing) {
3530
+ throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
3531
+ }
3532
+ logger5.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
3533
+ return false;
3534
+ }
3535
+ const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
3536
+ const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
3537
+ await cursor.actions.move({ x, y });
3538
+ await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
3539
+ await cursor.actions.click();
3540
+ await restoreOnce();
3541
+ logger5.success("humanClick");
3542
+ return true;
3543
+ } catch (error) {
3544
+ await restoreOnce();
3545
+ logger5.fail("humanClick", error);
3546
+ throw error;
3547
+ }
3548
+ },
3549
+ /**
3550
+ * 随机延迟一段毫秒数(带 ±30% 抖动)
3551
+ * @param {number} baseMs - 基础延迟毫秒数
3552
+ * @param {number} [jitterPercent=0.3] - 抖动百分比
3553
+ */
3554
+ async randomSleep(baseMs, jitterPercent = 0.3) {
3555
+ const ms = this.jitterMs(baseMs, jitterPercent);
3556
+ logger5.start("randomSleep", `base=${baseMs}, actual=${ms}ms`);
3557
+ await (0, import_delay2.default)(ms);
3558
+ logger5.success("randomSleep");
3559
+ },
3560
+ /**
3561
+ * 模拟人类"注视"或"阅读"行为:鼠标在页面上随机微动
3562
+ * @param {import('playwright').Page} page
3563
+ * @param {number} [baseDurationMs=2500] - 基础持续时间 (±40% 抖动)
3564
+ */
3565
+ async simulateGaze(page, baseDurationMs = 2500) {
3566
+ const cursor = $GetCursor(page);
3567
+ const durationMs = this.jitterMs(baseDurationMs, 0.4);
3568
+ logger5.start("simulateGaze", `duration=${durationMs}ms`);
3569
+ const startTime = Date.now();
3570
+ const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
3571
+ while (Date.now() - startTime < durationMs) {
3572
+ const x = 100 + Math.random() * (viewportSize.width - 200);
3573
+ const y = 100 + Math.random() * (viewportSize.height - 200);
3574
+ await cursor.actions.move({ x, y });
3575
+ await (0, import_delay2.default)(this.jitterMs(600, 0.5));
3576
+ }
3577
+ logger5.success("simulateGaze");
3578
+ },
3579
+ /**
3580
+ * 人类化输入 - 带节奏变化(快-慢-停顿-偶尔加速)
3581
+ * @param {import('playwright').Page} page
3582
+ * @param {string} selector - 输入框选择器
3583
+ * @param {string} text - 要输入的文本
3584
+ * @param {Object} [options]
3585
+ * @param {number} [options.baseDelay=180] - 基础按键延迟 (ms),实际 ±40% 抖动
3586
+ * @param {number} [options.pauseProbability=0.08] - 停顿概率 (0-1)
3587
+ * @param {number} [options.pauseBase=800] - 停顿时长基础值 (ms),实际 ±50% 抖动
3588
+ */
3589
+ async humanType(page, selector, text, options = {}) {
3590
+ logger5.start("humanType", `selector=${selector}, textLen=${text.length}`);
3591
+ const {
3592
+ baseDelay = 180,
3593
+ pauseProbability = 0.08,
3594
+ pauseBase = 800
3595
+ } = options;
3596
+ try {
3597
+ const locator = page.locator(selector);
3598
+ await Humanize.humanClick(page, locator);
3599
+ await (0, import_delay2.default)(this.jitterMs(200, 0.4));
3600
+ for (let i = 0; i < text.length; i++) {
3601
+ const char = text[i];
3602
+ let charDelay;
3603
+ if (char === " ") {
3604
+ charDelay = this.jitterMs(baseDelay * 0.6, 0.3);
3605
+ } else if (/[,.!?;:,。!?;:]/.test(char)) {
3606
+ charDelay = this.jitterMs(baseDelay * 1.5, 0.4);
3607
+ } else {
3608
+ charDelay = this.jitterMs(baseDelay, 0.4);
3951
3609
  }
3952
- }
3953
- if (scrollRect && beforeElementSnapshot) {
3954
- const afterSnapshot = await getElementViewportSnapshot(element).catch(() => null);
3955
- if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
3956
- await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
3957
- logger5.warn(`humanScroll | \u76EE\u6807\u4E0D\u968F\u6EDA\u52A8\u5BB9\u5668\u79FB\u52A8\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (status=${status.code}, direction=${status.direction || "unknown"})`);
3958
- return { element, didScroll, restore: null, unscrollable: true };
3610
+ await page.keyboard.type(char);
3611
+ await (0, import_delay2.default)(charDelay);
3612
+ if (Math.random() < pauseProbability && i < text.length - 1) {
3613
+ const pauseTime = this.jitterMs(pauseBase, 0.5);
3614
+ logger5.debug(`\u505C\u987F ${pauseTime}ms...`);
3615
+ await (0, import_delay2.default)(pauseTime);
3959
3616
  }
3960
3617
  }
3961
- didScroll = true;
3962
- }
3963
- try {
3964
- await element.scrollIntoViewIfNeeded?.();
3965
- await waitJitter(80, 0.3);
3966
- const finalStatus = await checkElementVisibility(element);
3967
- if (finalStatus.code === "VISIBLE") {
3968
- logger5.info("humanScroll | \u539F\u751F scrollIntoViewIfNeeded \u515C\u5E95\u6210\u529F");
3969
- logger5.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
3970
- return { element, didScroll: true, restore: null };
3971
- }
3972
- } catch (fallbackError) {
3973
- logger5.debug(`humanScroll | native fallback failed: ${fallbackError?.message || fallbackError}`);
3618
+ logger5.success("humanType");
3619
+ } catch (error) {
3620
+ logger5.fail("humanType", error);
3621
+ throw error;
3974
3622
  }
3975
- logger5.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
3976
- return { element, didScroll, restore: null };
3977
3623
  },
3978
- async humanClick(page, target, options = {}) {
3624
+ /**
3625
+ * 人类化按键 - 模拟用户在当前焦点或指定目标上按下一次键。
3626
+ * @param {import('playwright').Page} page
3627
+ * @param {string|import('playwright').ElementHandle|import('playwright').Locator} targetOrKey - 目标或按键
3628
+ * @param {string|Object} [maybeKey] - 按键或选项
3629
+ * @param {Object} [options]
3630
+ */
3631
+ async humanPress(page, targetOrKey, maybeKey, options = {}) {
3632
+ const hasTarget = typeof maybeKey === "string";
3633
+ const key = hasTarget ? maybeKey : targetOrKey;
3634
+ const pressOptions = hasTarget ? options : maybeKey || options;
3979
3635
  const {
3980
- reactionDelay = 220,
3981
- throwOnMissing = true,
3636
+ reactionDelay = 180,
3637
+ holdDelay = 45,
3638
+ focusDelay = 180,
3982
3639
  scrollIfNeeded = true,
3983
- tapTimeoutMs = DEFAULT_TAP_TIMEOUT_MS,
3984
- mouseFallbackTimeoutMs = DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS,
3985
- activateFallbackTimeoutMs = DEFAULT_ACTIVATE_FALLBACK_TIMEOUT_MS,
3986
- fallbackDomClick = true,
3987
- fallbackDomClickOnTapError = true
3988
- } = options;
3989
- const targetDesc = describeTarget(target);
3990
- logger5.start("humanClick", `target=${targetDesc}`);
3640
+ throwOnMissing = true,
3641
+ keyboardOptions = {}
3642
+ } = pressOptions || {};
3643
+ const targetDesc = hasTarget ? typeof targetOrKey === "string" ? targetOrKey : "ElementHandle" : "current focus";
3644
+ logger5.start("humanPress", `key=${key}, target=${targetDesc}`);
3991
3645
  try {
3992
- if (target == null) {
3993
- const viewport = resolveViewport(page);
3994
- await waitJitter(reactionDelay, 0.45);
3995
- await tapPoint(page, {
3996
- x: viewport.width * (0.45 + Math.random() * 0.1),
3997
- y: viewport.height * (0.48 + Math.random() * 0.12)
3998
- }, {
3999
- timeoutMs: tapTimeoutMs,
4000
- mouseFallbackTimeoutMs
4001
- });
4002
- logger5.success("humanClick", "Tapped current position");
4003
- return true;
3646
+ if (hasTarget) {
3647
+ await this.humanClick(page, targetOrKey, { reactionDelay: focusDelay, scrollIfNeeded, throwOnMissing });
4004
3648
  }
4005
- const element = await resolveElement(page, target, { throwOnMissing });
4006
- if (!element) {
4007
- logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
4008
- return false;
3649
+ await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.45));
3650
+ await page.keyboard.press(key, {
3651
+ ...keyboardOptions,
3652
+ delay: this.jitterMs(holdDelay, 0.5)
3653
+ });
3654
+ logger5.success("humanPress");
3655
+ return true;
3656
+ } catch (error) {
3657
+ logger5.fail("humanPress", error);
3658
+ throw error;
3659
+ }
3660
+ },
3661
+ /**
3662
+ * 人类化清空输入框 - 模拟人类删除文本的行为
3663
+ * @param {import('playwright').Page} page
3664
+ * @param {string} selector - 输入框选择器
3665
+ */
3666
+ async humanClear(page, selector) {
3667
+ logger5.start("humanClear", `selector=${selector}`);
3668
+ try {
3669
+ const locator = page.locator(selector);
3670
+ await locator.click();
3671
+ await (0, import_delay2.default)(this.jitterMs(200, 0.4));
3672
+ const currentValue = await locator.inputValue();
3673
+ if (!currentValue || currentValue.length === 0) {
3674
+ logger5.success("humanClear", "already empty");
3675
+ return;
4009
3676
  }
4010
- const scrollResult = scrollIfNeeded ? await MobileHumanize.humanScroll(page, element, { throwOnMissing }) : null;
4011
- const status = await checkElementVisibility(element).catch(() => null);
4012
- if (status && status.code !== "VISIBLE") {
4013
- if (fallbackDomClick && (status.code === "OUT_OF_VIEWPORT" || status.code === "OBSTRUCTED") && (status.isFixed || scrollResult?.unscrollable)) {
4014
- let fallback = await withTimeout(
4015
- () => activateElementFallback(element, null, {
4016
- editableOnly: true
4017
- }),
4018
- activateFallbackTimeoutMs,
4019
- "focus fallback"
4020
- ).catch(() => null);
4021
- if (!fallback?.activated) {
4022
- fallback = await withTimeout(
4023
- () => activateElementFallback(element, null, {
4024
- editableOnly: false
4025
- }),
4026
- activateFallbackTimeoutMs,
4027
- "activation fallback"
4028
- ).catch(() => null);
4029
- }
4030
- if (fallback?.activated) {
4031
- logger5.warn(`humanClick: \u4E0D\u53EF\u6EDA\u52A8\u76EE\u6807\u4E0D\u53EF\u7269\u7406\u70B9\u51FB\uFF0C\u5DF2\u7528 ${fallback.method} \u6FC0\u6D3B`);
4032
- return true;
4033
- }
3677
+ await page.keyboard.press("Meta+A");
3678
+ await (0, import_delay2.default)(this.jitterMs(100, 0.4));
3679
+ await page.keyboard.press("Backspace");
3680
+ logger5.success("humanClear");
3681
+ } catch (error) {
3682
+ logger5.fail("humanClear", error);
3683
+ throw error;
3684
+ }
3685
+ },
3686
+ /**
3687
+ * 页面预热浏览 - 模拟人类进入页面后的探索行为
3688
+ * @param {import('playwright').Page} page
3689
+ * @param {number} [baseDuration=3500] - 预热时长基础值 (±40% 抖动)
3690
+ */
3691
+ async warmUpBrowsing(page, baseDuration = 3500) {
3692
+ const cursor = $GetCursor(page);
3693
+ const durationMs = this.jitterMs(baseDuration, 0.4);
3694
+ logger5.start("warmUpBrowsing", `duration=${durationMs}ms`);
3695
+ const startTime = Date.now();
3696
+ const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
3697
+ try {
3698
+ while (Date.now() - startTime < durationMs) {
3699
+ const action = Math.random();
3700
+ if (action < 0.4) {
3701
+ const x = 100 + Math.random() * (viewportSize.width - 200);
3702
+ const y = 100 + Math.random() * (viewportSize.height - 200);
3703
+ await cursor.actions.move({ x, y });
3704
+ await (0, import_delay2.default)(this.jitterMs(350, 0.4));
3705
+ } else if (action < 0.7) {
3706
+ const scrollY = (Math.random() - 0.5) * 200;
3707
+ await page.mouse.wheel(0, scrollY);
3708
+ await (0, import_delay2.default)(this.jitterMs(500, 0.4));
3709
+ } else {
3710
+ await (0, import_delay2.default)(this.jitterMs(800, 0.5));
4034
3711
  }
4035
- const message = `\u5143\u7D20\u4E0D\u53EF\u70B9\u51FB: ${status.reason || status.code}`;
4036
- if (throwOnMissing) throw new Error(message);
4037
- logger5.warn(`humanClick: ${message}\uFF0C\u8DF3\u8FC7\u70B9\u51FB`);
4038
- return false;
4039
3712
  }
4040
- const box = await element.boundingBox();
4041
- if (!box) {
4042
- if (throwOnMissing) throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
4043
- logger5.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
4044
- return false;
3713
+ logger5.success("warmUpBrowsing");
3714
+ } catch (error) {
3715
+ logger5.fail("warmUpBrowsing", error);
3716
+ throw error;
3717
+ }
3718
+ },
3719
+ /**
3720
+ * 自然滚动 - 带惯性、减速效果和随机抖动
3721
+ * @param {import('playwright').Page} page
3722
+ * @param {'up' | 'down'} [direction='down'] - 滚动方向
3723
+ * @param {number} [distance=300] - 总滚动距离基础值 (px),±15% 抖动
3724
+ * @param {number} [baseSteps=5] - 分几步完成基础值,±1 随机
3725
+ */
3726
+ async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
3727
+ const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
3728
+ const actualDistance = this.jitterMs(distance, 0.15);
3729
+ logger5.start("naturalScroll", `dir=${direction}, dist=${actualDistance}, steps=${steps}`);
3730
+ const sign = direction === "down" ? 1 : -1;
3731
+ const stepDistance = actualDistance / steps;
3732
+ try {
3733
+ for (let i = 0; i < steps; i++) {
3734
+ const factor = 1 - i / steps * 0.5;
3735
+ const jitter = 0.9 + Math.random() * 0.2;
3736
+ const scrollAmount = stepDistance * factor * sign * jitter;
3737
+ await page.mouse.wheel(0, scrollAmount);
3738
+ const baseDelay = 60 + i * 25;
3739
+ await (0, import_delay2.default)(this.jitterMs(baseDelay, 0.3));
3740
+ }
3741
+ logger5.success("naturalScroll");
3742
+ } catch (error) {
3743
+ logger5.fail("naturalScroll", error);
3744
+ throw error;
3745
+ }
3746
+ }
3747
+ };
3748
+
3749
+ // src/internals/humanize/shared.js
3750
+ var import_delay3 = __toESM(require("delay"), 1);
3751
+ var jitterMs = (base, jitterPercent = 0.3) => {
3752
+ const jitter = Number(base || 0) * Number(jitterPercent || 0) * (Math.random() * 2 - 1);
3753
+ return Math.max(10, Math.round(Number(base || 0) + jitter));
3754
+ };
3755
+ var resolveElement = async (page, target, { throwOnMissing = true } = {}) => {
3756
+ if (target == null) return null;
3757
+ let element = target;
3758
+ if (typeof target === "string") {
3759
+ element = await page.$(target);
3760
+ }
3761
+ if (!element) {
3762
+ if (throwOnMissing) {
3763
+ throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${String(target)}`);
3764
+ }
3765
+ return null;
3766
+ }
3767
+ return element;
3768
+ };
3769
+ var waitJitter = (base, jitterPercent = 0.3) => (0, import_delay3.default)(jitterMs(base, jitterPercent));
3770
+
3771
+ // src/internals/humanize/mobile.js
3772
+ var logger6 = createInternalLogger("Humanize.Mobile");
3773
+ var initializedPages = /* @__PURE__ */ new WeakSet();
3774
+ var DEFAULT_TAP_TIMEOUT_MS = 2500;
3775
+ var DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS = 1200;
3776
+ var DEFAULT_ACTIVATE_FALLBACK_TIMEOUT_MS = 900;
3777
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
3778
+ var resolveViewport = (page) => page?.viewportSize?.() || { width: 390, height: 844 };
3779
+ var describeTarget = (target) => {
3780
+ if (target == null) return "Current Position";
3781
+ return typeof target === "string" ? target : "ElementHandle";
3782
+ };
3783
+ var clipBoxToViewport = (box, viewport) => {
3784
+ if (!box || !viewport) return box;
3785
+ const left = clamp(box.x, 0, viewport.width);
3786
+ const top = clamp(box.y, 0, viewport.height);
3787
+ const right = clamp(box.x + box.width, 0, viewport.width);
3788
+ const bottom = clamp(box.y + box.height, 0, viewport.height);
3789
+ if (right - left > 1 && bottom - top > 1) {
3790
+ return {
3791
+ x: left,
3792
+ y: top,
3793
+ width: right - left,
3794
+ height: bottom - top
3795
+ };
3796
+ }
3797
+ return box;
3798
+ };
3799
+ var centerPointInBox = (box) => ({
3800
+ x: box.x + box.width / 2,
3801
+ y: box.y + box.height / 2
3802
+ });
3803
+ var withTimeout = async (operation, timeoutMs, label) => {
3804
+ const safeTimeoutMs = Math.max(50, Number(timeoutMs || 0));
3805
+ let timeoutId = null;
3806
+ try {
3807
+ return await Promise.race([
3808
+ Promise.resolve().then(operation),
3809
+ new Promise((_, reject) => {
3810
+ timeoutId = setTimeout(() => {
3811
+ reject(new Error(`${label} timeout after ${safeTimeoutMs}ms`));
3812
+ }, safeTimeoutMs);
3813
+ })
3814
+ ]);
3815
+ } finally {
3816
+ if (timeoutId) clearTimeout(timeoutId);
3817
+ }
3818
+ };
3819
+ var checkElementVisibility = async (element) => {
3820
+ return element.evaluate((el) => {
3821
+ const targetStyle = window.getComputedStyle(el);
3822
+ if (!targetStyle || targetStyle.display === "none" || targetStyle.visibility === "hidden" || targetStyle.visibility === "collapse") {
3823
+ return { code: "NOT_INTERACTABLE", reason: "\u5143\u7D20\u4E0D\u53EF\u89C1", direction: "down" };
3824
+ }
3825
+ const rect = el.getBoundingClientRect();
3826
+ if (!rect || rect.width <= 0 || rect.height <= 0) {
3827
+ return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6", direction: "down" };
3828
+ }
3829
+ const viewW = window.innerWidth;
3830
+ const viewH = window.innerHeight;
3831
+ const centerY = rect.top + rect.height / 2;
3832
+ let clipLeft = 0;
3833
+ let clipRight = viewW;
3834
+ let clipTop = 0;
3835
+ let clipBottom = viewH;
3836
+ let isFixed = false;
3837
+ let positioning = null;
3838
+ for (let node = el; node && node !== document.body; node = node.parentElement) {
3839
+ const style = window.getComputedStyle(node);
3840
+ if (style && (style.position === "fixed" || style.position === "sticky")) {
3841
+ isFixed = true;
3842
+ positioning = style.position;
3843
+ break;
3844
+ }
3845
+ }
3846
+ for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
3847
+ const style = window.getComputedStyle(node);
3848
+ if (!style) continue;
3849
+ const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
3850
+ const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
3851
+ if (!clipsX && !clipsY) continue;
3852
+ const nodeRect = node.getBoundingClientRect();
3853
+ if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
3854
+ if (clipsX) {
3855
+ clipLeft = Math.max(clipLeft, nodeRect.left);
3856
+ clipRight = Math.min(clipRight, nodeRect.right);
4045
3857
  }
4046
- await waitJitter(reactionDelay, 0.45);
4047
- const visibleBox = clipBoxToViewport(box, resolveViewport(page));
4048
- const tapTarget = centerPointInBox(visibleBox);
4049
- try {
4050
- await tapPoint(page, tapTarget, {
4051
- timeoutMs: tapTimeoutMs,
4052
- mouseFallbackTimeoutMs
4053
- });
4054
- } catch (tapError) {
4055
- if (!fallbackDomClickOnTapError) throw tapError;
4056
- const fallback = await withTimeout(
4057
- () => activateElementFallback(element, tapTarget, {
4058
- editableOnly: false
4059
- }),
4060
- activateFallbackTimeoutMs,
4061
- "activation fallback"
4062
- ).catch(() => null);
4063
- if (!fallback?.activated) throw tapError;
4064
- logger5.warn(`humanClick: tap \u5931\u8D25\u540E\u5DF2\u7528 ${fallback.method} \u515C\u5E95: ${tapError?.message || tapError}`);
3858
+ if (clipsY) {
3859
+ clipTop = Math.max(clipTop, nodeRect.top);
3860
+ clipBottom = Math.min(clipBottom, nodeRect.bottom);
4065
3861
  }
4066
- await waitJitter(120, 0.35);
4067
- logger5.success("humanClick");
4068
- return true;
4069
- } catch (error) {
4070
- logger5.fail("humanClick", error);
4071
- throw error;
4072
3862
  }
4073
- },
4074
- async randomSleep(baseMs, jitterPercent = 0.3) {
4075
- await waitJitter(baseMs, jitterPercent);
4076
- },
4077
- async simulateGaze(page, baseDurationMs = 2500) {
4078
- const durationMs = jitterMs(baseDurationMs, 0.4);
4079
- const startTime = Date.now();
4080
- while (Date.now() - startTime < durationMs) {
4081
- if (Math.random() < 0.28) {
4082
- const distance = 70 + Math.random() * 120;
4083
- await dispatchTouchSwipe(page, Math.random() < 0.7 ? distance : -distance, {
4084
- durationMs: 180,
4085
- steps: 4
4086
- });
4087
- } else {
4088
- await waitJitter(420, 0.55);
4089
- }
3863
+ const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
3864
+ const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
3865
+ const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
3866
+ const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
3867
+ const visibleWidth = visibleRight - visibleLeft;
3868
+ const visibleHeight = visibleBottom - visibleTop;
3869
+ const cx = visibleLeft + visibleWidth / 2;
3870
+ const cy = visibleTop + visibleHeight / 2;
3871
+ if (visibleWidth <= 1 || visibleHeight <= 1) {
3872
+ return {
3873
+ code: "OUT_OF_VIEWPORT",
3874
+ reason: "\u4E0D\u5728\u89C6\u53E3\u5185",
3875
+ direction: centerY < clipTop ? "up" : "down",
3876
+ cy: centerY,
3877
+ viewH,
3878
+ isFixed,
3879
+ positioning
3880
+ };
4090
3881
  }
4091
- },
4092
- async humanType(page, selector, text, options = {}) {
4093
- const {
4094
- baseDelay = 160,
4095
- pauseProbability = 0.08,
4096
- pauseBase = 700
4097
- } = options;
4098
- await MobileHumanize.humanClick(page, selector, { scrollIfNeeded: true });
4099
- await waitJitter(220, 0.45);
4100
- for (let i = 0; i < String(text).length; i += 1) {
4101
- const char = String(text)[i];
4102
- const charDelay = /[,.!?;:,。!?;:]/.test(char) ? jitterMs(baseDelay * 1.45, 0.4) : jitterMs(char === " " ? baseDelay * 0.65 : baseDelay, 0.4);
4103
- await page.keyboard.type(char);
4104
- await waitJitter(charDelay, 0.15);
4105
- if (Math.random() < pauseProbability && i < String(text).length - 1) {
4106
- await waitJitter(pauseBase, 0.5);
3882
+ const isRootNode = (node) => !node || node === document || node === document.body || node === document.documentElement;
3883
+ const commonAncestor = (a, b) => {
3884
+ for (let current = a; current && !isRootNode(current); current = current.parentElement) {
3885
+ if (current.contains(b)) return current;
4107
3886
  }
4108
- }
4109
- },
4110
- async humanPress(page, targetOrKey, maybeKey, options = {}) {
4111
- const hasTarget = typeof maybeKey === "string";
4112
- const key = hasTarget ? maybeKey : targetOrKey;
4113
- const pressOptions = hasTarget ? options : maybeKey || options;
4114
- const {
4115
- reactionDelay = 170,
4116
- holdDelay = 42,
4117
- focusDelay = 180,
4118
- scrollIfNeeded = true,
4119
- throwOnMissing = true,
4120
- keyboardOptions = {}
4121
- } = pressOptions || {};
4122
- const targetDesc = hasTarget ? describeTarget(targetOrKey) : "current focus";
4123
- logger5.start("humanPress", `key=${key}, target=${targetDesc}`);
4124
- try {
4125
- if (hasTarget) {
4126
- await MobileHumanize.humanClick(page, targetOrKey, {
4127
- reactionDelay: focusDelay,
4128
- scrollIfNeeded,
4129
- throwOnMissing
4130
- });
3887
+ return null;
3888
+ };
3889
+ const sameTapTarget = (pointElement) => {
3890
+ if (!pointElement) return false;
3891
+ if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
3892
+ return true;
4131
3893
  }
4132
- await waitJitter(reactionDelay, 0.45);
4133
- await page.keyboard.press(key, {
4134
- ...keyboardOptions,
4135
- delay: jitterMs(holdDelay, 0.5)
4136
- });
4137
- logger5.success("humanPress");
4138
- return true;
4139
- } catch (error) {
4140
- logger5.fail("humanPress", error);
4141
- throw error;
4142
- }
4143
- },
4144
- async humanClear(page, selector) {
4145
- const locator = page.locator(selector);
4146
- await MobileHumanize.humanClick(page, locator, { scrollIfNeeded: true });
4147
- await waitJitter(160, 0.4);
4148
- const readValue = async () => {
4149
- try {
4150
- return await locator.inputValue({ timeout: 600 });
4151
- } catch {
4152
- return await locator.evaluate((el) => "value" in el ? String(el.value || "") : String(el.textContent || "")).catch(() => "");
3894
+ const common = commonAncestor(el, pointElement);
3895
+ if (!common) return false;
3896
+ const commonRect = common.getBoundingClientRect?.();
3897
+ if (!commonRect || commonRect.width <= 0 || commonRect.height <= 0) return false;
3898
+ const commonArea = commonRect.width * commonRect.height;
3899
+ const targetArea = Math.max(1, rect.width * rect.height);
3900
+ const maxSharedRegionArea = Math.max(targetArea * 12, 4096);
3901
+ if (commonArea > maxSharedRegionArea) return false;
3902
+ if (commonRect.width > Math.max(rect.width * 8, 120) || commonRect.height > Math.max(rect.height * 8, 120)) {
3903
+ return false;
4153
3904
  }
3905
+ return common.contains(el) && common.contains(pointElement);
4154
3906
  };
4155
- const currentValue = await readValue();
4156
- if (!currentValue) return;
4157
- await page.keyboard.press("ControlOrMeta+A");
4158
- await waitJitter(90, 0.35);
4159
- await page.keyboard.press("Backspace");
4160
- await waitJitter(120, 0.35);
4161
- if (!await readValue()) return;
4162
- await locator.evaluate((el) => {
4163
- if ("value" in el) {
4164
- el.value = "";
4165
- el.dispatchEvent(new Event("input", { bubbles: true }));
4166
- el.dispatchEvent(new Event("change", { bubbles: true }));
4167
- return;
3907
+ const describeElement = (node) => {
3908
+ if (!node) return null;
3909
+ const className = typeof node.className === "string" ? node.className : node.className && typeof node.className.baseVal === "string" ? node.className.baseVal : "";
3910
+ const rect2 = node.getBoundingClientRect?.();
3911
+ const style = window.getComputedStyle(node);
3912
+ return {
3913
+ tag: node.tagName,
3914
+ id: node.id || "",
3915
+ className,
3916
+ isFixed: Boolean(style && (style.position === "fixed" || style.position === "sticky")),
3917
+ positioning: style?.position || "",
3918
+ top: rect2 ? rect2.top : null,
3919
+ bottom: rect2 ? rect2.bottom : null,
3920
+ left: rect2 ? rect2.left : null,
3921
+ right: rect2 ? rect2.right : null
3922
+ };
3923
+ };
3924
+ const samplePoints = [
3925
+ { x: cx, y: cy },
3926
+ { x: visibleLeft + Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
3927
+ { x: visibleRight - Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
3928
+ { x: cx, y: visibleTop + Math.min(8, Math.max(1, visibleHeight * 0.25)) },
3929
+ { x: cx, y: visibleBottom - Math.min(8, Math.max(1, visibleHeight * 0.25)) }
3930
+ ];
3931
+ let obstruction = null;
3932
+ for (const point of samplePoints) {
3933
+ const pointElement = document.elementFromPoint(point.x, point.y);
3934
+ if (sameTapTarget(pointElement)) {
3935
+ return { code: "VISIBLE", isFixed, positioning };
4168
3936
  }
4169
- el.textContent = "";
4170
- el.dispatchEvent(new Event("input", { bubbles: true }));
4171
- });
4172
- },
4173
- async warmUpBrowsing(page, baseDuration = 3500) {
4174
- const durationMs = jitterMs(baseDuration, 0.4);
4175
- const startTime = Date.now();
4176
- while (Date.now() - startTime < durationMs) {
4177
- const action = Math.random();
4178
- if (action < 0.5) {
4179
- await dispatchTouchSwipe(page, 120 + Math.random() * 220, {
4180
- durationMs: 240,
4181
- steps: 5
4182
- });
4183
- } else if (action < 0.7) {
4184
- await dispatchTouchSwipe(page, -(80 + Math.random() * 160), {
4185
- durationMs: 220,
4186
- steps: 4
4187
- });
4188
- } else {
4189
- await waitJitter(560, 0.55);
3937
+ obstruction = obstruction || describeElement(pointElement);
3938
+ }
3939
+ if (obstruction) {
3940
+ return {
3941
+ code: "OBSTRUCTED",
3942
+ reason: "\u88AB\u906E\u6321",
3943
+ direction: cy > viewH / 2 ? "down" : "up",
3944
+ obstruction,
3945
+ cy,
3946
+ viewH,
3947
+ isFixed,
3948
+ positioning
3949
+ };
3950
+ }
3951
+ return { code: "VISIBLE", isFixed, positioning };
3952
+ });
3953
+ };
3954
+ var activateElementFallback = async (element, point = null, options = {}) => {
3955
+ return element.evaluate((el, { innerOptions }) => {
3956
+ const isEditable = (node) => {
3957
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) return false;
3958
+ if (node.isContentEditable) return true;
3959
+ if (node instanceof HTMLTextAreaElement) return !node.disabled && !node.readOnly;
3960
+ if (node instanceof HTMLInputElement) {
3961
+ return !node.disabled && !node.readOnly && typeof node.select === "function";
3962
+ }
3963
+ return false;
3964
+ };
3965
+ const findEditable = (node) => {
3966
+ for (let current = node; current && current !== document.body; current = current.parentElement) {
3967
+ if (isEditable(current)) return current;
3968
+ }
3969
+ return null;
3970
+ };
3971
+ const editable = findEditable(el);
3972
+ if (editable && typeof editable.focus === "function") {
3973
+ editable.focus({ preventScroll: true });
3974
+ if (innerOptions.editableOnly) {
3975
+ return { activated: true, method: "focus", tag: editable.tagName || "" };
4190
3976
  }
4191
3977
  }
4192
- },
4193
- async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
4194
- const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
4195
- const actualDistance = jitterMs(distance, 0.15);
4196
- const sign = direction === "down" ? 1 : -1;
4197
- for (let i = 0; i < steps; i += 1) {
4198
- const factor = 1 - i / steps * 0.45;
4199
- await dispatchTouchSwipe(page, actualDistance / steps * factor * sign, {
4200
- durationMs: 150 + i * 20,
4201
- steps: 4
4202
- });
3978
+ if (innerOptions.editableOnly) {
3979
+ return { activated: false, method: "none", tag: el?.tagName || "" };
4203
3980
  }
4204
- }
3981
+ if (typeof el.focus === "function") {
3982
+ el.focus({ preventScroll: true });
3983
+ }
3984
+ if (typeof el.click === "function") {
3985
+ el.click();
3986
+ return { activated: true, method: "dom-click", tag: el.tagName || "" };
3987
+ }
3988
+ if (typeof el.dispatchEvent === "function") {
3989
+ el.dispatchEvent(new MouseEvent("click", {
3990
+ bubbles: true,
3991
+ cancelable: true,
3992
+ view: window
3993
+ }));
3994
+ return { activated: true, method: "dispatch-click", tag: el.tagName || "" };
3995
+ }
3996
+ return {
3997
+ activated: Boolean(editable),
3998
+ method: editable ? "focus" : "none",
3999
+ tag: el?.tagName || ""
4000
+ };
4001
+ }, {
4002
+ innerOptions: options || {}
4003
+ });
4205
4004
  };
4206
-
4207
- // src/internals/humanize/cloakbrowser.js
4208
- var FORCE_CLICK_OPTIONS = Object.freeze({ force: true });
4209
- var resolveDeviceFromPage2 = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
4210
- var isPoint2 = (value) => value && typeof value === "object" && Number.isFinite(Number(value.x)) && Number.isFinite(Number(value.y));
4211
- var resolveTarget = (page, target) => typeof target === "string" ? page.locator(target).first() : target;
4212
- var resolveTargetPoint = async (page, target) => {
4213
- if (typeof target === "string") {
4214
- return await DeviceInput.selectorCenterPoint(page, target).catch(() => null);
4215
- }
4216
- const box = await target?.boundingBox?.().catch(() => null);
4217
- if (!box) {
4005
+ var getScrollableRect = async (element) => {
4006
+ return element.evaluate((el) => {
4007
+ const isScrollable = (node) => {
4008
+ const style = window.getComputedStyle(node);
4009
+ if (!style) return false;
4010
+ const overflowY = style.overflowY;
4011
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
4012
+ return node.scrollHeight > node.clientHeight + 1;
4013
+ };
4014
+ let current = el;
4015
+ while (current && current !== document.body) {
4016
+ if (isScrollable(current)) {
4017
+ const rect = current.getBoundingClientRect();
4018
+ if (rect && rect.width > 0 && rect.height > 0) {
4019
+ return {
4020
+ x: rect.x,
4021
+ y: rect.y,
4022
+ width: rect.width,
4023
+ height: rect.height
4024
+ };
4025
+ }
4026
+ }
4027
+ current = current.parentElement;
4028
+ }
4218
4029
  return null;
4219
- }
4220
- return {
4221
- x: box.x + box.width / 2,
4222
- y: box.y + box.height / 2
4223
- };
4030
+ });
4224
4031
  };
4225
- var sleep = (baseMs, jitterPercent = 0.3) => (0, import_delay3.default)(jitterMs(baseMs, jitterPercent));
4226
- var buildKeyboardOptions = (options = {}) => ({
4227
- ...options.holdDelay != null ? { delay: Math.max(0, Number(options.holdDelay) || 0) } : {},
4228
- ...options.keyboardOptions || {}
4229
- });
4230
- var CloakBrowserDeviceHumanize = {
4231
- jitterMs(base, jitterPercent = 0.3) {
4232
- return jitterMs(base, jitterPercent);
4233
- },
4234
- async initializeCursor(page) {
4235
- return Boolean(page);
4236
- },
4237
- async humanMove(page, target) {
4238
- if (target == null) {
4239
- return false;
4032
+ var scrollScrollableAncestor = async (element, deltaY) => {
4033
+ return element.evaluate((el, amount) => {
4034
+ const isScrollable = (node) => {
4035
+ const style = window.getComputedStyle(node);
4036
+ if (!style) return false;
4037
+ const overflowY = style.overflowY;
4038
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
4039
+ return node.scrollHeight > node.clientHeight + 1;
4040
+ };
4041
+ let current = el;
4042
+ while (current && current !== document.body) {
4043
+ if (isScrollable(current)) {
4044
+ const beforeTop2 = current.scrollTop;
4045
+ current.scrollTop = beforeTop2 + amount;
4046
+ return {
4047
+ scroller: true,
4048
+ moved: current.scrollTop !== beforeTop2,
4049
+ scrollTop: current.scrollTop
4050
+ };
4051
+ }
4052
+ current = current.parentElement;
4240
4053
  }
4241
- if (isPoint2(target)) {
4242
- await DeviceInput.move(page, target, void 0, { forceMouse: true });
4243
- return true;
4054
+ const beforeTop = window.scrollY;
4055
+ window.scrollBy(0, amount);
4056
+ return {
4057
+ scroller: null,
4058
+ moved: window.scrollY !== beforeTop,
4059
+ scrollTop: window.scrollY
4060
+ };
4061
+ }, deltaY);
4062
+ };
4063
+ var scrollAwayFromObstruction = async (element, status) => {
4064
+ return element.evaluate((el, innerStatus) => {
4065
+ const isScrollable = (node) => {
4066
+ const style = window.getComputedStyle(node);
4067
+ if (!style) return false;
4068
+ const overflowY = style.overflowY;
4069
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
4070
+ return node.scrollHeight > node.clientHeight + 1;
4071
+ };
4072
+ let scroller = el;
4073
+ while (scroller && scroller !== document.body) {
4074
+ if (isScrollable(scroller)) break;
4075
+ scroller = scroller.parentElement;
4244
4076
  }
4245
- const point = await resolveTargetPoint(page, target);
4246
- if (!point) {
4247
- return false;
4077
+ if (!scroller || scroller === document.body) {
4078
+ return { moved: false, scrollTop: 0, deltaY: 0 };
4079
+ }
4080
+ const rect = el.getBoundingClientRect();
4081
+ const scrollerRect = scroller.getBoundingClientRect();
4082
+ const obstruction = innerStatus?.obstruction || {};
4083
+ const obstructionTop = Number(obstruction.top);
4084
+ const obstructionBottom = Number(obstruction.bottom);
4085
+ const obstructionMiddle = Number.isFinite(obstructionTop) && Number.isFinite(obstructionBottom) ? (obstructionTop + obstructionBottom) / 2 : scrollerRect.top + scrollerRect.height / 2;
4086
+ const scrollerMiddle = scrollerRect.top + scrollerRect.height / 2;
4087
+ const padding = 18;
4088
+ let deltaY = 0;
4089
+ if (Number.isFinite(obstructionTop) && rect.bottom > obstructionTop && obstructionTop >= scrollerRect.top && obstructionTop <= scrollerRect.bottom && obstructionMiddle >= scrollerMiddle) {
4090
+ deltaY = rect.bottom - obstructionTop + padding;
4091
+ } else if (Number.isFinite(obstructionBottom) && rect.top < obstructionBottom && obstructionBottom >= scrollerRect.top && obstructionBottom <= scrollerRect.bottom && obstructionMiddle < scrollerMiddle) {
4092
+ deltaY = rect.top - obstructionBottom - padding;
4093
+ } else {
4094
+ const fallbackDistance = Math.max(48, Math.min(180, scrollerRect.height * 0.45));
4095
+ deltaY = obstructionMiddle >= scrollerMiddle ? fallbackDistance : -fallbackDistance;
4248
4096
  }
4249
- await DeviceInput.move(page, point, void 0, { forceMouse: true });
4097
+ const beforeTop = scroller.scrollTop;
4098
+ scroller.scrollTop = beforeTop + deltaY;
4099
+ return {
4100
+ moved: Math.abs(scroller.scrollTop - beforeTop) > 2,
4101
+ scrollTop: scroller.scrollTop,
4102
+ deltaY
4103
+ };
4104
+ }, status);
4105
+ };
4106
+ var getElementViewportSnapshot = async (element) => {
4107
+ return element.evaluate((el) => {
4108
+ const rect = el.getBoundingClientRect();
4109
+ return {
4110
+ top: rect.top,
4111
+ bottom: rect.bottom,
4112
+ left: rect.left,
4113
+ right: rect.right,
4114
+ width: rect.width,
4115
+ height: rect.height,
4116
+ scrollX: window.scrollX,
4117
+ scrollY: window.scrollY
4118
+ };
4119
+ });
4120
+ };
4121
+ var isTargetImmobileAfterScroll = (before, after) => {
4122
+ if (!before || !after) return false;
4123
+ const rectDeltaY = Number(after.top || 0) - Number(before.top || 0);
4124
+ const rectDeltaX = Number(after.left || 0) - Number(before.left || 0);
4125
+ const scrollDeltaY = Number(after.scrollY || 0) - Number(before.scrollY || 0);
4126
+ const scrollDeltaX = Number(after.scrollX || 0) - Number(before.scrollX || 0);
4127
+ const rectMoved = Math.abs(rectDeltaY) > 3 || Math.abs(rectDeltaX) > 3;
4128
+ const pageMoved = Math.abs(scrollDeltaY) > 3 || Math.abs(scrollDeltaX) > 3;
4129
+ if (!rectMoved && !pageMoved) return true;
4130
+ if (pageMoved && !rectMoved) return true;
4131
+ if (Math.abs(scrollDeltaY) > 12 && Math.abs(rectDeltaY) < Math.min(12, Math.abs(scrollDeltaY) * 0.2)) {
4250
4132
  return true;
4251
- },
4252
- async humanScroll(page, target) {
4253
- const element = resolveTarget(page, target);
4254
- if (!element) {
4255
- return { element: null, didScroll: false, restore: null };
4133
+ }
4134
+ return false;
4135
+ };
4136
+ var restoreWindowFromSnapshot = async (page, before, after) => {
4137
+ if (!before || !after) return;
4138
+ if (Math.abs(Number(after.scrollX || 0) - Number(before.scrollX || 0)) <= 2 && Math.abs(Number(after.scrollY || 0) - Number(before.scrollY || 0)) <= 2) {
4139
+ return;
4140
+ }
4141
+ await page.evaluate(
4142
+ (state2) => window.scrollTo(state2.x, state2.y),
4143
+ { x: Number(before.scrollX || 0), y: Number(before.scrollY || 0) }
4144
+ ).catch(() => {
4145
+ });
4146
+ };
4147
+ var dispatchTouchSwipe = async (page, deltaY, options = {}) => {
4148
+ const viewport = resolveViewport(page);
4149
+ const rawRect = options.rect || null;
4150
+ const rect = rawRect ? {
4151
+ x: clamp(rawRect.x, 0, viewport.width),
4152
+ y: clamp(rawRect.y, 0, viewport.height),
4153
+ width: clamp(rawRect.width, 0, viewport.width),
4154
+ height: clamp(rawRect.height, 0, viewport.height)
4155
+ } : null;
4156
+ const area = rect && rect.width > 24 && rect.height > 48 ? {
4157
+ left: rect.x,
4158
+ right: Math.min(viewport.width, rect.x + rect.width),
4159
+ top: rect.y,
4160
+ bottom: Math.min(viewport.height, rect.y + rect.height)
4161
+ } : {
4162
+ left: 0,
4163
+ right: viewport.width,
4164
+ top: 0,
4165
+ bottom: viewport.height
4166
+ };
4167
+ const areaWidth = Math.max(1, area.right - area.left);
4168
+ const areaHeight = Math.max(1, area.bottom - area.top);
4169
+ const maxDistance = rect ? Math.max(60, areaHeight * 0.72) : Math.max(120, viewport.height * 0.72);
4170
+ const distance = clamp(Math.abs(deltaY), Math.min(80, maxDistance), maxDistance);
4171
+ const direction = deltaY >= 0 ? 1 : -1;
4172
+ const startX = clamp(
4173
+ area.left + areaWidth * (0.45 + Math.random() * 0.1),
4174
+ 24,
4175
+ viewport.width - 24
4176
+ );
4177
+ 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);
4178
+ const endY = clamp(startY - direction * distance, Math.max(16, area.top + 12), Math.min(viewport.height - 16, area.bottom - 12));
4179
+ const steps = Math.max(4, Math.round(Number(options.steps || 6)));
4180
+ const durationMs = jitterMs(options.durationMs || 320, 0.35);
4181
+ let client = null;
4182
+ const scrollBefore = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })).catch(() => null);
4183
+ try {
4184
+ if (page.context && typeof page.context().newCDPSession === "function") {
4185
+ client = await page.context().newCDPSession(page);
4256
4186
  }
4257
- await element.scrollIntoViewIfNeeded?.().catch(() => {
4187
+ if (!client) throw new Error("CDP session unavailable");
4188
+ await client.send("Input.synthesizeScrollGesture", {
4189
+ x: startX,
4190
+ y: startY,
4191
+ yDistance: -direction * distance,
4192
+ xOverscroll: (Math.random() - 0.5) * 4,
4193
+ yOverscroll: (Math.random() - 0.5) * 8,
4194
+ speed: 700 + Math.round(Math.random() * 350),
4195
+ gestureSourceType: "touch"
4258
4196
  });
4259
- return { element, didScroll: true, restore: null };
4260
- },
4261
- async humanClick(page, target) {
4262
- if (target == null) {
4263
- await DeviceInput.clickPoint(page, { x: 0, y: 0 }, void 0, { forceMouse: true });
4264
- return true;
4265
- }
4266
- if (isPoint2(target)) {
4267
- await DeviceInput.clickPoint(page, target, void 0, { forceMouse: true });
4197
+ await waitJitter(160, 0.35);
4198
+ const scrollAfterSynth = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })).catch(() => null);
4199
+ if (scrollBefore && scrollAfterSynth && (Math.abs(scrollAfterSynth.y - scrollBefore.y) > 2 || Math.abs(scrollAfterSynth.x - scrollBefore.x) > 2)) {
4268
4200
  return true;
4269
4201
  }
4270
- await DeviceInput.click(page, target, {
4271
- clickOptions: FORCE_CLICK_OPTIONS
4272
- });
4273
- return true;
4274
- },
4275
- async randomSleep(baseMs, jitterPercent = 0.3) {
4276
- await sleep(baseMs, jitterPercent);
4277
- },
4278
- async simulateGaze(page, baseDurationMs = 2500) {
4279
- const durationMs = jitterMs(baseDurationMs, 0.4);
4280
- const startedAt = Date.now();
4281
- const viewport = page.viewportSize() || { width: 1365, height: 900 };
4282
- while (Date.now() - startedAt < durationMs) {
4283
- await DeviceInput.move(page, {
4284
- x: 100 + Math.random() * Math.max(120, viewport.width - 200),
4285
- y: 100 + Math.random() * Math.max(120, viewport.height - 200)
4286
- }, void 0, { forceMouse: true });
4287
- await sleep(300, 0.5);
4288
- }
4289
- },
4290
- async humanType(page, target, text, options = {}) {
4291
- await DeviceInput.click(page, target, {
4292
- clickOptions: FORCE_CLICK_OPTIONS
4293
- });
4294
- await DeviceInput.keyboardType(page, text, {
4295
- ...options.baseDelay != null ? { delay: Math.max(0, Number(options.baseDelay) || 0) } : {}
4202
+ await client.send("Input.dispatchTouchEvent", {
4203
+ type: "touchStart",
4204
+ touchPoints: [{ x: startX, y: startY, id: 1 }]
4296
4205
  });
4297
- return true;
4298
- },
4299
- async humanPress(page, targetOrKey, maybeKey, options = {}) {
4300
- if (typeof maybeKey !== "string") {
4301
- await DeviceInput.press(page, targetOrKey, {
4302
- keyboardOptions: buildKeyboardOptions(maybeKey || options)
4206
+ for (let i = 1; i <= steps; i += 1) {
4207
+ const progress = i / steps;
4208
+ const eased = 1 - Math.pow(1 - progress, 2);
4209
+ const x = startX + (Math.random() - 0.5) * 3;
4210
+ const y = startY + (endY - startY) * eased + (Math.random() - 0.5) * 5;
4211
+ await waitJitter(durationMs / steps, 0.25);
4212
+ await client.send("Input.dispatchTouchEvent", {
4213
+ type: "touchMove",
4214
+ touchPoints: [{ x, y, id: 1 }]
4303
4215
  });
4304
- return true;
4305
4216
  }
4306
- await DeviceInput.press(page, targetOrKey, maybeKey, {
4307
- clickOptions: FORCE_CLICK_OPTIONS,
4308
- keyboardOptions: buildKeyboardOptions(options)
4217
+ await client.send("Input.dispatchTouchEvent", {
4218
+ type: "touchEnd",
4219
+ touchPoints: []
4309
4220
  });
4221
+ await waitJitter(120, 0.35);
4222
+ const scrollAfterTouch = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })).catch(() => null);
4223
+ if (scrollBefore && scrollAfterTouch && Math.abs(scrollAfterTouch.y - scrollBefore.y) <= 2 && Math.abs(scrollAfterTouch.x - scrollBefore.x) <= 2) {
4224
+ await page.evaluate((amount) => window.scrollBy(0, amount), deltaY);
4225
+ await waitJitter(120, 0.35);
4226
+ }
4310
4227
  return true;
4311
- },
4312
- async humanClear(page, target) {
4313
- await DeviceInput.fill(page, target, "", { force: true });
4314
- },
4315
- async warmUpBrowsing(page, baseDuration = 3500) {
4316
- await this.simulateGaze(page, Math.min(baseDuration, 900));
4317
- },
4318
- async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
4319
- const steps = Math.max(1, Number(baseSteps) || 1);
4320
- const delta = (Number(distance) || 0) / steps * (direction === "down" ? 1 : -1);
4321
- for (let index = 0; index < steps; index += 1) {
4322
- await page.mouse.wheel(0, delta);
4323
- await sleep(60 + index * 20, 0.3);
4228
+ } catch (error) {
4229
+ logger6.debug(`touch swipe fallback: ${error?.message || error}`);
4230
+ try {
4231
+ await page.evaluate((amount) => window.scrollBy(0, amount), deltaY);
4232
+ await waitJitter(120, 0.35);
4233
+ return true;
4234
+ } catch {
4235
+ await page.mouse.wheel(0, deltaY);
4236
+ await waitJitter(120, 0.35);
4237
+ return true;
4238
+ }
4239
+ } finally {
4240
+ if (client && typeof client.detach === "function") {
4241
+ try {
4242
+ await client.detach();
4243
+ } catch {
4244
+ }
4324
4245
  }
4325
4246
  }
4326
4247
  };
4327
- var resolveCloakBrowserHumanizeDelegate = (page) => resolveDeviceFromPage2(page) === Device.Mobile ? MobileHumanize : CloakBrowserDeviceHumanize;
4328
- var CloakBrowserHumanizeContext = Object.freeze({
4329
- desktopDelegate: CloakBrowserDeviceHumanize,
4330
- resolveDelegate: resolveCloakBrowserHumanizeDelegate
4331
- });
4332
-
4333
- // src/internals/humanize/desktop.js
4334
- var import_delay4 = __toESM(require("delay"), 1);
4335
- var import_ghost_cursor_playwright = require("ghost-cursor-playwright");
4336
- var logger6 = createInternalLogger("Humanize");
4337
- var $CursorWeakMap = /* @__PURE__ */ new WeakMap();
4338
- function $GetCursor(page) {
4339
- const cursor = $CursorWeakMap.get(page);
4340
- if (!cursor) {
4341
- throw new Error("Cursor \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 Humanize.initializeCursor(page)");
4248
+ var tapPoint = async (page, point, options = {}) => {
4249
+ const {
4250
+ timeoutMs = DEFAULT_TAP_TIMEOUT_MS,
4251
+ mouseFallbackTimeoutMs = DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS,
4252
+ allowMouseFallback = true
4253
+ } = options;
4254
+ if (page.touchscreen && typeof page.touchscreen.tap === "function") {
4255
+ try {
4256
+ await withTimeout(
4257
+ () => page.touchscreen.tap(point.x, point.y),
4258
+ timeoutMs,
4259
+ "touchscreen.tap"
4260
+ );
4261
+ return { method: "touchscreen" };
4262
+ } catch (error) {
4263
+ logger6.warn(`tapPoint | touchscreen.tap \u5931\u8D25\u6216\u8D85\u65F6\uFF0C\u5C1D\u8BD5\u9F20\u6807\u515C\u5E95: ${error?.message || error}`);
4264
+ if (!allowMouseFallback) throw error;
4265
+ }
4342
4266
  }
4343
- return cursor;
4344
- }
4345
- var Humanize = {
4346
- /**
4347
- * 生成带抖动的毫秒数 - 基于基础值添加随机浮动 (±30% 默认)
4348
- * @param {number} base - 基础延迟 (ms)
4349
- * @param {number} [jitterPercent=0.3] - 抖动百分比 (0.3 = ±30%)
4350
- * @returns {number} 抖动后的毫秒数
4351
- */
4352
- jitterMs(base, jitterPercent = 0.3) {
4353
- const jitter = base * jitterPercent * (Math.random() * 2 - 1);
4354
- return Math.max(10, Math.round(base + jitter));
4355
- },
4356
- /**
4357
- * 初始化页面的 Ghost Cursor(必须在使用其他 cursor 相关方法前调用)
4358
- *
4359
- * @param {import('playwright').Page} page
4360
- * @returns {Promise<void>}
4361
- */
4267
+ await withTimeout(
4268
+ () => page.mouse.click(point.x, point.y),
4269
+ mouseFallbackTimeoutMs,
4270
+ "mouse.click fallback"
4271
+ );
4272
+ return { method: "mouse" };
4273
+ };
4274
+ var MobileHumanize = {
4275
+ jitterMs,
4362
4276
  async initializeCursor(page) {
4363
- if ($CursorWeakMap.has(page)) {
4364
- logger6.debug("initializeCursor: cursor already exists, skipping");
4365
- return;
4366
- }
4367
- logger6.start("initializeCursor", "creating cursor");
4368
- const cursor = await (0, import_ghost_cursor_playwright.createCursor)(page);
4369
- $CursorWeakMap.set(page, cursor);
4370
- logger6.success("initializeCursor", "cursor initialized");
4277
+ if (initializedPages.has(page)) return;
4278
+ initializedPages.add(page);
4279
+ logger6.debug("initializeCursor: mobile mode uses touch gestures, cursor init skipped");
4371
4280
  },
4372
- /**
4373
- * 人类化鼠标移动 - 使用 ghost-cursor 移动到指定位置或元素
4374
- *
4375
- * @param {import('playwright').Page} page
4376
- * @param {string|{x: number, y: number}|import('playwright').ElementHandle} target - CSS选择器、坐标对象或元素句柄
4377
- */
4378
4281
  async humanMove(page, target) {
4379
- const cursor = $GetCursor(page);
4380
- logger6.start("humanMove", `target=${typeof target === "string" ? target : "element/coords"}`);
4381
- try {
4382
- if (typeof target === "string") {
4383
- const element = await page.$(target);
4384
- if (!element) {
4385
- logger6.warn(`humanMove: \u5143\u7D20\u4E0D\u5B58\u5728 ${target}`);
4386
- return false;
4387
- }
4388
- const box = await element.boundingBox();
4389
- if (!box) {
4390
- logger6.warn(`humanMove: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E ${target}`);
4391
- return false;
4392
- }
4393
- const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
4394
- const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
4395
- await cursor.actions.move({ x, y });
4396
- } else if (target && typeof target.x === "number" && typeof target.y === "number") {
4397
- await cursor.actions.move(target);
4398
- } else if (target && typeof target.boundingBox === "function") {
4399
- const box = await target.boundingBox();
4400
- if (box) {
4401
- const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
4402
- const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
4403
- await cursor.actions.move({ x, y });
4404
- }
4282
+ logger6.debug(`humanMove: mobile no-op target=${typeof target === "string" ? target : "element/coords"}`);
4283
+ if (typeof target === "string" || target && typeof target.boundingBox === "function") {
4284
+ const element = await resolveElement(page, target, { throwOnMissing: false });
4285
+ if (!element) {
4286
+ return false;
4405
4287
  }
4406
- logger6.success("humanMove");
4407
- return true;
4408
- } catch (error) {
4409
- logger6.fail("humanMove", error);
4410
- throw error;
4411
4288
  }
4289
+ await waitJitter(80, 0.4);
4290
+ return true;
4412
4291
  },
4413
- /**
4414
- * 渐进式滚动到元素可见(仅处理 Y 轴滚动)
4415
- * 返回 restore 方法,用于将滚动容器恢复到原位置
4416
- *
4417
- * @param {import('playwright').Page} page
4418
- * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
4419
- * @param {Object} [options]
4420
- * @param {number} [options.maxSteps=20] - 最大滚动步数
4421
- * @param {number} [options.minStep=260] - 单次滚动最小步长
4422
- * @param {number} [options.maxStep=800] - 单次滚动最大步长
4423
- * @param {number} [options.maxDurationMs] - 最长耗时上限 (默认随 maxSteps 估算)
4424
- */
4425
4292
  async humanScroll(page, target, options = {}) {
4426
4293
  const {
4427
4294
  maxSteps = 20,
4428
- minStep = 260,
4429
- maxStep = 800,
4430
- maxDurationMs = maxSteps * 220 + 800
4295
+ minStep = 180,
4296
+ maxStep = 520,
4297
+ maxDurationMs = maxSteps * 280 + 1200,
4298
+ throwOnMissing = false
4431
4299
  } = options;
4432
- const targetDesc = typeof target === "string" ? target : "ElementHandle";
4300
+ const targetDesc = describeTarget(target);
4433
4301
  logger6.start("humanScroll", `target=${targetDesc}`);
4434
- let element;
4435
- if (typeof target === "string") {
4436
- element = await page.$(target);
4437
- if (!element) {
4438
- logger6.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
4439
- return { element: null, didScroll: false };
4440
- }
4441
- } else {
4442
- element = target;
4302
+ const element = await resolveElement(page, target, { throwOnMissing });
4303
+ if (!element) {
4304
+ logger6.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${targetDesc}`);
4305
+ return { element: null, didScroll: false, restore: null };
4443
4306
  }
4444
- const cursor = $GetCursor(page);
4307
+ const startTime = Date.now();
4445
4308
  let didScroll = false;
4446
- const checkVisibility = async () => {
4447
- return await element.evaluate((el) => {
4448
- const rect = el.getBoundingClientRect();
4449
- if (!rect || rect.width === 0 || rect.height === 0) {
4450
- return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6" };
4451
- }
4452
- const cx = rect.left + rect.width / 2;
4453
- const cy = rect.top + rect.height / 2;
4454
- const viewH = window.innerHeight;
4455
- const viewW = window.innerWidth;
4456
- let isFixed = false;
4457
- for (let node = el; node && node !== document.body; node = node.parentElement) {
4458
- const style = window.getComputedStyle(node);
4459
- if (style && style.position === "fixed") {
4460
- isFixed = true;
4461
- break;
4462
- }
4463
- }
4464
- if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
4465
- const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
4466
- return { code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy, viewH, isFixed };
4309
+ for (let i = 0; i < maxSteps; i += 1) {
4310
+ const status = await checkElementVisibility(element);
4311
+ if (status.code === "VISIBLE") {
4312
+ if (status.isFixed) {
4313
+ logger6.info("humanScroll | fixed/sticky \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
4314
+ } else {
4315
+ logger6.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
4467
4316
  }
4468
- const pointElement = document.elementFromPoint(cx, cy);
4469
- if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
4470
- return {
4471
- code: "OBSTRUCTED",
4472
- reason: "\u88AB\u906E\u6321",
4473
- obstruction: {
4474
- tag: pointElement.tagName,
4475
- id: pointElement.id,
4476
- className: pointElement.className
4477
- },
4478
- cy,
4479
- // Return Center Y for smart direction calculation
4480
- viewH,
4481
- isFixed
4482
- };
4317
+ logger6.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
4318
+ return { element, didScroll, restore: null };
4319
+ }
4320
+ if (status.code === "ZERO_DIMENSIONS" || status.code === "NOT_INTERACTABLE") {
4321
+ logger6.warn(`humanScroll | \u5143\u7D20\u4E0D\u53EF\u6EDA\u52A8\u81F3\u53EF\u70B9\u51FB\u72B6\u6001: ${status.reason || status.code}`);
4322
+ return { element, didScroll, restore: null };
4323
+ }
4324
+ const scrollRect = await getScrollableRect(element);
4325
+ if (!scrollRect && status.isFixed && status.code === "OUT_OF_VIEWPORT") {
4326
+ logger6.warn(`humanScroll | fixed/sticky \u76EE\u6807\u4E0D\u5728\u89C6\u53E3\u5185\uFF0C\u9875\u9762\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (direction=${status.direction || "unknown"})`);
4327
+ return { element, didScroll, restore: null, unscrollable: true };
4328
+ }
4329
+ if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
4330
+ logger6.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
4331
+ return { element, didScroll, restore: null, unscrollable: true };
4332
+ }
4333
+ if (scrollRect && status.code === "OBSTRUCTED" && status.obstruction?.isFixed) {
4334
+ const moved = await scrollAwayFromObstruction(element, status);
4335
+ if (moved.moved) {
4336
+ logger6.debug(`humanScroll | sticky/fixed \u906E\u6321\u8865\u507F\u6EDA\u52A8 top=${Math.round(moved.scrollTop || 0)}`);
4337
+ await waitJitter(90, 0.3);
4338
+ didScroll = true;
4339
+ continue;
4483
4340
  }
4484
- return { code: "VISIBLE", isFixed };
4485
- });
4486
- };
4487
- const getScrollableRect2 = async () => {
4488
- return await element.evaluate((el) => {
4341
+ }
4342
+ if (Date.now() - startTime > maxDurationMs) {
4343
+ logger6.warn(`humanScroll | mobile timeout (${maxDurationMs}ms, status=${status.code}, direction=${status.direction || "unknown"}, fixed=${Boolean(status.isFixed)})`);
4344
+ return { element, didScroll, restore: null };
4345
+ }
4346
+ const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
4347
+ const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
4348
+ logger6.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason || status.code} ${status.direction ? `(${status.direction})` : ""}`);
4349
+ const distance = stepMin + Math.random() * Math.max(1, stepMax - stepMin);
4350
+ let deltaY = status.direction === "up" ? -distance : distance;
4351
+ if (status.code === "OBSTRUCTED") {
4352
+ if (status.obstruction?.isFixed && status.obstruction.top != null) {
4353
+ const obstructionMiddle = (Number(status.obstruction.top || 0) + Number(status.obstruction.bottom || status.obstruction.top || 0)) / 2;
4354
+ const visibleMiddle = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
4355
+ deltaY = obstructionMiddle < visibleMiddle ? -distance : distance;
4356
+ } else {
4357
+ const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
4358
+ deltaY = status.cy > halfY ? distance : -distance;
4359
+ }
4360
+ }
4361
+ const beforeWindowState = scrollRect ? await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) : null;
4362
+ const beforeElementSnapshot = await getElementViewportSnapshot(element).catch(() => null);
4363
+ const beforeState = scrollRect ? await element.evaluate((el) => {
4489
4364
  const isScrollable = (node) => {
4490
4365
  const style = window.getComputedStyle(node);
4491
4366
  if (!style) return false;
@@ -4496,259 +4371,259 @@ var Humanize = {
4496
4371
  let current = el;
4497
4372
  while (current && current !== document.body) {
4498
4373
  if (isScrollable(current)) {
4499
- const rect = current.getBoundingClientRect();
4500
- if (rect && rect.width > 0 && rect.height > 0) {
4501
- return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
4502
- }
4374
+ return {
4375
+ kind: "element",
4376
+ top: current.scrollTop,
4377
+ left: current.scrollLeft
4378
+ };
4503
4379
  }
4504
4380
  current = current.parentElement;
4505
4381
  }
4506
- return null;
4507
- });
4508
- };
4509
- const startTime = Date.now();
4510
- try {
4511
- for (let i = 0; i < maxSteps; i++) {
4512
- if (Date.now() - startTime > maxDurationMs) {
4513
- logger6.warn(`humanScroll | \u8D85\u65F6\u4FDD\u62A4\u89E6\u53D1 (${maxDurationMs}ms)`);
4514
- return { element, didScroll };
4515
- }
4516
- const status = await checkVisibility();
4517
- if (status.code === "VISIBLE") {
4518
- if (status.isFixed) {
4519
- logger6.info("humanScroll | fixed \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
4520
- } else {
4521
- logger6.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
4522
- }
4523
- logger6.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
4524
- return { element, didScroll };
4382
+ return { kind: "window", top: window.scrollY, left: window.scrollX };
4383
+ }) : null;
4384
+ await dispatchTouchSwipe(page, deltaY, { rect: scrollRect });
4385
+ if (scrollRect && beforeWindowState) {
4386
+ const afterWindowState = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
4387
+ if (Math.abs(afterWindowState.x - beforeWindowState.x) > 2 || Math.abs(afterWindowState.y - beforeWindowState.y) > 2) {
4388
+ await page.evaluate((state2) => window.scrollTo(state2.x, state2.y), beforeWindowState);
4389
+ logger6.debug(`humanScroll | \u7A97\u53E3\u6EDA\u52A8\u56DE\u6536 from=${Math.round(afterWindowState.y)} to=${Math.round(beforeWindowState.y)}`);
4525
4390
  }
4526
- logger6.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${status.direction ? `(${status.direction})` : ""}`);
4527
- if (status.code === "OBSTRUCTED" && status.obstruction) {
4528
- logger6.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
4391
+ }
4392
+ let afterElementSnapshot = null;
4393
+ const readAfterElementSnapshot = async () => {
4394
+ if (!afterElementSnapshot) {
4395
+ afterElementSnapshot = await getElementViewportSnapshot(element).catch(() => null);
4529
4396
  }
4530
- const scrollRect = await getScrollableRect2();
4531
- if (!scrollRect && status.isFixed) {
4532
- logger6.warn("humanScroll | fixed \u5BB9\u5668\u5185\u4E14\u65E0\u53EF\u6EDA\u52A8\u7956\u5148\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
4533
- return { element, didScroll };
4397
+ return afterElementSnapshot;
4398
+ };
4399
+ if (!scrollRect && beforeElementSnapshot) {
4400
+ const afterSnapshot = await readAfterElementSnapshot();
4401
+ if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
4402
+ await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
4403
+ logger6.warn(`humanScroll | \u76EE\u6807\u4E0D\u968F\u9875\u9762\u6EDA\u52A8\u79FB\u52A8\uFF0C\u9875\u9762\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (status=${status.code}, direction=${status.direction || "unknown"})`);
4404
+ return { element, didScroll, restore: null, unscrollable: true };
4534
4405
  }
4535
- const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
4536
- const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
4537
- let deltaY = 0;
4538
- if (status.code === "OUT_OF_VIEWPORT") {
4539
- if (status.direction === "down") {
4540
- deltaY = stepMin + Math.random() * (stepMax - stepMin);
4541
- } else if (status.direction === "up") {
4542
- deltaY = -(stepMin + Math.random() * (stepMax - stepMin));
4543
- } else {
4544
- deltaY = 100;
4406
+ }
4407
+ if (scrollRect && beforeState) {
4408
+ const afterState = await element.evaluate((el) => {
4409
+ const isScrollable = (node) => {
4410
+ const style = window.getComputedStyle(node);
4411
+ if (!style) return false;
4412
+ const overflowY = style.overflowY;
4413
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
4414
+ return node.scrollHeight > node.clientHeight + 1;
4415
+ };
4416
+ let current = el;
4417
+ while (current && current !== document.body) {
4418
+ if (isScrollable(current)) {
4419
+ return {
4420
+ kind: "element",
4421
+ top: current.scrollTop,
4422
+ left: current.scrollLeft
4423
+ };
4424
+ }
4425
+ current = current.parentElement;
4545
4426
  }
4546
- } else if (status.code === "OBSTRUCTED") {
4547
- const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
4548
- const isBottomHalf = status.cy > halfY;
4549
- const direction = isBottomHalf ? 1 : -1;
4550
- deltaY = direction * (stepMin + Math.random() * 50);
4551
- }
4552
- if (i === 0) {
4553
- const viewSize = page.viewportSize();
4554
- if (scrollRect) {
4555
- const safeX = scrollRect.x + scrollRect.width * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.width * 0.4);
4556
- const safeY = scrollRect.y + scrollRect.height * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.height * 0.4);
4557
- await cursor.actions.move({ x: safeX, y: safeY });
4558
- } else if (viewSize) {
4559
- const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 80;
4560
- const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 80;
4561
- await cursor.actions.move({ x: safeX, y: safeY });
4427
+ return { kind: "window", top: window.scrollY, left: window.scrollX };
4428
+ });
4429
+ const topDelta = Number(afterState.top || 0) - Number(beforeState.top || 0);
4430
+ const leftDelta = Number(afterState.left || 0) - Number(beforeState.left || 0);
4431
+ const expectedDelta = Number(deltaY || 0);
4432
+ const moved = beforeState.kind !== afterState.kind || Math.abs(topDelta) > 2 || Math.abs(leftDelta) > 2;
4433
+ if (!moved) {
4434
+ const fallback = await scrollScrollableAncestor(element, deltaY);
4435
+ logger6.debug(`humanScroll | \u5BB9\u5668\u89E6\u6478\u65E0\u6548\uFF0C\u76F4\u63A5\u6EDA\u52A8 fallback=${fallback.scroller ? "ancestor" : "window"} top=${Math.round(fallback.scrollTop || 0)}`);
4436
+ } 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)) {
4437
+ const residualDelta = expectedDelta - topDelta;
4438
+ if (Math.sign(residualDelta) === Math.sign(expectedDelta) && Math.abs(residualDelta) > 24) {
4439
+ const fallback = await scrollScrollableAncestor(element, residualDelta);
4440
+ logger6.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)}`);
4562
4441
  }
4563
4442
  }
4564
- await page.mouse.wheel(0, deltaY);
4565
- didScroll = true;
4566
- await (0, import_delay4.default)(this.jitterMs(20 + Math.random() * 40, 0.2));
4567
4443
  }
4568
- logger6.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
4569
- return { element, didScroll };
4570
- } catch (error) {
4571
- logger6.fail("humanScroll", error);
4572
- throw error;
4444
+ if (scrollRect && beforeElementSnapshot) {
4445
+ const afterSnapshot = await getElementViewportSnapshot(element).catch(() => null);
4446
+ if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
4447
+ await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
4448
+ logger6.warn(`humanScroll | \u76EE\u6807\u4E0D\u968F\u6EDA\u52A8\u5BB9\u5668\u79FB\u52A8\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (status=${status.code}, direction=${status.direction || "unknown"})`);
4449
+ return { element, didScroll, restore: null, unscrollable: true };
4450
+ }
4451
+ }
4452
+ didScroll = true;
4453
+ }
4454
+ try {
4455
+ await element.scrollIntoViewIfNeeded?.();
4456
+ await waitJitter(80, 0.3);
4457
+ const finalStatus = await checkElementVisibility(element);
4458
+ if (finalStatus.code === "VISIBLE") {
4459
+ logger6.info("humanScroll | \u539F\u751F scrollIntoViewIfNeeded \u515C\u5E95\u6210\u529F");
4460
+ logger6.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
4461
+ return { element, didScroll: true, restore: null };
4462
+ }
4463
+ } catch (fallbackError) {
4464
+ logger6.debug(`humanScroll | native fallback failed: ${fallbackError?.message || fallbackError}`);
4573
4465
  }
4466
+ logger6.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
4467
+ return { element, didScroll, restore: null };
4574
4468
  },
4575
- /**
4576
- * 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
4577
- *
4578
- * @param {import('playwright').Page} page
4579
- * @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
4580
- * @param {Object} [options]
4581
- * @param {number} [options.reactionDelay=250] - 反应延迟基础值 (ms),实际 ±30% 抖动
4582
- * @param {boolean} [options.throwOnMissing=true] - 元素不存在时是否抛出错误
4583
- * @param {boolean} [options.scrollIfNeeded=true] - 元素不在视口时是否自动滚动
4584
- */
4585
4469
  async humanClick(page, target, options = {}) {
4586
- const cursor = $GetCursor(page);
4587
- const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
4588
- const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
4470
+ const {
4471
+ reactionDelay = 220,
4472
+ throwOnMissing = true,
4473
+ scrollIfNeeded = true,
4474
+ tapTimeoutMs = DEFAULT_TAP_TIMEOUT_MS,
4475
+ mouseFallbackTimeoutMs = DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS,
4476
+ activateFallbackTimeoutMs = DEFAULT_ACTIVATE_FALLBACK_TIMEOUT_MS,
4477
+ fallbackDomClick = true,
4478
+ fallbackDomClickOnTapError = true
4479
+ } = options;
4480
+ const targetDesc = describeTarget(target);
4589
4481
  logger6.start("humanClick", `target=${targetDesc}`);
4590
- const restoreOnce = async () => {
4591
- if (restoreOnce.restored) return;
4592
- restoreOnce.restored = true;
4593
- if (typeof restoreOnce.do !== "function") return;
4594
- try {
4595
- await (0, import_delay4.default)(this.jitterMs(1e3));
4596
- await restoreOnce.do();
4597
- } catch (restoreError) {
4598
- logger6.warn(`humanClick: \u6062\u590D\u6EDA\u52A8\u4F4D\u7F6E\u5931\u8D25: ${restoreError.message}`);
4599
- }
4600
- };
4601
4482
  try {
4602
4483
  if (target == null) {
4603
- await (0, import_delay4.default)(this.jitterMs(reactionDelay, 0.4));
4604
- await cursor.actions.click();
4605
- logger6.success("humanClick", "Clicked current position");
4484
+ const viewport = resolveViewport(page);
4485
+ await waitJitter(reactionDelay, 0.45);
4486
+ await tapPoint(page, {
4487
+ x: viewport.width * (0.45 + Math.random() * 0.1),
4488
+ y: viewport.height * (0.48 + Math.random() * 0.12)
4489
+ }, {
4490
+ timeoutMs: tapTimeoutMs,
4491
+ mouseFallbackTimeoutMs
4492
+ });
4493
+ logger6.success("humanClick", "Tapped current position");
4606
4494
  return true;
4607
4495
  }
4608
- let element;
4609
- if (typeof target === "string") {
4610
- element = await page.$(target);
4611
- if (!element) {
4612
- if (throwOnMissing) {
4613
- throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${target}`);
4496
+ const element = await resolveElement(page, target, { throwOnMissing });
4497
+ if (!element) {
4498
+ logger6.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
4499
+ return false;
4500
+ }
4501
+ const scrollResult = scrollIfNeeded ? await MobileHumanize.humanScroll(page, element, { throwOnMissing }) : null;
4502
+ const status = await checkElementVisibility(element).catch(() => null);
4503
+ if (status && status.code !== "VISIBLE") {
4504
+ if (fallbackDomClick && (status.code === "OUT_OF_VIEWPORT" || status.code === "OBSTRUCTED") && (status.isFixed || scrollResult?.unscrollable)) {
4505
+ let fallback = await withTimeout(
4506
+ () => activateElementFallback(element, null, {
4507
+ editableOnly: true
4508
+ }),
4509
+ activateFallbackTimeoutMs,
4510
+ "focus fallback"
4511
+ ).catch(() => null);
4512
+ if (!fallback?.activated) {
4513
+ fallback = await withTimeout(
4514
+ () => activateElementFallback(element, null, {
4515
+ editableOnly: false
4516
+ }),
4517
+ activateFallbackTimeoutMs,
4518
+ "activation fallback"
4519
+ ).catch(() => null);
4520
+ }
4521
+ if (fallback?.activated) {
4522
+ logger6.warn(`humanClick: \u4E0D\u53EF\u6EDA\u52A8\u76EE\u6807\u4E0D\u53EF\u7269\u7406\u70B9\u51FB\uFF0C\u5DF2\u7528 ${fallback.method} \u6FC0\u6D3B`);
4523
+ return true;
4614
4524
  }
4615
- logger6.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
4616
- return false;
4617
4525
  }
4618
- } else {
4619
- element = target;
4620
- }
4621
- if (scrollIfNeeded) {
4622
- const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
4623
- restoreOnce.do = didScroll && restore ? restoreFn : null;
4526
+ const message = `\u5143\u7D20\u4E0D\u53EF\u70B9\u51FB: ${status.reason || status.code}`;
4527
+ if (throwOnMissing) throw new Error(message);
4528
+ logger6.warn(`humanClick: ${message}\uFF0C\u8DF3\u8FC7\u70B9\u51FB`);
4529
+ return false;
4624
4530
  }
4625
4531
  const box = await element.boundingBox();
4626
4532
  if (!box) {
4627
- await restoreOnce();
4628
- if (throwOnMissing) {
4629
- throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
4630
- }
4533
+ if (throwOnMissing) throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
4631
4534
  logger6.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
4632
4535
  return false;
4633
4536
  }
4634
- const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
4635
- const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
4636
- await cursor.actions.move({ x, y });
4637
- await (0, import_delay4.default)(this.jitterMs(reactionDelay, 0.4));
4638
- await cursor.actions.click();
4639
- await restoreOnce();
4537
+ await waitJitter(reactionDelay, 0.45);
4538
+ const visibleBox = clipBoxToViewport(box, resolveViewport(page));
4539
+ const tapTarget = centerPointInBox(visibleBox);
4540
+ try {
4541
+ await tapPoint(page, tapTarget, {
4542
+ timeoutMs: tapTimeoutMs,
4543
+ mouseFallbackTimeoutMs
4544
+ });
4545
+ } catch (tapError) {
4546
+ if (!fallbackDomClickOnTapError) throw tapError;
4547
+ const fallback = await withTimeout(
4548
+ () => activateElementFallback(element, tapTarget, {
4549
+ editableOnly: false
4550
+ }),
4551
+ activateFallbackTimeoutMs,
4552
+ "activation fallback"
4553
+ ).catch(() => null);
4554
+ if (!fallback?.activated) throw tapError;
4555
+ logger6.warn(`humanClick: tap \u5931\u8D25\u540E\u5DF2\u7528 ${fallback.method} \u515C\u5E95: ${tapError?.message || tapError}`);
4556
+ }
4557
+ await waitJitter(120, 0.35);
4640
4558
  logger6.success("humanClick");
4641
4559
  return true;
4642
4560
  } catch (error) {
4643
- await restoreOnce();
4644
4561
  logger6.fail("humanClick", error);
4645
4562
  throw error;
4646
4563
  }
4647
4564
  },
4648
- /**
4649
- * 随机延迟一段毫秒数(带 ±30% 抖动)
4650
- * @param {number} baseMs - 基础延迟毫秒数
4651
- * @param {number} [jitterPercent=0.3] - 抖动百分比
4652
- */
4653
4565
  async randomSleep(baseMs, jitterPercent = 0.3) {
4654
- const ms = this.jitterMs(baseMs, jitterPercent);
4655
- logger6.start("randomSleep", `base=${baseMs}, actual=${ms}ms`);
4656
- await (0, import_delay4.default)(ms);
4657
- logger6.success("randomSleep");
4566
+ await waitJitter(baseMs, jitterPercent);
4658
4567
  },
4659
- /**
4660
- * 模拟人类"注视"或"阅读"行为:鼠标在页面上随机微动
4661
- * @param {import('playwright').Page} page
4662
- * @param {number} [baseDurationMs=2500] - 基础持续时间 (±40% 抖动)
4663
- */
4664
4568
  async simulateGaze(page, baseDurationMs = 2500) {
4665
- const cursor = $GetCursor(page);
4666
- const durationMs = this.jitterMs(baseDurationMs, 0.4);
4667
- logger6.start("simulateGaze", `duration=${durationMs}ms`);
4569
+ const durationMs = jitterMs(baseDurationMs, 0.4);
4668
4570
  const startTime = Date.now();
4669
- const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
4670
4571
  while (Date.now() - startTime < durationMs) {
4671
- const x = 100 + Math.random() * (viewportSize.width - 200);
4672
- const y = 100 + Math.random() * (viewportSize.height - 200);
4673
- await cursor.actions.move({ x, y });
4674
- await (0, import_delay4.default)(this.jitterMs(600, 0.5));
4572
+ if (Math.random() < 0.28) {
4573
+ const distance = 70 + Math.random() * 120;
4574
+ await dispatchTouchSwipe(page, Math.random() < 0.7 ? distance : -distance, {
4575
+ durationMs: 180,
4576
+ steps: 4
4577
+ });
4578
+ } else {
4579
+ await waitJitter(420, 0.55);
4580
+ }
4675
4581
  }
4676
- logger6.success("simulateGaze");
4677
4582
  },
4678
- /**
4679
- * 人类化输入 - 带节奏变化(快-慢-停顿-偶尔加速)
4680
- * @param {import('playwright').Page} page
4681
- * @param {string} selector - 输入框选择器
4682
- * @param {string} text - 要输入的文本
4683
- * @param {Object} [options]
4684
- * @param {number} [options.baseDelay=180] - 基础按键延迟 (ms),实际 ±40% 抖动
4685
- * @param {number} [options.pauseProbability=0.08] - 停顿概率 (0-1)
4686
- * @param {number} [options.pauseBase=800] - 停顿时长基础值 (ms),实际 ±50% 抖动
4687
- */
4688
4583
  async humanType(page, selector, text, options = {}) {
4689
- logger6.start("humanType", `selector=${selector}, textLen=${text.length}`);
4690
4584
  const {
4691
- baseDelay = 180,
4585
+ baseDelay = 160,
4692
4586
  pauseProbability = 0.08,
4693
- pauseBase = 800
4587
+ pauseBase = 700
4694
4588
  } = options;
4695
- try {
4696
- const locator = page.locator(selector);
4697
- await Humanize.humanClick(page, locator);
4698
- await (0, import_delay4.default)(this.jitterMs(200, 0.4));
4699
- for (let i = 0; i < text.length; i++) {
4700
- const char = text[i];
4701
- let charDelay;
4702
- if (char === " ") {
4703
- charDelay = this.jitterMs(baseDelay * 0.6, 0.3);
4704
- } else if (/[,.!?;:,。!?;:]/.test(char)) {
4705
- charDelay = this.jitterMs(baseDelay * 1.5, 0.4);
4706
- } else {
4707
- charDelay = this.jitterMs(baseDelay, 0.4);
4708
- }
4709
- await page.keyboard.type(char);
4710
- await (0, import_delay4.default)(charDelay);
4711
- if (Math.random() < pauseProbability && i < text.length - 1) {
4712
- const pauseTime = this.jitterMs(pauseBase, 0.5);
4713
- logger6.debug(`\u505C\u987F ${pauseTime}ms...`);
4714
- await (0, import_delay4.default)(pauseTime);
4715
- }
4589
+ await MobileHumanize.humanClick(page, selector, { scrollIfNeeded: true });
4590
+ await waitJitter(220, 0.45);
4591
+ for (let i = 0; i < String(text).length; i += 1) {
4592
+ const char = String(text)[i];
4593
+ const charDelay = /[,.!?;:,。!?;:]/.test(char) ? jitterMs(baseDelay * 1.45, 0.4) : jitterMs(char === " " ? baseDelay * 0.65 : baseDelay, 0.4);
4594
+ await page.keyboard.type(char);
4595
+ await waitJitter(charDelay, 0.15);
4596
+ if (Math.random() < pauseProbability && i < String(text).length - 1) {
4597
+ await waitJitter(pauseBase, 0.5);
4716
4598
  }
4717
- logger6.success("humanType");
4718
- } catch (error) {
4719
- logger6.fail("humanType", error);
4720
- throw error;
4721
4599
  }
4722
4600
  },
4723
- /**
4724
- * 人类化按键 - 模拟用户在当前焦点或指定目标上按下一次键。
4725
- * @param {import('playwright').Page} page
4726
- * @param {string|import('playwright').ElementHandle|import('playwright').Locator} targetOrKey - 目标或按键
4727
- * @param {string|Object} [maybeKey] - 按键或选项
4728
- * @param {Object} [options]
4729
- */
4730
4601
  async humanPress(page, targetOrKey, maybeKey, options = {}) {
4731
4602
  const hasTarget = typeof maybeKey === "string";
4732
4603
  const key = hasTarget ? maybeKey : targetOrKey;
4733
4604
  const pressOptions = hasTarget ? options : maybeKey || options;
4734
4605
  const {
4735
- reactionDelay = 180,
4736
- holdDelay = 45,
4606
+ reactionDelay = 170,
4607
+ holdDelay = 42,
4737
4608
  focusDelay = 180,
4738
4609
  scrollIfNeeded = true,
4739
4610
  throwOnMissing = true,
4740
4611
  keyboardOptions = {}
4741
4612
  } = pressOptions || {};
4742
- const targetDesc = hasTarget ? typeof targetOrKey === "string" ? targetOrKey : "ElementHandle" : "current focus";
4613
+ const targetDesc = hasTarget ? describeTarget(targetOrKey) : "current focus";
4743
4614
  logger6.start("humanPress", `key=${key}, target=${targetDesc}`);
4744
4615
  try {
4745
4616
  if (hasTarget) {
4746
- await this.humanClick(page, targetOrKey, { reactionDelay: focusDelay, scrollIfNeeded, throwOnMissing });
4617
+ await MobileHumanize.humanClick(page, targetOrKey, {
4618
+ reactionDelay: focusDelay,
4619
+ scrollIfNeeded,
4620
+ throwOnMissing
4621
+ });
4747
4622
  }
4748
- await (0, import_delay4.default)(this.jitterMs(reactionDelay, 0.45));
4623
+ await waitJitter(reactionDelay, 0.45);
4749
4624
  await page.keyboard.press(key, {
4750
4625
  ...keyboardOptions,
4751
- delay: this.jitterMs(holdDelay, 0.5)
4626
+ delay: jitterMs(holdDelay, 0.5)
4752
4627
  });
4753
4628
  logger6.success("humanPress");
4754
4629
  return true;
@@ -4757,164 +4632,153 @@ var Humanize = {
4757
4632
  throw error;
4758
4633
  }
4759
4634
  },
4760
- /**
4761
- * 人类化清空输入框 - 模拟人类删除文本的行为
4762
- * @param {import('playwright').Page} page
4763
- * @param {string} selector - 输入框选择器
4764
- */
4765
4635
  async humanClear(page, selector) {
4766
- logger6.start("humanClear", `selector=${selector}`);
4767
- try {
4768
- const locator = page.locator(selector);
4769
- await locator.click();
4770
- await (0, import_delay4.default)(this.jitterMs(200, 0.4));
4771
- const currentValue = await locator.inputValue();
4772
- if (!currentValue || currentValue.length === 0) {
4773
- logger6.success("humanClear", "already empty");
4636
+ const locator = page.locator(selector);
4637
+ await MobileHumanize.humanClick(page, locator, { scrollIfNeeded: true });
4638
+ await waitJitter(160, 0.4);
4639
+ const readValue = async () => {
4640
+ try {
4641
+ return await locator.inputValue({ timeout: 600 });
4642
+ } catch {
4643
+ return await locator.evaluate((el) => "value" in el ? String(el.value || "") : String(el.textContent || "")).catch(() => "");
4644
+ }
4645
+ };
4646
+ const currentValue = await readValue();
4647
+ if (!currentValue) return;
4648
+ await page.keyboard.press("ControlOrMeta+A");
4649
+ await waitJitter(90, 0.35);
4650
+ await page.keyboard.press("Backspace");
4651
+ await waitJitter(120, 0.35);
4652
+ if (!await readValue()) return;
4653
+ await locator.evaluate((el) => {
4654
+ if ("value" in el) {
4655
+ el.value = "";
4656
+ el.dispatchEvent(new Event("input", { bubbles: true }));
4657
+ el.dispatchEvent(new Event("change", { bubbles: true }));
4774
4658
  return;
4775
4659
  }
4776
- await page.keyboard.press("Meta+A");
4777
- await (0, import_delay4.default)(this.jitterMs(100, 0.4));
4778
- await page.keyboard.press("Backspace");
4779
- logger6.success("humanClear");
4780
- } catch (error) {
4781
- logger6.fail("humanClear", error);
4782
- throw error;
4783
- }
4660
+ el.textContent = "";
4661
+ el.dispatchEvent(new Event("input", { bubbles: true }));
4662
+ });
4784
4663
  },
4785
- /**
4786
- * 页面预热浏览 - 模拟人类进入页面后的探索行为
4787
- * @param {import('playwright').Page} page
4788
- * @param {number} [baseDuration=3500] - 预热时长基础值 (±40% 抖动)
4789
- */
4790
4664
  async warmUpBrowsing(page, baseDuration = 3500) {
4791
- const cursor = $GetCursor(page);
4792
- const durationMs = this.jitterMs(baseDuration, 0.4);
4793
- logger6.start("warmUpBrowsing", `duration=${durationMs}ms`);
4665
+ const durationMs = jitterMs(baseDuration, 0.4);
4794
4666
  const startTime = Date.now();
4795
- const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
4796
- try {
4797
- while (Date.now() - startTime < durationMs) {
4798
- const action = Math.random();
4799
- if (action < 0.4) {
4800
- const x = 100 + Math.random() * (viewportSize.width - 200);
4801
- const y = 100 + Math.random() * (viewportSize.height - 200);
4802
- await cursor.actions.move({ x, y });
4803
- await (0, import_delay4.default)(this.jitterMs(350, 0.4));
4804
- } else if (action < 0.7) {
4805
- const scrollY = (Math.random() - 0.5) * 200;
4806
- await page.mouse.wheel(0, scrollY);
4807
- await (0, import_delay4.default)(this.jitterMs(500, 0.4));
4808
- } else {
4809
- await (0, import_delay4.default)(this.jitterMs(800, 0.5));
4810
- }
4667
+ while (Date.now() - startTime < durationMs) {
4668
+ const action = Math.random();
4669
+ if (action < 0.5) {
4670
+ await dispatchTouchSwipe(page, 120 + Math.random() * 220, {
4671
+ durationMs: 240,
4672
+ steps: 5
4673
+ });
4674
+ } else if (action < 0.7) {
4675
+ await dispatchTouchSwipe(page, -(80 + Math.random() * 160), {
4676
+ durationMs: 220,
4677
+ steps: 4
4678
+ });
4679
+ } else {
4680
+ await waitJitter(560, 0.55);
4811
4681
  }
4812
- logger6.success("warmUpBrowsing");
4813
- } catch (error) {
4814
- logger6.fail("warmUpBrowsing", error);
4815
- throw error;
4816
4682
  }
4817
4683
  },
4818
- /**
4819
- * 自然滚动 - 带惯性、减速效果和随机抖动
4820
- * @param {import('playwright').Page} page
4821
- * @param {'up' | 'down'} [direction='down'] - 滚动方向
4822
- * @param {number} [distance=300] - 总滚动距离基础值 (px),±15% 抖动
4823
- * @param {number} [baseSteps=5] - 分几步完成基础值,±1 随机
4824
- */
4825
4684
  async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
4826
4685
  const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
4827
- const actualDistance = this.jitterMs(distance, 0.15);
4828
- logger6.start("naturalScroll", `dir=${direction}, dist=${actualDistance}, steps=${steps}`);
4686
+ const actualDistance = jitterMs(distance, 0.15);
4829
4687
  const sign = direction === "down" ? 1 : -1;
4830
- const stepDistance = actualDistance / steps;
4831
- try {
4832
- for (let i = 0; i < steps; i++) {
4833
- const factor = 1 - i / steps * 0.5;
4834
- const jitter = 0.9 + Math.random() * 0.2;
4835
- const scrollAmount = stepDistance * factor * sign * jitter;
4836
- await page.mouse.wheel(0, scrollAmount);
4837
- const baseDelay = 60 + i * 25;
4838
- await (0, import_delay4.default)(this.jitterMs(baseDelay, 0.3));
4839
- }
4840
- logger6.success("naturalScroll");
4841
- } catch (error) {
4842
- logger6.fail("naturalScroll", error);
4843
- throw error;
4688
+ for (let i = 0; i < steps; i += 1) {
4689
+ const factor = 1 - i / steps * 0.45;
4690
+ await dispatchTouchSwipe(page, actualDistance / steps * factor * sign, {
4691
+ durationMs: 150 + i * 20,
4692
+ steps: 4
4693
+ });
4844
4694
  }
4845
4695
  }
4846
4696
  };
4847
4697
 
4848
4698
  // src/internals/humanize/default.js
4849
- var resolveDeviceFromPage3 = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
4850
- var resolveDefaultHumanizeDelegate = (page) => resolveDeviceFromPage3(page) === Device.Mobile ? MobileHumanize : Humanize;
4851
- var DefaultHumanizeContext = Object.freeze({
4852
- desktopDelegate: Humanize,
4853
- resolveDelegate: resolveDefaultHumanizeDelegate
4699
+ var resolveDeviceFromPage2 = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
4700
+ var resolveDelegate = (page) => {
4701
+ return resolveDeviceFromPage2(page) === Device.Mobile ? MobileHumanize : Humanize;
4702
+ };
4703
+ var DefaultHumanizeDevice = withPageReflect(
4704
+ "DefaultHumanize",
4705
+ resolveDelegate,
4706
+ [Humanize, MobileHumanize]
4707
+ );
4708
+
4709
+ // src/internals/humanize/cloak.js
4710
+ var FORCE_CLICK = Object.freeze({
4711
+ forceClick: true,
4712
+ clickOptions: { force: true }
4854
4713
  });
4714
+ var pointOrNull = async (target) => {
4715
+ if (!target || typeof target.boundingBox !== "function") return null;
4716
+ const box = await target.boundingBox().catch(() => null);
4717
+ return box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : null;
4718
+ };
4719
+ var CloakHumanizeInput = {
4720
+ async initializeCursor(page) {
4721
+ return Boolean(page);
4722
+ },
4723
+ async humanMove(page, target) {
4724
+ const point = target?.x != null && target?.y != null ? target : await pointOrNull(typeof target === "string" ? page.locator(target).first() : target);
4725
+ return point ? await DeviceInput.move(page, point, { forceMouse: true }) : false;
4726
+ },
4727
+ async humanScroll(page, target) {
4728
+ const element = typeof target === "string" ? page.locator(target).first() : target;
4729
+ if (!element) return { element: null, didScroll: false, restore: null };
4730
+ await element.scrollIntoViewIfNeeded?.();
4731
+ return { element, didScroll: true, restore: null };
4732
+ },
4733
+ async humanClick(page, target) {
4734
+ return await DeviceInput.click(page, target, FORCE_CLICK);
4735
+ },
4736
+ async humanType(page, selector, text) {
4737
+ await DeviceInput.click(page, selector, FORCE_CLICK);
4738
+ return await DeviceInput.keyboardType(page, text);
4739
+ },
4740
+ async humanPress(page, targetOrKey, maybeKey) {
4741
+ return await DeviceInput.press(page, targetOrKey, maybeKey, {
4742
+ clickOptions: FORCE_CLICK,
4743
+ keyboardOptions: {}
4744
+ });
4745
+ },
4746
+ async humanClear(page, selector) {
4747
+ return await DeviceInput.fill(page, selector, "", { force: true });
4748
+ },
4749
+ async simulateGaze(page) {
4750
+ return await DeviceInput.move(page, { x: 0, y: 0 }, { forceMouse: true });
4751
+ },
4752
+ async warmUpBrowsing(page) {
4753
+ return await this.simulateGaze(page);
4754
+ },
4755
+ async naturalScroll(page, direction = "down", distance = 300) {
4756
+ const sign = direction === "down" ? 1 : -1;
4757
+ await page.mouse.wheel(0, Number(distance || 0) * sign);
4758
+ }
4759
+ };
4855
4760
 
4856
4761
  // src/internals/humanize/index.js
4857
- var HUMANIZE_DELEGATED_METHODS = Object.freeze({
4858
- initializeCursor: { enumerable: true },
4859
- humanMove: { enumerable: true },
4860
- humanScroll: { enumerable: true },
4861
- humanClick: { enumerable: true },
4862
- simulateGaze: { enumerable: true },
4863
- humanType: { enumerable: true },
4864
- humanPress: { enumerable: true },
4865
- humanClear: { enumerable: true },
4866
- warmUpBrowsing: { enumerable: true },
4867
- naturalScroll: { enumerable: true }
4868
- });
4869
- var isPageLike2 = (value) => value && typeof value === "object" && typeof value.evaluate === "function";
4870
- var createSharedHumanizeMethods = ({ desktopDelegate, resolveDelegate }) => ({
4762
+ var HumanizeCommon = {
4871
4763
  jitterMs(base, jitterPercent = 0.3) {
4872
- return desktopDelegate.jitterMs(base, jitterPercent);
4764
+ return jitterMs(base, jitterPercent);
4873
4765
  },
4874
- randomSleep(pageOrBaseMs, maybeBaseMs, maybeJitterPercent) {
4875
- if (isPageLike2(pageOrBaseMs)) {
4876
- return resolveDelegate(pageOrBaseMs).randomSleep(maybeBaseMs, maybeJitterPercent);
4877
- }
4878
- return desktopDelegate.randomSleep(pageOrBaseMs, maybeBaseMs);
4766
+ async randomSleep(pageOrBaseMs, maybeBaseMs, maybeJitterPercent) {
4767
+ const hasPage = pageOrBaseMs && typeof pageOrBaseMs === "object" && typeof pageOrBaseMs.evaluate === "function";
4768
+ const baseMs = hasPage ? maybeBaseMs : pageOrBaseMs;
4769
+ const jitterPercent = hasPage ? maybeJitterPercent : maybeBaseMs;
4770
+ await (0, import_delay4.default)(jitterMs(baseMs, jitterPercent ?? 0.3));
4879
4771
  }
4880
- });
4881
- var withHumanizeDelegates = (target, resolveDelegate) => withDelegatedProperties(target, {
4882
- namespace: "Humanize",
4883
- methods: HUMANIZE_DELEGATED_METHODS,
4884
- resolveDelegate: (_method, args) => ({
4885
- delegate: resolveDelegate(args[0]),
4886
- label: "resolved humanize delegate"
4887
- })
4888
- });
4889
- var createHumanizeExport = (context = {}) => {
4890
- return Object.assign(
4891
- createSharedHumanizeMethods(context),
4892
- withHumanizeDelegates({}, context.resolveDelegate)
4893
- );
4894
4772
  };
4895
- var DefaultHumanize = createHumanizeExport(DefaultHumanizeContext);
4896
- var CloakBrowserHumanize = createHumanizeExport(CloakBrowserHumanizeContext);
4773
+ var DefaultHumanize = Object.assign({}, DefaultHumanizeDevice, HumanizeCommon);
4774
+ var CloakHumanize = Object.assign({}, CloakHumanizeInput, HumanizeCommon);
4897
4775
 
4898
4776
  // src/humanize.js
4899
4777
  var humanizeStrategies = {
4900
4778
  [Mode.Default]: DefaultHumanize,
4901
- [Mode.CloakBrowser]: CloakBrowserHumanize
4902
- };
4903
- var humanizeFacadeMethods = Object.freeze({
4904
- jitterMs: { enumerable: true },
4905
- initializeCursor: { enumerable: true },
4906
- humanMove: { enumerable: true },
4907
- humanScroll: { enumerable: true },
4908
- humanClick: { enumerable: true },
4909
- randomSleep: { enumerable: true },
4910
- simulateGaze: { enumerable: true },
4911
- humanType: { enumerable: true },
4912
- humanPress: { enumerable: true },
4913
- humanClear: { enumerable: true },
4914
- warmUpBrowsing: { enumerable: true },
4915
- naturalScroll: { enumerable: true }
4916
- });
4917
- var Humanize2 = createDelegatedFacade("Humanize", humanizeStrategies, humanizeFacadeMethods);
4779
+ [Mode.Cloak]: CloakHumanize
4780
+ };
4781
+ var Humanize2 = withModeReflect("Humanize", humanizeStrategies);
4918
4782
 
4919
4783
  // src/internals/launch/default.js
4920
4784
  var import_node_child_process = require("node:child_process");
@@ -5352,10 +5216,10 @@ var DefaultLaunch = {
5352
5216
  }
5353
5217
  };
5354
5218
 
5355
- // src/internals/launch/cloakbrowser.js
5219
+ // src/internals/launch/cloak.js
5356
5220
  var import_node_child_process2 = require("node:child_process");
5357
5221
  var import_node_util = require("node:util");
5358
- var logger8 = createInternalLogger("CloakBrowser");
5222
+ var logger8 = createInternalLogger("Cloak");
5359
5223
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
5360
5224
  var DEFAULT_CLOAK_CRAWLER_BASE_OPTIONS = Object.freeze({
5361
5225
  maxConcurrency: 1,
@@ -5369,21 +5233,21 @@ var DEFAULT_CLOAK_HUMANIZE_OPTIONS = Object.freeze({
5369
5233
  var DEFAULT_CLOAK_GOTO_OPTIONS = Object.freeze({
5370
5234
  waitUntil: "commit"
5371
5235
  });
5372
- var cachedCloakBrowserModulePromise = null;
5236
+ var cachedCloakModulePromise = null;
5373
5237
  var hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key);
5374
- var loadCloakBrowserModule = async () => {
5375
- if (!cachedCloakBrowserModulePromise) {
5376
- cachedCloakBrowserModulePromise = import("cloakbrowser").catch((error) => {
5377
- cachedCloakBrowserModulePromise = null;
5378
- throw new Error("cloakbrowser \u6A21\u5757\u52A0\u8F7D\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u5DF2\u5B89\u88C5 cloakbrowser\u3002", {
5238
+ var loadCloakModule = async () => {
5239
+ if (!cachedCloakModulePromise) {
5240
+ cachedCloakModulePromise = import("cloakbrowser").catch((error) => {
5241
+ cachedCloakModulePromise = null;
5242
+ throw new Error("Cloak \u6A21\u5757\u52A0\u8F7D\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u5DF2\u5B89\u88C5 cloakbrowser\u3002", {
5379
5243
  cause: error
5380
5244
  });
5381
5245
  });
5382
5246
  }
5383
- return cachedCloakBrowserModulePromise;
5247
+ return cachedCloakModulePromise;
5384
5248
  };
5385
5249
  var buildCloakLaunchOptions = async (options = {}) => {
5386
- const { buildLaunchOptions } = await loadCloakBrowserModule();
5250
+ const { buildLaunchOptions } = await loadCloakModule();
5387
5251
  return await buildLaunchOptions(normalizeObject(options));
5388
5252
  };
5389
5253
  var normalizeObject = (value) => {
@@ -5398,7 +5262,7 @@ var normalizeStringArray = (value) => {
5398
5262
  }
5399
5263
  return value.map((item) => String(item || "").trim()).filter(Boolean);
5400
5264
  };
5401
- var resolveCloakBrowserProxy = (proxyConfiguration = {}) => {
5265
+ var resolveCloakProxy = (proxyConfiguration = {}) => {
5402
5266
  const config = normalizeObject(proxyConfiguration);
5403
5267
  const proxyUrl = String(config.proxy_url || "").trim();
5404
5268
  const enableProxy = typeof config.enable_proxy === "boolean" ? config.enable_proxy : proxyUrl !== "";
@@ -5432,7 +5296,7 @@ var createStableGotoHook = (recommendedGotoOptions = DEFAULT_CLOAK_GOTO_OPTIONS)
5432
5296
  }
5433
5297
  };
5434
5298
  };
5435
- var attachCloakBrowserHumanizeHook = ({
5299
+ var attachCloakHumanizeHook = ({
5436
5300
  browserPoolOptions = {},
5437
5301
  activeBrowsers,
5438
5302
  patchedBrowsers,
@@ -5463,7 +5327,7 @@ var attachCloakBrowserHumanizeHook = ({
5463
5327
  if (!shouldHumanize || patchedBrowsers.has(browser)) {
5464
5328
  return;
5465
5329
  }
5466
- const { humanizeBrowser } = await loadCloakBrowserModule();
5330
+ const { humanizeBrowser } = await loadCloakModule();
5467
5331
  await humanizeBrowser(browser, normalizedHumanizeOptions);
5468
5332
  patchedBrowsers.add(browser);
5469
5333
  }
@@ -5491,12 +5355,12 @@ var forceTerminateBrowsersByFingerprintArg = async (fingerprintArg) => {
5491
5355
  if (error?.code === 1 || error?.code === "ENOENT") {
5492
5356
  return;
5493
5357
  }
5494
- logger8.info(`\u5F3A\u5236\u5173\u95ED CloakBrowser \u8FDB\u7A0B\u5931\u8D25\uFF08\u5FFD\u7565\uFF09: ${error?.message || String(error)}`);
5358
+ logger8.info(`\u5F3A\u5236\u5173\u95ED Cloak \u8FDB\u7A0B\u5931\u8D25\uFF08\u5FFD\u7565\uFF09: ${error?.message || String(error)}`);
5495
5359
  });
5496
5360
  };
5497
- var CloakBrowserLaunch = {
5361
+ var CloakLaunch = {
5498
5362
  resolveProxyConfiguration(proxyConfiguration = {}) {
5499
- return resolveCloakBrowserProxy(proxyConfiguration);
5363
+ return resolveCloakProxy(proxyConfiguration);
5500
5364
  },
5501
5365
  extractFingerprintArg(launchOptions = {}) {
5502
5366
  return extractFingerprintArg(launchOptions);
@@ -5505,7 +5369,7 @@ var CloakBrowserLaunch = {
5505
5369
  return createStableGotoHook(recommendedGotoOptions);
5506
5370
  },
5507
5371
  async getPlaywrightCrawlerOptions(options = {}) {
5508
- const runtime2 = await CloakBrowserLaunch.createPlaywrightCrawlerRuntime(options);
5372
+ const runtime2 = await CloakLaunch.createPlaywrightCrawlerRuntime(options);
5509
5373
  return Object.defineProperties(runtime2.crawlerOptions, {
5510
5374
  cleanup: {
5511
5375
  enumerable: false,
@@ -5545,7 +5409,7 @@ var CloakBrowserLaunch = {
5545
5409
  const patchedBrowsers = /* @__PURE__ */ new WeakSet();
5546
5410
  const defaultArgs = isRunningOnApify ? ["--no-sandbox", "--disable-setuid-sandbox"] : [];
5547
5411
  const extraArgs = normalizeStringArray(normalizedCloakOptions.args);
5548
- const proxy = hasOwn(normalizedCloakOptions, "proxy") ? normalizedCloakOptions.proxy : resolveCloakBrowserProxy(proxyConfiguration);
5412
+ const proxy = hasOwn(normalizedCloakOptions, "proxy") ? normalizedCloakOptions.proxy : resolveCloakProxy(proxyConfiguration);
5549
5413
  const headless = hasOwn(normalizedCloakOptions, "headless") ? normalizedCloakOptions.headless : !runInHeadfulMode || isRunningOnApify;
5550
5414
  const mergedCloakOptions = {
5551
5415
  ...normalizedCloakOptions,
@@ -5568,7 +5432,7 @@ var CloakBrowserLaunch = {
5568
5432
  ...launcher ? { launcher } : {},
5569
5433
  launchOptions
5570
5434
  },
5571
- browserPoolOptions: attachCloakBrowserHumanizeHook({
5435
+ browserPoolOptions: attachCloakHumanizeHook({
5572
5436
  browserPoolOptions,
5573
5437
  activeBrowsers,
5574
5438
  patchedBrowsers,
@@ -5602,12 +5466,9 @@ var CloakBrowserLaunch = {
5602
5466
  // src/launch.js
5603
5467
  var launchStrategies = {
5604
5468
  [Mode.Default]: DefaultLaunch,
5605
- [Mode.CloakBrowser]: CloakBrowserLaunch
5469
+ [Mode.Cloak]: CloakLaunch
5606
5470
  };
5607
- var launchFacadeMethods = Object.freeze({
5608
- getPlaywrightCrawlerOptions: { enumerable: true }
5609
- });
5610
- var Launch = createDelegatedFacade("Launch", launchStrategies, launchFacadeMethods);
5471
+ var Launch = withModeReflect("Launch", launchStrategies);
5611
5472
 
5612
5473
  // src/live-view.js
5613
5474
  var import_express = __toESM(require("express"), 1);
@@ -6696,7 +6557,7 @@ var Mutation = {
6696
6557
  const overallTimeout = options.timeout ?? 180 * 1e3;
6697
6558
  const onMutation = options.onMutation;
6698
6559
  const pollInterval = 500;
6699
- const sleep2 = (ms) => new Promise((resolve) => {
6560
+ const sleep = (ms) => new Promise((resolve) => {
6700
6561
  setTimeout(resolve, ms);
6701
6562
  });
6702
6563
  const truncate = (value, max = 800) => {
@@ -6888,7 +6749,7 @@ var Mutation = {
6888
6749
  const deadline = Date.now() + overallTimeout;
6889
6750
  let lastState = state2;
6890
6751
  while (Date.now() < deadline) {
6891
- await sleep2(pollInterval);
6752
+ await sleep(pollInterval);
6892
6753
  lastState = await buildState();
6893
6754
  if (!lastState?.hasMatched) {
6894
6755
  continue;
@@ -8473,7 +8334,7 @@ var buildWatermarkifyRenderHtml = ({ imageSrc, overlaySvg, width, height, imageH
8473
8334
  `;
8474
8335
  };
8475
8336
  var normalizeWatermarkifyRenderMode = (value) => {
8476
- return String(value || "default").trim().toLowerCase() === "cloakbrowser" ? "cloakbrowser" : "default";
8337
+ return String(value || "default").trim().toLowerCase() === "cloak" ? "cloak" : "default";
8477
8338
  };
8478
8339
  var composeScreenshotBufferWithBrowser = async (page, buffer, overlaySvg, imageInfo = {}, options = {}) => {
8479
8340
  if (!page || typeof page.context !== "function") {
@@ -8500,7 +8361,7 @@ var composeScreenshotBufferWithBrowser = async (page, buffer, overlaySvg, imageI
8500
8361
  }).catch(() => {
8501
8362
  });
8502
8363
  const renderMode = normalizeWatermarkifyRenderMode(options.mode);
8503
- if (renderMode === "cloakbrowser") {
8364
+ if (renderMode === "cloak") {
8504
8365
  const renderHtml = buildWatermarkifyRenderHtml({
8505
8366
  imageSrc: `data:${imageInfo.mimeType || "image/png"};base64,${buffer.toString("base64")}`,
8506
8367
  overlaySvg,
@@ -10081,7 +9942,7 @@ var Share = {
10081
9942
  * @param {number} [options.maxBytes] 默认 5MiB,返回 base64 超过后会压缩
10082
9943
  * @param {'jpeg'|'jpg'} [options.type] 压缩输出格式,默认 jpeg
10083
9944
  * @param {boolean|Object} [options.compression] 传 false 可关闭压缩
10084
- * @param {'default'|'cloakbrowser'} [options.mode] 截图水印合成模式,默认 default
9945
+ * @param {'default'|'cloak'} [options.mode] 截图水印合成模式,默认 default
10085
9946
  * @returns {Promise<string>} base64 image
10086
9947
  */
10087
9948
  async captureScreen(page, options = {}) {
@@ -10103,7 +9964,7 @@ var Share = {
10103
9964
  capturedAt
10104
9965
  });
10105
9966
  outputBuffer = await watermarkifyScreenshotBuffer(rawBuffer, watermarkifyMeta, page, {
10106
- mode: options.mode === "cloakbrowser" ? "cloakbrowser" : "default"
9967
+ mode: options.mode
10107
9968
  });
10108
9969
  }
10109
9970
  return await compressImageBufferToBase64(outputBuffer, compression);