@skrillex1224/playwright-toolkit 2.1.286 → 3.0.2

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");
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" };
3825
3352
  }
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;
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
+ }
3849
3364
  }
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;
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
+ };
3868
3384
  }
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) => {
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 });
3950
3463
  }
3951
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));
3952
3468
  }
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 };
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}`);
3515
+ }
3516
+ logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
3517
+ return false;
3959
3518
  }
3519
+ } else {
3520
+ element = target;
3960
3521
  }
3961
- didScroll = true;
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;
3962
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;
3963
3596
  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 };
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);
3609
+ }
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);
3616
+ }
3971
3617
  }
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
- }
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;
4045
3712
  }
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}`);
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));
4065
3740
  }
4066
- await waitJitter(120, 0.35);
4067
- logger5.success("humanClick");
4068
- return true;
3741
+ logger5.success("naturalScroll");
4069
3742
  } catch (error) {
4070
- logger5.fail("humanClick", error);
3743
+ logger5.fail("naturalScroll", error);
4071
3744
  throw error;
4072
3745
  }
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);
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;
4089
3844
  }
4090
3845
  }
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);
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);
3857
+ }
3858
+ if (clipsY) {
3859
+ clipTop = Math.max(clipTop, nodeRect.top);
3860
+ clipBottom = Math.min(clipBottom, nodeRect.bottom);
4107
3861
  }
4108
3862
  }
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
- });
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
+ };
3881
+ }
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;
4131
3886
  }
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;
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;
3893
+ }
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;
3904
+ }
3905
+ return common.contains(el) && common.contains(pointElement);
3906
+ };
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 };
3936
+ }
3937
+ obstruction = obstruction || describeElement(pointElement);
4142
3938
  }
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(() => "");
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";
4153
3962
  }
3963
+ return false;
4154
3964
  };
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;
3965
+ const findEditable = (node) => {
3966
+ for (let current = node; current && current !== document.body; current = current.parentElement) {
3967
+ if (isEditable(current)) return current;
4168
3968
  }
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);
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 || "" };
3980
+ }
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 || "" };
4203
3995
  }
4204
- }
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
- }
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");
4463
4316
  }
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 };
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;
4467
4340
  }
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
- };
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;
4483
4359
  }
4484
- return { code: "VISIBLE", isFixed };
4485
- });
4486
- };
4487
- const getScrollableRect2 = async () => {
4488
- return await element.evaluate((el) => {
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");
4658
- },
4659
- /**
4660
- * 模拟人类"注视"或"阅读"行为:鼠标在页面上随机微动
4661
- * @param {import('playwright').Page} page
4662
- * @param {number} [baseDurationMs=2500] - 基础持续时间 (±40% 抖动)
4663
- */
4566
+ await waitJitter(baseMs, jitterPercent);
4567
+ },
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,11 +5216,12 @@ 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("Launch");
5359
5223
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
5224
+ var REQUEST_HOOK_FLAG2 = Symbol("playwright-toolkit-cloak-request-hook");
5360
5225
  var DEFAULT_CLOAK_CRAWLER_BASE_OPTIONS = Object.freeze({
5361
5226
  maxConcurrency: 1,
5362
5227
  maxRequestRetries: 0,
@@ -5369,21 +5234,21 @@ var DEFAULT_CLOAK_HUMANIZE_OPTIONS = Object.freeze({
5369
5234
  var DEFAULT_CLOAK_GOTO_OPTIONS = Object.freeze({
5370
5235
  waitUntil: "commit"
5371
5236
  });
5372
- var cachedCloakBrowserModulePromise = null;
5237
+ var cachedCloakModulePromise = null;
5373
5238
  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", {
5239
+ var loadCloakModule = async () => {
5240
+ if (!cachedCloakModulePromise) {
5241
+ cachedCloakModulePromise = import("cloakbrowser").catch((error) => {
5242
+ cachedCloakModulePromise = null;
5243
+ 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
5244
  cause: error
5380
5245
  });
5381
5246
  });
5382
5247
  }
5383
- return cachedCloakBrowserModulePromise;
5248
+ return cachedCloakModulePromise;
5384
5249
  };
5385
5250
  var buildCloakLaunchOptions = async (options = {}) => {
5386
- const { buildLaunchOptions } = await loadCloakBrowserModule();
5251
+ const { buildLaunchOptions } = await loadCloakModule();
5387
5252
  return await buildLaunchOptions(normalizeObject(options));
5388
5253
  };
5389
5254
  var normalizeObject = (value) => {
@@ -5398,7 +5263,7 @@ var normalizeStringArray = (value) => {
5398
5263
  }
5399
5264
  return value.map((item) => String(item || "").trim()).filter(Boolean);
5400
5265
  };
5401
- var resolveCloakBrowserProxy = (proxyConfiguration = {}) => {
5266
+ var resolveCloakProxy = (proxyConfiguration = {}) => {
5402
5267
  const config = normalizeObject(proxyConfiguration);
5403
5268
  const proxyUrl = String(config.proxy_url || "").trim();
5404
5269
  const enableProxy = typeof config.enable_proxy === "boolean" ? config.enable_proxy : proxyUrl !== "";
@@ -5417,6 +5282,86 @@ var resolveCloakBrowserProxy = (proxyConfiguration = {}) => {
5417
5282
  bypass: byPassDomains.join(",")
5418
5283
  };
5419
5284
  };
5285
+ var resolveProxyLaunchOptions2 = (proxyConfiguration = {}) => {
5286
+ const config = normalizeObject(proxyConfiguration);
5287
+ const proxyUrl = String(config.proxy_url || "").trim();
5288
+ const enableProxy = typeof config.enable_proxy === "boolean" ? config.enable_proxy : proxyUrl !== "";
5289
+ const byPassDomains = enableProxy && proxyUrl ? ByPass.normalizeByPassDomains(config.by_pass_domains) : [];
5290
+ return {
5291
+ byPassDomains,
5292
+ enableProxy,
5293
+ proxyUrl
5294
+ };
5295
+ };
5296
+ var buildMeteredProxy = ({ proxyConfiguration = {}, debugMode = false } = {}) => {
5297
+ const { byPassDomains, enableProxy, proxyUrl } = resolveProxyLaunchOptions2(proxyConfiguration);
5298
+ const byPassRules = ByPass.buildByPassDomainRules(byPassDomains);
5299
+ const proxyMeter = enableProxy && proxyUrl ? ProxyMeterRuntime.startProxyMeter({ proxyUrl, debugMode }) : null;
5300
+ const launchProxy = proxyMeter ? { server: proxyMeter.server } : null;
5301
+ if (launchProxy && byPassDomains.length > 0) {
5302
+ launchProxy.bypass = byPassDomains.join(",");
5303
+ }
5304
+ return {
5305
+ byPassDomains,
5306
+ byPassRules,
5307
+ enableProxy,
5308
+ proxyUrl,
5309
+ launchProxy
5310
+ };
5311
+ };
5312
+ var logProxyLaunchState = ({
5313
+ byPassDomains = [],
5314
+ debugMode = false,
5315
+ enableByPassLogger = false,
5316
+ enableProxy = false,
5317
+ launchProxy = null,
5318
+ proxyUrl = ""
5319
+ } = {}) => {
5320
+ if (!enableByPassLogger) return;
5321
+ if (launchProxy) {
5322
+ let upstreamLabel = "";
5323
+ try {
5324
+ const parsedProxyUrl = new URL(proxyUrl.includes("://") ? proxyUrl : `http://${proxyUrl}`);
5325
+ upstreamLabel = `${parsedProxyUrl.protocol}//${parsedProxyUrl.host}`;
5326
+ } catch {
5327
+ }
5328
+ logger8.info(
5329
+ `[\u4EE3\u7406\u5DF2\u542F\u7528] \u672C\u5730=${launchProxy.server} \u4E0A\u6E38=${upstreamLabel || "-"} \u76F4\u8FDE\u57DF\u540D=${byPassDomains.join(",")}`
5330
+ );
5331
+ logger8.info(`[\u6D41\u91CF\u89C2\u6D4B] \u9010\u8BF7\u6C42\u8C03\u8BD5=${Boolean(debugMode) ? "\u5F00\u542F" : "\u5173\u95ED"}\uFF08\u6C47\u603B\u59CB\u7EC8\u5F00\u542F\uFF09`);
5332
+ return;
5333
+ }
5334
+ if (enableProxy) {
5335
+ logger8.info("[\u4EE3\u7406\u672A\u542F\u7528] enable_proxy=true \u4F46 proxy_url \u4E3A\u7A7A");
5336
+ } else if (proxyUrl) {
5337
+ logger8.info("[\u4EE3\u7406\u672A\u542F\u7528] enable_proxy=false \u4E14 proxy_url \u5DF2\u914D\u7F6E");
5338
+ }
5339
+ logger8.info(`[\u6D41\u91CF\u89C2\u6D4B] \u9010\u8BF7\u6C42\u8C03\u8BD5=${Boolean(debugMode) ? "\u5F00\u542F" : "\u5173\u95ED"}\uFF08\u6C47\u603B\u59CB\u7EC8\u5F00\u542F\uFF09`);
5340
+ };
5341
+ var createProxyRequestHook = ({
5342
+ byPassDomains = [],
5343
+ byPassRules = [],
5344
+ enableByPassLogger = false,
5345
+ launchProxy = null
5346
+ } = {}) => {
5347
+ return (page) => {
5348
+ if (!page || typeof page.on !== "function" || page[REQUEST_HOOK_FLAG2]) {
5349
+ return;
5350
+ }
5351
+ page[REQUEST_HOOK_FLAG2] = true;
5352
+ page.on("request", (req) => {
5353
+ const requestUrl = req.url();
5354
+ const resourceType = req.resourceType();
5355
+ const matched = byPassDomains.length > 0 ? ByPass.findMatchedByPassRule(byPassRules, requestUrl) : null;
5356
+ if (launchProxy) {
5357
+ ProxyMeterRuntime.recordProxyMeterResourceType(requestUrl, resourceType);
5358
+ }
5359
+ if (!enableByPassLogger || byPassDomains.length === 0) return;
5360
+ if (!matched || !matched.rule) return;
5361
+ logger8.info(`[\u76F4\u8FDE\u547D\u4E2D] \u89C4\u5219=${matched.rule.pattern} \u57DF\u540D=${matched.hostname} \u8D44\u6E90\u7C7B\u578B=${resourceType} \u65B9\u6CD5=${req.method()} \u5730\u5740=${requestUrl}`);
5362
+ });
5363
+ };
5364
+ };
5420
5365
  var extractFingerprintArg = (launchOptions = {}) => {
5421
5366
  const args = Array.isArray(launchOptions?.args) ? launchOptions.args : [];
5422
5367
  return args.find((value) => String(value || "").startsWith("--fingerprint=")) || "";
@@ -5432,7 +5377,7 @@ var createStableGotoHook = (recommendedGotoOptions = DEFAULT_CLOAK_GOTO_OPTIONS)
5432
5377
  }
5433
5378
  };
5434
5379
  };
5435
- var attachCloakBrowserHumanizeHook = ({
5380
+ var attachCloakHumanizeHook = ({
5436
5381
  browserPoolOptions = {},
5437
5382
  activeBrowsers,
5438
5383
  patchedBrowsers,
@@ -5463,7 +5408,7 @@ var attachCloakBrowserHumanizeHook = ({
5463
5408
  if (!shouldHumanize || patchedBrowsers.has(browser)) {
5464
5409
  return;
5465
5410
  }
5466
- const { humanizeBrowser } = await loadCloakBrowserModule();
5411
+ const { humanizeBrowser } = await loadCloakModule();
5467
5412
  await humanizeBrowser(browser, normalizedHumanizeOptions);
5468
5413
  patchedBrowsers.add(browser);
5469
5414
  }
@@ -5491,12 +5436,12 @@ var forceTerminateBrowsersByFingerprintArg = async (fingerprintArg) => {
5491
5436
  if (error?.code === 1 || error?.code === "ENOENT") {
5492
5437
  return;
5493
5438
  }
5494
- logger8.info(`\u5F3A\u5236\u5173\u95ED CloakBrowser \u8FDB\u7A0B\u5931\u8D25\uFF08\u5FFD\u7565\uFF09: ${error?.message || String(error)}`);
5439
+ logger8.info(`\u5F3A\u5236\u5173\u95ED Cloak \u8FDB\u7A0B\u5931\u8D25\uFF08\u5FFD\u7565\uFF09: ${error?.message || String(error)}`);
5495
5440
  });
5496
5441
  };
5497
- var CloakBrowserLaunch = {
5442
+ var CloakLaunch = {
5498
5443
  resolveProxyConfiguration(proxyConfiguration = {}) {
5499
- return resolveCloakBrowserProxy(proxyConfiguration);
5444
+ return resolveCloakProxy(proxyConfiguration);
5500
5445
  },
5501
5446
  extractFingerprintArg(launchOptions = {}) {
5502
5447
  return extractFingerprintArg(launchOptions);
@@ -5505,7 +5450,7 @@ var CloakBrowserLaunch = {
5505
5450
  return createStableGotoHook(recommendedGotoOptions);
5506
5451
  },
5507
5452
  async getPlaywrightCrawlerOptions(options = {}) {
5508
- const runtime2 = await CloakBrowserLaunch.createPlaywrightCrawlerRuntime(options);
5453
+ const runtime2 = await CloakLaunch.createPlaywrightCrawlerRuntime(options);
5509
5454
  return Object.defineProperties(runtime2.crawlerOptions, {
5510
5455
  cleanup: {
5511
5456
  enumerable: false,
@@ -5528,6 +5473,8 @@ var CloakBrowserLaunch = {
5528
5473
  const normalizedOptions = normalizeObject(options);
5529
5474
  const {
5530
5475
  proxyConfiguration = {},
5476
+ log: logOptions = null,
5477
+ debugMode = false,
5531
5478
  runInHeadfulMode = false,
5532
5479
  isRunningOnApify = false,
5533
5480
  launcher = null,
@@ -5545,8 +5492,15 @@ var CloakBrowserLaunch = {
5545
5492
  const patchedBrowsers = /* @__PURE__ */ new WeakSet();
5546
5493
  const defaultArgs = isRunningOnApify ? ["--no-sandbox", "--disable-setuid-sandbox"] : [];
5547
5494
  const extraArgs = normalizeStringArray(normalizedCloakOptions.args);
5548
- const proxy = hasOwn(normalizedCloakOptions, "proxy") ? normalizedCloakOptions.proxy : resolveCloakBrowserProxy(proxyConfiguration);
5495
+ const hasExplicitProxy = hasOwn(normalizedCloakOptions, "proxy");
5496
+ const proxyLaunchState = hasExplicitProxy ? {
5497
+ ...resolveProxyLaunchOptions2(proxyConfiguration),
5498
+ byPassRules: [],
5499
+ launchProxy: null
5500
+ } : buildMeteredProxy({ proxyConfiguration, debugMode });
5501
+ const proxy = hasExplicitProxy ? normalizedCloakOptions.proxy : proxyLaunchState.launchProxy;
5549
5502
  const headless = hasOwn(normalizedCloakOptions, "headless") ? normalizedCloakOptions.headless : !runInHeadfulMode || isRunningOnApify;
5503
+ const enableByPassLogger = Boolean(logOptions && logOptions.enable);
5550
5504
  const mergedCloakOptions = {
5551
5505
  ...normalizedCloakOptions,
5552
5506
  headless,
@@ -5556,8 +5510,23 @@ var CloakBrowserLaunch = {
5556
5510
  const launchOptions = await buildCloakLaunchOptions(mergedCloakOptions);
5557
5511
  const fingerprintArg = extractFingerprintArg(launchOptions);
5558
5512
  const internalPreNavigationHook = createStableGotoHook(recommendedGotoOptions);
5513
+ const proxyRequestHook = createProxyRequestHook({
5514
+ byPassDomains: proxyLaunchState.byPassDomains,
5515
+ byPassRules: proxyLaunchState.byPassRules,
5516
+ enableByPassLogger,
5517
+ launchProxy: proxyLaunchState.launchProxy
5518
+ });
5559
5519
  const normalizedPreNavigationHooks = Array.isArray(preNavigationHooks) ? preNavigationHooks : [];
5560
5520
  const normalizedPostNavigationHooks = Array.isArray(postNavigationHooks) ? postNavigationHooks : [];
5521
+ if (hasExplicitProxy && enableByPassLogger) {
5522
+ logger8.info("[\u4EE3\u7406\u5DF2\u542F\u7528] \u4F7F\u7528 cloakOptions.proxy\uFF0C\u8DF3\u8FC7 toolkit \u672C\u5730\u6D41\u91CF\u89C2\u6D4B");
5523
+ } else {
5524
+ logProxyLaunchState({
5525
+ ...proxyLaunchState,
5526
+ debugMode,
5527
+ enableByPassLogger
5528
+ });
5529
+ }
5561
5530
  const crawlerOptions = {
5562
5531
  ...DEFAULT_CLOAK_CRAWLER_BASE_OPTIONS,
5563
5532
  ...normalizeObject(crawlerBaseOptions),
@@ -5568,13 +5537,19 @@ var CloakBrowserLaunch = {
5568
5537
  ...launcher ? { launcher } : {},
5569
5538
  launchOptions
5570
5539
  },
5571
- browserPoolOptions: attachCloakBrowserHumanizeHook({
5540
+ browserPoolOptions: attachCloakHumanizeHook({
5572
5541
  browserPoolOptions,
5573
5542
  activeBrowsers,
5574
5543
  patchedBrowsers,
5575
5544
  humanizeOptions
5576
5545
  }),
5577
- preNavigationHooks: [internalPreNavigationHook, ...normalizedPreNavigationHooks],
5546
+ preNavigationHooks: [
5547
+ async (crawlingContext, gotoOptions = {}) => {
5548
+ proxyRequestHook(crawlingContext?.page);
5549
+ await internalPreNavigationHook(crawlingContext, gotoOptions);
5550
+ },
5551
+ ...normalizedPreNavigationHooks
5552
+ ],
5578
5553
  ...normalizedPostNavigationHooks.length > 0 ? { postNavigationHooks: normalizedPostNavigationHooks } : {}
5579
5554
  };
5580
5555
  const closeActiveBrowsers = async () => {
@@ -5602,12 +5577,9 @@ var CloakBrowserLaunch = {
5602
5577
  // src/launch.js
5603
5578
  var launchStrategies = {
5604
5579
  [Mode.Default]: DefaultLaunch,
5605
- [Mode.CloakBrowser]: CloakBrowserLaunch
5580
+ [Mode.Cloak]: CloakLaunch
5606
5581
  };
5607
- var launchFacadeMethods = Object.freeze({
5608
- getPlaywrightCrawlerOptions: { enumerable: true }
5609
- });
5610
- var Launch = createDelegatedFacade("Launch", launchStrategies, launchFacadeMethods);
5582
+ var Launch = withModeReflect("Launch", launchStrategies);
5611
5583
 
5612
5584
  // src/live-view.js
5613
5585
  var import_express = __toESM(require("express"), 1);
@@ -6696,7 +6668,7 @@ var Mutation = {
6696
6668
  const overallTimeout = options.timeout ?? 180 * 1e3;
6697
6669
  const onMutation = options.onMutation;
6698
6670
  const pollInterval = 500;
6699
- const sleep2 = (ms) => new Promise((resolve) => {
6671
+ const sleep = (ms) => new Promise((resolve) => {
6700
6672
  setTimeout(resolve, ms);
6701
6673
  });
6702
6674
  const truncate = (value, max = 800) => {
@@ -6888,7 +6860,7 @@ var Mutation = {
6888
6860
  const deadline = Date.now() + overallTimeout;
6889
6861
  let lastState = state2;
6890
6862
  while (Date.now() < deadline) {
6891
- await sleep2(pollInterval);
6863
+ await sleep(pollInterval);
6892
6864
  lastState = await buildState();
6893
6865
  if (!lastState?.hasMatched) {
6894
6866
  continue;
@@ -8473,7 +8445,7 @@ var buildWatermarkifyRenderHtml = ({ imageSrc, overlaySvg, width, height, imageH
8473
8445
  `;
8474
8446
  };
8475
8447
  var normalizeWatermarkifyRenderMode = (value) => {
8476
- return String(value || "default").trim().toLowerCase() === "cloakbrowser" ? "cloakbrowser" : "default";
8448
+ return String(value || "default").trim().toLowerCase() === "cloak" ? "cloak" : "default";
8477
8449
  };
8478
8450
  var composeScreenshotBufferWithBrowser = async (page, buffer, overlaySvg, imageInfo = {}, options = {}) => {
8479
8451
  if (!page || typeof page.context !== "function") {
@@ -8500,7 +8472,7 @@ var composeScreenshotBufferWithBrowser = async (page, buffer, overlaySvg, imageI
8500
8472
  }).catch(() => {
8501
8473
  });
8502
8474
  const renderMode = normalizeWatermarkifyRenderMode(options.mode);
8503
- if (renderMode === "cloakbrowser") {
8475
+ if (renderMode === "cloak") {
8504
8476
  const renderHtml = buildWatermarkifyRenderHtml({
8505
8477
  imageSrc: `data:${imageInfo.mimeType || "image/png"};base64,${buffer.toString("base64")}`,
8506
8478
  overlaySvg,
@@ -10081,7 +10053,7 @@ var Share = {
10081
10053
  * @param {number} [options.maxBytes] 默认 5MiB,返回 base64 超过后会压缩
10082
10054
  * @param {'jpeg'|'jpg'} [options.type] 压缩输出格式,默认 jpeg
10083
10055
  * @param {boolean|Object} [options.compression] 传 false 可关闭压缩
10084
- * @param {'default'|'cloakbrowser'} [options.mode] 截图水印合成模式,默认 default
10056
+ * @param {'default'|'cloak'} [options.mode] 截图水印合成模式,默认 default
10085
10057
  * @returns {Promise<string>} base64 image
10086
10058
  */
10087
10059
  async captureScreen(page, options = {}) {
@@ -10103,7 +10075,7 @@ var Share = {
10103
10075
  capturedAt
10104
10076
  });
10105
10077
  outputBuffer = await watermarkifyScreenshotBuffer(rawBuffer, watermarkifyMeta, page, {
10106
- mode: options.mode === "cloakbrowser" ? "cloakbrowser" : "default"
10078
+ mode: options.mode
10107
10079
  });
10108
10080
  }
10109
10081
  return await compressImageBufferToBase64(outputBuffer, compression);