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