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