@skrillex1224/playwright-toolkit 2.1.286 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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");
3797
- }
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;
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" };
3821
3324
  }
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;
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
+ }
3840
3336
  }
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) => {
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
+ };
3356
+ }
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 });
3435
+ }
3436
+ }
3437
+ await page.mouse.wheel(0, deltaY);
3438
+ didScroll = true;
3439
+ await delay2(this.jitterMs(20 + Math.random() * 40, 0.2));
3440
+ }
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}`);
3922
3487
  }
3488
+ logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
3489
+ return false;
3490
+ }
3491
+ } else {
3492
+ element = target;
3493
+ }
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;
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;
3568
+ try {
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);
3923
3581
  }
3924
- }
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 };
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);
3931
3588
  }
3932
3589
  }
3933
- didScroll = true;
3934
- }
3935
- 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 };
3943
- }
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
3684
  }
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;
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));
3712
+ }
3713
+ logger5.success("naturalScroll");
3714
+ } catch (error) {
3715
+ logger5.fail("naturalScroll", error);
3716
+ throw error;
3717
+ }
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;
3816
+ }
3817
+ }
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);
4017
3829
  }
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}`);
3830
+ if (clipsY) {
3831
+ clipTop = Math.max(clipTop, nodeRect.top);
3832
+ clipBottom = Math.min(clipBottom, nodeRect.bottom);
4037
3833
  }
4038
- await waitJitter(120, 0.35);
4039
- logger5.success("humanClick");
4040
- return true;
4041
- } catch (error) {
4042
- logger5.fail("humanClick", error);
4043
- throw error;
4044
3834
  }
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);
4061
- }
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
+ };
4062
3853
  }
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);
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;
4079
3858
  }
4080
- }
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
- });
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;
4103
3865
  }
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;
4114
- }
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(() => "");
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;
4125
3876
  }
3877
+ return common.contains(el) && common.contains(pointElement);
4126
3878
  };
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;
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 };
4140
3908
  }
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);
3909
+ obstruction = obstruction || describeElement(pointElement);
3910
+ }
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";
3934
+ }
3935
+ return false;
3936
+ };
3937
+ const findEditable = (node) => {
3938
+ for (let current = node; current && current !== document.body; current = current.parentElement) {
3939
+ if (isEditable(current)) return current;
3940
+ }
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 || "" };
4175
3952
  }
4176
- }
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 || "" };
3967
+ }
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
- }
4435
- }
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 };
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");
4439
4288
  }
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
- };
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;
4455
4312
  }
4456
- return { code: "VISIBLE", isFixed };
4457
- });
4458
- };
4459
- const getScrollableRect2 = async () => {
4460
- return await element.evaluate((el) => {
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;
4331
+ }
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");
4538
+ await waitJitter(baseMs, jitterPercent);
4630
4539
  },
4631
- /**
4632
- * 模拟人类"注视"或"阅读"行为:鼠标在页面上随机微动
4633
- * @param {import('playwright').Page} page
4634
- * @param {number} [baseDurationMs=2500] - 基础持续时间 (±40% 抖动)
4635
- */
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,10 +5188,10 @@ 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("Cloak");
5331
5195
  var execFileAsync = promisify(execFile);
5332
5196
  var DEFAULT_CLOAK_CRAWLER_BASE_OPTIONS = Object.freeze({
5333
5197
  maxConcurrency: 1,
@@ -5341,21 +5205,21 @@ var DEFAULT_CLOAK_HUMANIZE_OPTIONS = Object.freeze({
5341
5205
  var DEFAULT_CLOAK_GOTO_OPTIONS = Object.freeze({
5342
5206
  waitUntil: "commit"
5343
5207
  });
5344
- var cachedCloakBrowserModulePromise = null;
5208
+ var cachedCloakModulePromise = null;
5345
5209
  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", {
5210
+ var loadCloakModule = async () => {
5211
+ if (!cachedCloakModulePromise) {
5212
+ cachedCloakModulePromise = import("cloakbrowser").catch((error) => {
5213
+ cachedCloakModulePromise = null;
5214
+ 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
5215
  cause: error
5352
5216
  });
5353
5217
  });
5354
5218
  }
5355
- return cachedCloakBrowserModulePromise;
5219
+ return cachedCloakModulePromise;
5356
5220
  };
5357
5221
  var buildCloakLaunchOptions = async (options = {}) => {
5358
- const { buildLaunchOptions } = await loadCloakBrowserModule();
5222
+ const { buildLaunchOptions } = await loadCloakModule();
5359
5223
  return await buildLaunchOptions(normalizeObject(options));
5360
5224
  };
5361
5225
  var normalizeObject = (value) => {
@@ -5370,7 +5234,7 @@ var normalizeStringArray = (value) => {
5370
5234
  }
5371
5235
  return value.map((item) => String(item || "").trim()).filter(Boolean);
5372
5236
  };
5373
- var resolveCloakBrowserProxy = (proxyConfiguration = {}) => {
5237
+ var resolveCloakProxy = (proxyConfiguration = {}) => {
5374
5238
  const config = normalizeObject(proxyConfiguration);
5375
5239
  const proxyUrl = String(config.proxy_url || "").trim();
5376
5240
  const enableProxy = typeof config.enable_proxy === "boolean" ? config.enable_proxy : proxyUrl !== "";
@@ -5404,7 +5268,7 @@ var createStableGotoHook = (recommendedGotoOptions = DEFAULT_CLOAK_GOTO_OPTIONS)
5404
5268
  }
5405
5269
  };
5406
5270
  };
5407
- var attachCloakBrowserHumanizeHook = ({
5271
+ var attachCloakHumanizeHook = ({
5408
5272
  browserPoolOptions = {},
5409
5273
  activeBrowsers,
5410
5274
  patchedBrowsers,
@@ -5435,7 +5299,7 @@ var attachCloakBrowserHumanizeHook = ({
5435
5299
  if (!shouldHumanize || patchedBrowsers.has(browser)) {
5436
5300
  return;
5437
5301
  }
5438
- const { humanizeBrowser } = await loadCloakBrowserModule();
5302
+ const { humanizeBrowser } = await loadCloakModule();
5439
5303
  await humanizeBrowser(browser, normalizedHumanizeOptions);
5440
5304
  patchedBrowsers.add(browser);
5441
5305
  }
@@ -5463,12 +5327,12 @@ var forceTerminateBrowsersByFingerprintArg = async (fingerprintArg) => {
5463
5327
  if (error?.code === 1 || error?.code === "ENOENT") {
5464
5328
  return;
5465
5329
  }
5466
- logger8.info(`\u5F3A\u5236\u5173\u95ED CloakBrowser \u8FDB\u7A0B\u5931\u8D25\uFF08\u5FFD\u7565\uFF09: ${error?.message || String(error)}`);
5330
+ logger8.info(`\u5F3A\u5236\u5173\u95ED Cloak \u8FDB\u7A0B\u5931\u8D25\uFF08\u5FFD\u7565\uFF09: ${error?.message || String(error)}`);
5467
5331
  });
5468
5332
  };
5469
- var CloakBrowserLaunch = {
5333
+ var CloakLaunch = {
5470
5334
  resolveProxyConfiguration(proxyConfiguration = {}) {
5471
- return resolveCloakBrowserProxy(proxyConfiguration);
5335
+ return resolveCloakProxy(proxyConfiguration);
5472
5336
  },
5473
5337
  extractFingerprintArg(launchOptions = {}) {
5474
5338
  return extractFingerprintArg(launchOptions);
@@ -5477,7 +5341,7 @@ var CloakBrowserLaunch = {
5477
5341
  return createStableGotoHook(recommendedGotoOptions);
5478
5342
  },
5479
5343
  async getPlaywrightCrawlerOptions(options = {}) {
5480
- const runtime2 = await CloakBrowserLaunch.createPlaywrightCrawlerRuntime(options);
5344
+ const runtime2 = await CloakLaunch.createPlaywrightCrawlerRuntime(options);
5481
5345
  return Object.defineProperties(runtime2.crawlerOptions, {
5482
5346
  cleanup: {
5483
5347
  enumerable: false,
@@ -5517,7 +5381,7 @@ var CloakBrowserLaunch = {
5517
5381
  const patchedBrowsers = /* @__PURE__ */ new WeakSet();
5518
5382
  const defaultArgs = isRunningOnApify ? ["--no-sandbox", "--disable-setuid-sandbox"] : [];
5519
5383
  const extraArgs = normalizeStringArray(normalizedCloakOptions.args);
5520
- const proxy = hasOwn(normalizedCloakOptions, "proxy") ? normalizedCloakOptions.proxy : resolveCloakBrowserProxy(proxyConfiguration);
5384
+ const proxy = hasOwn(normalizedCloakOptions, "proxy") ? normalizedCloakOptions.proxy : resolveCloakProxy(proxyConfiguration);
5521
5385
  const headless = hasOwn(normalizedCloakOptions, "headless") ? normalizedCloakOptions.headless : !runInHeadfulMode || isRunningOnApify;
5522
5386
  const mergedCloakOptions = {
5523
5387
  ...normalizedCloakOptions,
@@ -5540,7 +5404,7 @@ var CloakBrowserLaunch = {
5540
5404
  ...launcher ? { launcher } : {},
5541
5405
  launchOptions
5542
5406
  },
5543
- browserPoolOptions: attachCloakBrowserHumanizeHook({
5407
+ browserPoolOptions: attachCloakHumanizeHook({
5544
5408
  browserPoolOptions,
5545
5409
  activeBrowsers,
5546
5410
  patchedBrowsers,
@@ -5574,12 +5438,9 @@ var CloakBrowserLaunch = {
5574
5438
  // src/launch.js
5575
5439
  var launchStrategies = {
5576
5440
  [Mode.Default]: DefaultLaunch,
5577
- [Mode.CloakBrowser]: CloakBrowserLaunch
5441
+ [Mode.Cloak]: CloakLaunch
5578
5442
  };
5579
- var launchFacadeMethods = Object.freeze({
5580
- getPlaywrightCrawlerOptions: { enumerable: true }
5581
- });
5582
- var Launch = createDelegatedFacade("Launch", launchStrategies, launchFacadeMethods);
5443
+ var Launch = withModeReflect("Launch", launchStrategies);
5583
5444
 
5584
5445
  // src/live-view.js
5585
5446
  import express from "express";
@@ -6668,7 +6529,7 @@ var Mutation = {
6668
6529
  const overallTimeout = options.timeout ?? 180 * 1e3;
6669
6530
  const onMutation = options.onMutation;
6670
6531
  const pollInterval = 500;
6671
- const sleep2 = (ms) => new Promise((resolve) => {
6532
+ const sleep = (ms) => new Promise((resolve) => {
6672
6533
  setTimeout(resolve, ms);
6673
6534
  });
6674
6535
  const truncate = (value, max = 800) => {
@@ -6860,7 +6721,7 @@ var Mutation = {
6860
6721
  const deadline = Date.now() + overallTimeout;
6861
6722
  let lastState = state2;
6862
6723
  while (Date.now() < deadline) {
6863
- await sleep2(pollInterval);
6724
+ await sleep(pollInterval);
6864
6725
  lastState = await buildState();
6865
6726
  if (!lastState?.hasMatched) {
6866
6727
  continue;
@@ -8445,7 +8306,7 @@ var buildWatermarkifyRenderHtml = ({ imageSrc, overlaySvg, width, height, imageH
8445
8306
  `;
8446
8307
  };
8447
8308
  var normalizeWatermarkifyRenderMode = (value) => {
8448
- return String(value || "default").trim().toLowerCase() === "cloakbrowser" ? "cloakbrowser" : "default";
8309
+ return String(value || "default").trim().toLowerCase() === "cloak" ? "cloak" : "default";
8449
8310
  };
8450
8311
  var composeScreenshotBufferWithBrowser = async (page, buffer, overlaySvg, imageInfo = {}, options = {}) => {
8451
8312
  if (!page || typeof page.context !== "function") {
@@ -8472,7 +8333,7 @@ var composeScreenshotBufferWithBrowser = async (page, buffer, overlaySvg, imageI
8472
8333
  }).catch(() => {
8473
8334
  });
8474
8335
  const renderMode = normalizeWatermarkifyRenderMode(options.mode);
8475
- if (renderMode === "cloakbrowser") {
8336
+ if (renderMode === "cloak") {
8476
8337
  const renderHtml = buildWatermarkifyRenderHtml({
8477
8338
  imageSrc: `data:${imageInfo.mimeType || "image/png"};base64,${buffer.toString("base64")}`,
8478
8339
  overlaySvg,
@@ -10053,7 +9914,7 @@ var Share = {
10053
9914
  * @param {number} [options.maxBytes] 默认 5MiB,返回 base64 超过后会压缩
10054
9915
  * @param {'jpeg'|'jpg'} [options.type] 压缩输出格式,默认 jpeg
10055
9916
  * @param {boolean|Object} [options.compression] 传 false 可关闭压缩
10056
- * @param {'default'|'cloakbrowser'} [options.mode] 截图水印合成模式,默认 default
9917
+ * @param {'default'|'cloak'} [options.mode] 截图水印合成模式,默认 default
10057
9918
  * @returns {Promise<string>} base64 image
10058
9919
  */
10059
9920
  async captureScreen(page, options = {}) {
@@ -10075,7 +9936,7 @@ var Share = {
10075
9936
  capturedAt
10076
9937
  });
10077
9938
  outputBuffer = await watermarkifyScreenshotBuffer(rawBuffer, watermarkifyMeta, page, {
10078
- mode: options.mode === "cloakbrowser" ? "cloakbrowser" : "default"
9939
+ mode: options.mode
10079
9940
  });
10080
9941
  }
10081
9942
  return await compressImageBufferToBase64(outputBuffer, compression);