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