@skrillex1224/playwright-toolkit 2.1.285 → 2.1.286
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1528 -1502
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +1528 -1502
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2489,28 +2489,69 @@ var resolveModeStrategy = (strategies = {}, mode = getToolkitMode(), fallbackMod
|
|
|
2489
2489
|
};
|
|
2490
2490
|
|
|
2491
2491
|
// src/internals/delegate.js
|
|
2492
|
-
var
|
|
2492
|
+
var normalizeArrayMethodDefinition = (definition) => typeof definition === "string" ? { name: definition, enumerable: true } : {
|
|
2493
2493
|
name: String(definition?.name || "").trim(),
|
|
2494
2494
|
enumerable: definition?.enumerable !== false
|
|
2495
2495
|
};
|
|
2496
|
-
var
|
|
2496
|
+
var normalizeMethodDefinitions = (methods = {}) => {
|
|
2497
|
+
if (Array.isArray(methods)) {
|
|
2498
|
+
return methods.map(normalizeArrayMethodDefinition).filter((method) => method.name);
|
|
2499
|
+
}
|
|
2500
|
+
if (!methods || typeof methods !== "object") {
|
|
2501
|
+
return [];
|
|
2502
|
+
}
|
|
2503
|
+
return Object.entries(methods).map(([name, definition]) => ({
|
|
2504
|
+
name: String(name || "").trim(),
|
|
2505
|
+
enumerable: definition?.enumerable !== false
|
|
2506
|
+
})).filter((method) => method.name);
|
|
2507
|
+
};
|
|
2508
|
+
var normalizeDelegateResolution = (resolution) => {
|
|
2509
|
+
if (resolution && typeof resolution === "object" && ("delegate" in resolution || "label" in resolution)) {
|
|
2510
|
+
return {
|
|
2511
|
+
delegate: resolution.delegate ?? null,
|
|
2512
|
+
label: String(resolution.label || "current delegate").trim() || "current delegate"
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
return {
|
|
2516
|
+
delegate: resolution,
|
|
2517
|
+
label: "current delegate"
|
|
2518
|
+
};
|
|
2519
|
+
};
|
|
2520
|
+
var createMethodDescriptor = (namespace, methodName, enumerable, resolveDelegate) => ({
|
|
2497
2521
|
enumerable,
|
|
2498
2522
|
value: (...args) => {
|
|
2499
|
-
const {
|
|
2523
|
+
const { delegate, label } = normalizeDelegateResolution(resolveDelegate(methodName, args));
|
|
2500
2524
|
if (typeof delegate?.[methodName] !== "function") {
|
|
2501
|
-
throw new Error(`${namespace}.${methodName} is not available in ${
|
|
2525
|
+
throw new Error(`${namespace}.${methodName} is not available in ${label}`);
|
|
2502
2526
|
}
|
|
2503
2527
|
return delegate[methodName](...args);
|
|
2504
2528
|
}
|
|
2505
2529
|
});
|
|
2506
|
-
var
|
|
2530
|
+
var withDelegatedProperties = (target = {}, {
|
|
2531
|
+
namespace = "Delegate",
|
|
2532
|
+
methods = {},
|
|
2533
|
+
resolveDelegate = () => null
|
|
2534
|
+
} = {}) => {
|
|
2507
2535
|
const descriptors = Object.fromEntries(
|
|
2508
|
-
methods
|
|
2536
|
+
normalizeMethodDefinitions(methods).map((method) => [
|
|
2509
2537
|
method.name,
|
|
2510
|
-
createMethodDescriptor(namespace, method.name, method.enumerable,
|
|
2538
|
+
createMethodDescriptor(namespace, method.name, method.enumerable, resolveDelegate)
|
|
2511
2539
|
])
|
|
2512
2540
|
);
|
|
2513
|
-
return Object.defineProperties(
|
|
2541
|
+
return Object.defineProperties(target, descriptors);
|
|
2542
|
+
};
|
|
2543
|
+
var createDelegatedFacade = (namespace, strategies = {}, methods = {}) => {
|
|
2544
|
+
return withDelegatedProperties({}, {
|
|
2545
|
+
namespace,
|
|
2546
|
+
methods,
|
|
2547
|
+
resolveDelegate: () => {
|
|
2548
|
+
const { mode, delegate } = resolveModeStrategy(strategies);
|
|
2549
|
+
return {
|
|
2550
|
+
delegate,
|
|
2551
|
+
label: `${mode} mode`
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
});
|
|
2514
2555
|
};
|
|
2515
2556
|
|
|
2516
2557
|
// src/internals/anti-cheat/default.js
|
|
@@ -2624,13 +2665,14 @@ var antiCheatStrategies = {
|
|
|
2624
2665
|
[Mode.Default]: DefaultAntiCheat,
|
|
2625
2666
|
[Mode.CloakBrowser]: CloakBrowserAntiCheat
|
|
2626
2667
|
};
|
|
2627
|
-
var
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2668
|
+
var antiCheatFacadeMethods = Object.freeze({
|
|
2669
|
+
getBaseConfig: { enumerable: true },
|
|
2670
|
+
getFingerprintGeneratorOptions: { enumerable: true },
|
|
2671
|
+
getLaunchArgs: { enumerable: true },
|
|
2672
|
+
getTlsFingerprintOptions: { enumerable: true },
|
|
2673
|
+
applyLocaleHeaders: { enumerable: true }
|
|
2674
|
+
});
|
|
2675
|
+
var AntiCheat = createDelegatedFacade("AntiCheat", antiCheatStrategies, antiCheatFacadeMethods);
|
|
2634
2676
|
|
|
2635
2677
|
// src/device-input.js
|
|
2636
2678
|
var resolveDeviceFromPage = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
|
|
@@ -3182,1136 +3224,1240 @@ var DeviceView = {
|
|
|
3182
3224
|
}
|
|
3183
3225
|
};
|
|
3184
3226
|
|
|
3185
|
-
// src/internals/humanize/
|
|
3227
|
+
// src/internals/humanize/cloakbrowser.js
|
|
3228
|
+
import delay3 from "delay";
|
|
3229
|
+
|
|
3230
|
+
// src/internals/humanize/shared.js
|
|
3186
3231
|
import delay2 from "delay";
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
if (
|
|
3193
|
-
|
|
3232
|
+
var jitterMs = (base, jitterPercent = 0.3) => {
|
|
3233
|
+
const jitter = Number(base || 0) * Number(jitterPercent || 0) * (Math.random() * 2 - 1);
|
|
3234
|
+
return Math.max(10, Math.round(Number(base || 0) + jitter));
|
|
3235
|
+
};
|
|
3236
|
+
var resolveElement = async (page, target, { throwOnMissing = true } = {}) => {
|
|
3237
|
+
if (target == null) return null;
|
|
3238
|
+
let element = target;
|
|
3239
|
+
if (typeof target === "string") {
|
|
3240
|
+
element = await page.$(target);
|
|
3194
3241
|
}
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
/**
|
|
3199
|
-
* 生成带抖动的毫秒数 - 基于基础值添加随机浮动 (±30% 默认)
|
|
3200
|
-
* @param {number} base - 基础延迟 (ms)
|
|
3201
|
-
* @param {number} [jitterPercent=0.3] - 抖动百分比 (0.3 = ±30%)
|
|
3202
|
-
* @returns {number} 抖动后的毫秒数
|
|
3203
|
-
*/
|
|
3204
|
-
jitterMs(base, jitterPercent = 0.3) {
|
|
3205
|
-
const jitter = base * jitterPercent * (Math.random() * 2 - 1);
|
|
3206
|
-
return Math.max(10, Math.round(base + jitter));
|
|
3207
|
-
},
|
|
3208
|
-
/**
|
|
3209
|
-
* 初始化页面的 Ghost Cursor(必须在使用其他 cursor 相关方法前调用)
|
|
3210
|
-
*
|
|
3211
|
-
* @param {import('playwright').Page} page
|
|
3212
|
-
* @returns {Promise<void>}
|
|
3213
|
-
*/
|
|
3214
|
-
async initializeCursor(page) {
|
|
3215
|
-
if ($CursorWeakMap.has(page)) {
|
|
3216
|
-
logger5.debug("initializeCursor: cursor already exists, skipping");
|
|
3217
|
-
return;
|
|
3242
|
+
if (!element) {
|
|
3243
|
+
if (throwOnMissing) {
|
|
3244
|
+
throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${String(target)}`);
|
|
3218
3245
|
}
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
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);
|
|
3298
|
+
}
|
|
3299
|
+
};
|
|
3300
|
+
var checkElementVisibility = async (element) => {
|
|
3301
|
+
return element.evaluate((el) => {
|
|
3302
|
+
const targetStyle = window.getComputedStyle(el);
|
|
3303
|
+
if (!targetStyle || targetStyle.display === "none" || targetStyle.visibility === "hidden" || targetStyle.visibility === "collapse") {
|
|
3304
|
+
return { code: "NOT_INTERACTABLE", reason: "\u5143\u7D20\u4E0D\u53EF\u89C1", direction: "down" };
|
|
3305
|
+
}
|
|
3306
|
+
const rect = el.getBoundingClientRect();
|
|
3307
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
3308
|
+
return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6", direction: "down" };
|
|
3309
|
+
}
|
|
3310
|
+
const viewW = window.innerWidth;
|
|
3311
|
+
const viewH = window.innerHeight;
|
|
3312
|
+
const centerY = rect.top + rect.height / 2;
|
|
3313
|
+
let clipLeft = 0;
|
|
3314
|
+
let clipRight = viewW;
|
|
3315
|
+
let clipTop = 0;
|
|
3316
|
+
let clipBottom = viewH;
|
|
3317
|
+
let isFixed = false;
|
|
3318
|
+
let positioning = null;
|
|
3319
|
+
for (let node = el; node && node !== document.body; node = node.parentElement) {
|
|
3320
|
+
const style = window.getComputedStyle(node);
|
|
3321
|
+
if (style && (style.position === "fixed" || style.position === "sticky")) {
|
|
3322
|
+
isFixed = true;
|
|
3323
|
+
positioning = style.position;
|
|
3324
|
+
break;
|
|
3257
3325
|
}
|
|
3258
|
-
logger5.success("humanMove");
|
|
3259
|
-
return true;
|
|
3260
|
-
} catch (error) {
|
|
3261
|
-
logger5.fail("humanMove", error);
|
|
3262
|
-
throw error;
|
|
3263
3326
|
}
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
maxSteps = 20,
|
|
3280
|
-
minStep = 260,
|
|
3281
|
-
maxStep = 800,
|
|
3282
|
-
maxDurationMs = maxSteps * 220 + 800
|
|
3283
|
-
} = options;
|
|
3284
|
-
const targetDesc = typeof target === "string" ? target : "ElementHandle";
|
|
3285
|
-
logger5.start("humanScroll", `target=${targetDesc}`);
|
|
3286
|
-
let element;
|
|
3287
|
-
if (typeof target === "string") {
|
|
3288
|
-
element = await page.$(target);
|
|
3289
|
-
if (!element) {
|
|
3290
|
-
logger5.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
|
|
3291
|
-
return { element: null, didScroll: false };
|
|
3327
|
+
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
3328
|
+
const style = window.getComputedStyle(node);
|
|
3329
|
+
if (!style) continue;
|
|
3330
|
+
const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
|
|
3331
|
+
const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
|
|
3332
|
+
if (!clipsX && !clipsY) continue;
|
|
3333
|
+
const nodeRect = node.getBoundingClientRect();
|
|
3334
|
+
if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
|
|
3335
|
+
if (clipsX) {
|
|
3336
|
+
clipLeft = Math.max(clipLeft, nodeRect.left);
|
|
3337
|
+
clipRight = Math.min(clipRight, nodeRect.right);
|
|
3338
|
+
}
|
|
3339
|
+
if (clipsY) {
|
|
3340
|
+
clipTop = Math.max(clipTop, nodeRect.top);
|
|
3341
|
+
clipBottom = Math.min(clipBottom, nodeRect.bottom);
|
|
3292
3342
|
}
|
|
3293
|
-
} else {
|
|
3294
|
-
element = target;
|
|
3295
3343
|
}
|
|
3296
|
-
const
|
|
3297
|
-
|
|
3298
|
-
const
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
|
|
3317
|
-
const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
|
|
3318
|
-
return { code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy, viewH, isFixed };
|
|
3319
|
-
}
|
|
3320
|
-
const pointElement = document.elementFromPoint(cx, cy);
|
|
3321
|
-
if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
|
|
3322
|
-
return {
|
|
3323
|
-
code: "OBSTRUCTED",
|
|
3324
|
-
reason: "\u88AB\u906E\u6321",
|
|
3325
|
-
obstruction: {
|
|
3326
|
-
tag: pointElement.tagName,
|
|
3327
|
-
id: pointElement.id,
|
|
3328
|
-
className: pointElement.className
|
|
3329
|
-
},
|
|
3330
|
-
cy,
|
|
3331
|
-
// Return Center Y for smart direction calculation
|
|
3332
|
-
viewH,
|
|
3333
|
-
isFixed
|
|
3334
|
-
};
|
|
3335
|
-
}
|
|
3336
|
-
return { code: "VISIBLE", isFixed };
|
|
3337
|
-
});
|
|
3338
|
-
};
|
|
3339
|
-
const getScrollableRect2 = async () => {
|
|
3340
|
-
return await element.evaluate((el) => {
|
|
3341
|
-
const isScrollable = (node) => {
|
|
3342
|
-
const style = window.getComputedStyle(node);
|
|
3343
|
-
if (!style) return false;
|
|
3344
|
-
const overflowY = style.overflowY;
|
|
3345
|
-
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3346
|
-
return node.scrollHeight > node.clientHeight + 1;
|
|
3347
|
-
};
|
|
3348
|
-
let current = el;
|
|
3349
|
-
while (current && current !== document.body) {
|
|
3350
|
-
if (isScrollable(current)) {
|
|
3351
|
-
const rect = current.getBoundingClientRect();
|
|
3352
|
-
if (rect && rect.width > 0 && rect.height > 0) {
|
|
3353
|
-
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
3354
|
-
}
|
|
3355
|
-
}
|
|
3356
|
-
current = current.parentElement;
|
|
3357
|
-
}
|
|
3358
|
-
return null;
|
|
3359
|
-
});
|
|
3360
|
-
};
|
|
3361
|
-
const startTime = Date.now();
|
|
3362
|
-
try {
|
|
3363
|
-
for (let i = 0; i < maxSteps; i++) {
|
|
3364
|
-
if (Date.now() - startTime > maxDurationMs) {
|
|
3365
|
-
logger5.warn(`humanScroll | \u8D85\u65F6\u4FDD\u62A4\u89E6\u53D1 (${maxDurationMs}ms)`);
|
|
3366
|
-
return { element, didScroll };
|
|
3367
|
-
}
|
|
3368
|
-
const status = await checkVisibility();
|
|
3369
|
-
if (status.code === "VISIBLE") {
|
|
3370
|
-
if (status.isFixed) {
|
|
3371
|
-
logger5.info("humanScroll | fixed \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
3372
|
-
} else {
|
|
3373
|
-
logger5.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
|
|
3374
|
-
}
|
|
3375
|
-
logger5.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
3376
|
-
return { element, didScroll };
|
|
3377
|
-
}
|
|
3378
|
-
logger5.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${status.direction ? `(${status.direction})` : ""}`);
|
|
3379
|
-
if (status.code === "OBSTRUCTED" && status.obstruction) {
|
|
3380
|
-
logger5.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
|
|
3381
|
-
}
|
|
3382
|
-
const scrollRect = await getScrollableRect2();
|
|
3383
|
-
if (!scrollRect && status.isFixed) {
|
|
3384
|
-
logger5.warn("humanScroll | fixed \u5BB9\u5668\u5185\u4E14\u65E0\u53EF\u6EDA\u52A8\u7956\u5148\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
3385
|
-
return { element, didScroll };
|
|
3386
|
-
}
|
|
3387
|
-
const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
|
|
3388
|
-
const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
|
|
3389
|
-
let deltaY = 0;
|
|
3390
|
-
if (status.code === "OUT_OF_VIEWPORT") {
|
|
3391
|
-
if (status.direction === "down") {
|
|
3392
|
-
deltaY = stepMin + Math.random() * (stepMax - stepMin);
|
|
3393
|
-
} else if (status.direction === "up") {
|
|
3394
|
-
deltaY = -(stepMin + Math.random() * (stepMax - stepMin));
|
|
3395
|
-
} else {
|
|
3396
|
-
deltaY = 100;
|
|
3397
|
-
}
|
|
3398
|
-
} else if (status.code === "OBSTRUCTED") {
|
|
3399
|
-
const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
3400
|
-
const isBottomHalf = status.cy > halfY;
|
|
3401
|
-
const direction = isBottomHalf ? 1 : -1;
|
|
3402
|
-
deltaY = direction * (stepMin + Math.random() * 50);
|
|
3403
|
-
}
|
|
3404
|
-
if (i === 0) {
|
|
3405
|
-
const viewSize = page.viewportSize();
|
|
3406
|
-
if (scrollRect) {
|
|
3407
|
-
const safeX = scrollRect.x + scrollRect.width * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.width * 0.4);
|
|
3408
|
-
const safeY = scrollRect.y + scrollRect.height * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.height * 0.4);
|
|
3409
|
-
await cursor.actions.move({ x: safeX, y: safeY });
|
|
3410
|
-
} else if (viewSize) {
|
|
3411
|
-
const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 80;
|
|
3412
|
-
const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 80;
|
|
3413
|
-
await cursor.actions.move({ x: safeX, y: safeY });
|
|
3414
|
-
}
|
|
3415
|
-
}
|
|
3416
|
-
await page.mouse.wheel(0, deltaY);
|
|
3417
|
-
didScroll = true;
|
|
3418
|
-
await delay2(this.jitterMs(20 + Math.random() * 40, 0.2));
|
|
3419
|
-
}
|
|
3420
|
-
logger5.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
|
|
3421
|
-
return { element, didScroll };
|
|
3422
|
-
} catch (error) {
|
|
3423
|
-
logger5.fail("humanScroll", error);
|
|
3424
|
-
throw error;
|
|
3344
|
+
const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
|
|
3345
|
+
const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
|
|
3346
|
+
const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
|
|
3347
|
+
const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
|
|
3348
|
+
const visibleWidth = visibleRight - visibleLeft;
|
|
3349
|
+
const visibleHeight = visibleBottom - visibleTop;
|
|
3350
|
+
const cx = visibleLeft + visibleWidth / 2;
|
|
3351
|
+
const cy = visibleTop + visibleHeight / 2;
|
|
3352
|
+
if (visibleWidth <= 1 || visibleHeight <= 1) {
|
|
3353
|
+
return {
|
|
3354
|
+
code: "OUT_OF_VIEWPORT",
|
|
3355
|
+
reason: "\u4E0D\u5728\u89C6\u53E3\u5185",
|
|
3356
|
+
direction: centerY < clipTop ? "up" : "down",
|
|
3357
|
+
cy: centerY,
|
|
3358
|
+
viewH,
|
|
3359
|
+
isFixed,
|
|
3360
|
+
positioning
|
|
3361
|
+
};
|
|
3425
3362
|
}
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
* @param {import('playwright').Page} page
|
|
3431
|
-
* @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
|
|
3432
|
-
* @param {Object} [options]
|
|
3433
|
-
* @param {number} [options.reactionDelay=250] - 反应延迟基础值 (ms),实际 ±30% 抖动
|
|
3434
|
-
* @param {boolean} [options.throwOnMissing=true] - 元素不存在时是否抛出错误
|
|
3435
|
-
* @param {boolean} [options.scrollIfNeeded=true] - 元素不在视口时是否自动滚动
|
|
3436
|
-
*/
|
|
3437
|
-
async humanClick(page, target, options = {}) {
|
|
3438
|
-
const cursor = $GetCursor(page);
|
|
3439
|
-
const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
|
|
3440
|
-
const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
|
|
3441
|
-
logger5.start("humanClick", `target=${targetDesc}`);
|
|
3442
|
-
const restoreOnce = async () => {
|
|
3443
|
-
if (restoreOnce.restored) return;
|
|
3444
|
-
restoreOnce.restored = true;
|
|
3445
|
-
if (typeof restoreOnce.do !== "function") return;
|
|
3446
|
-
try {
|
|
3447
|
-
await delay2(this.jitterMs(1e3));
|
|
3448
|
-
await restoreOnce.do();
|
|
3449
|
-
} catch (restoreError) {
|
|
3450
|
-
logger5.warn(`humanClick: \u6062\u590D\u6EDA\u52A8\u4F4D\u7F6E\u5931\u8D25: ${restoreError.message}`);
|
|
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;
|
|
3451
3367
|
}
|
|
3368
|
+
return null;
|
|
3452
3369
|
};
|
|
3453
|
-
|
|
3454
|
-
if (
|
|
3455
|
-
|
|
3456
|
-
await cursor.actions.click();
|
|
3457
|
-
logger5.success("humanClick", "Clicked current position");
|
|
3370
|
+
const sameTapTarget = (pointElement) => {
|
|
3371
|
+
if (!pointElement) return false;
|
|
3372
|
+
if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
|
|
3458
3373
|
return true;
|
|
3459
3374
|
}
|
|
3460
|
-
|
|
3461
|
-
if (
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
}
|
|
3470
|
-
} else {
|
|
3471
|
-
element = target;
|
|
3472
|
-
}
|
|
3473
|
-
if (scrollIfNeeded) {
|
|
3474
|
-
const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
|
|
3475
|
-
restoreOnce.do = didScroll && restore ? restoreFn : null;
|
|
3476
|
-
}
|
|
3477
|
-
const box = await element.boundingBox();
|
|
3478
|
-
if (!box) {
|
|
3479
|
-
await restoreOnce();
|
|
3480
|
-
if (throwOnMissing) {
|
|
3481
|
-
throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
3482
|
-
}
|
|
3483
|
-
logger5.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
|
|
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)) {
|
|
3484
3384
|
return false;
|
|
3485
3385
|
}
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
return
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
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);
|
|
3498
3419
|
}
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
},
|
|
3511
|
-
/**
|
|
3512
|
-
* 模拟人类"注视"或"阅读"行为:鼠标在页面上随机微动
|
|
3513
|
-
* @param {import('playwright').Page} page
|
|
3514
|
-
* @param {number} [baseDurationMs=2500] - 基础持续时间 (±40% 抖动)
|
|
3515
|
-
*/
|
|
3516
|
-
async simulateGaze(page, baseDurationMs = 2500) {
|
|
3517
|
-
const cursor = $GetCursor(page);
|
|
3518
|
-
const durationMs = this.jitterMs(baseDurationMs, 0.4);
|
|
3519
|
-
logger5.start("simulateGaze", `duration=${durationMs}ms`);
|
|
3520
|
-
const startTime = Date.now();
|
|
3521
|
-
const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
|
|
3522
|
-
while (Date.now() - startTime < durationMs) {
|
|
3523
|
-
const x = 100 + Math.random() * (viewportSize.width - 200);
|
|
3524
|
-
const y = 100 + Math.random() * (viewportSize.height - 200);
|
|
3525
|
-
await cursor.actions.move({ x, y });
|
|
3526
|
-
await delay2(this.jitterMs(600, 0.5));
|
|
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
|
+
};
|
|
3527
3431
|
}
|
|
3528
|
-
|
|
3529
|
-
}
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
const {
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
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
|
+
};
|
|
3567
3506
|
}
|
|
3568
3507
|
}
|
|
3569
|
-
|
|
3570
|
-
} catch (error) {
|
|
3571
|
-
logger5.fail("humanType", error);
|
|
3572
|
-
throw error;
|
|
3508
|
+
current = current.parentElement;
|
|
3573
3509
|
}
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
try {
|
|
3597
|
-
if (hasTarget) {
|
|
3598
|
-
await this.humanClick(page, targetOrKey, { reactionDelay: focusDelay, scrollIfNeeded, throwOnMissing });
|
|
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
|
+
};
|
|
3599
3532
|
}
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
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 }]
|
|
3604
3696
|
});
|
|
3605
|
-
logger5.success("humanPress");
|
|
3606
|
-
return true;
|
|
3607
|
-
} catch (error) {
|
|
3608
|
-
logger5.fail("humanPress", error);
|
|
3609
|
-
throw error;
|
|
3610
3697
|
}
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
const locator = page.locator(selector);
|
|
3621
|
-
await locator.click();
|
|
3622
|
-
await delay2(this.jitterMs(200, 0.4));
|
|
3623
|
-
const currentValue = await locator.inputValue();
|
|
3624
|
-
if (!currentValue || currentValue.length === 0) {
|
|
3625
|
-
logger5.success("humanClear", "already empty");
|
|
3626
|
-
return;
|
|
3627
|
-
}
|
|
3628
|
-
await page.keyboard.press("Meta+A");
|
|
3629
|
-
await delay2(this.jitterMs(100, 0.4));
|
|
3630
|
-
await page.keyboard.press("Backspace");
|
|
3631
|
-
logger5.success("humanClear");
|
|
3632
|
-
} catch (error) {
|
|
3633
|
-
logger5.fail("humanClear", error);
|
|
3634
|
-
throw error;
|
|
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);
|
|
3635
3707
|
}
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
* @param {import('playwright').Page} page
|
|
3640
|
-
* @param {number} [baseDuration=3500] - 预热时长基础值 (±40% 抖动)
|
|
3641
|
-
*/
|
|
3642
|
-
async warmUpBrowsing(page, baseDuration = 3500) {
|
|
3643
|
-
const cursor = $GetCursor(page);
|
|
3644
|
-
const durationMs = this.jitterMs(baseDuration, 0.4);
|
|
3645
|
-
logger5.start("warmUpBrowsing", `duration=${durationMs}ms`);
|
|
3646
|
-
const startTime = Date.now();
|
|
3647
|
-
const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
|
|
3708
|
+
return true;
|
|
3709
|
+
} catch (error) {
|
|
3710
|
+
logger5.debug(`touch swipe fallback: ${error?.message || error}`);
|
|
3648
3711
|
try {
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
} else if (action < 0.7) {
|
|
3657
|
-
const scrollY = (Math.random() - 0.5) * 200;
|
|
3658
|
-
await page.mouse.wheel(0, scrollY);
|
|
3659
|
-
await delay2(this.jitterMs(500, 0.4));
|
|
3660
|
-
} else {
|
|
3661
|
-
await delay2(this.jitterMs(800, 0.5));
|
|
3662
|
-
}
|
|
3663
|
-
}
|
|
3664
|
-
logger5.success("warmUpBrowsing");
|
|
3665
|
-
} catch (error) {
|
|
3666
|
-
logger5.fail("warmUpBrowsing", error);
|
|
3667
|
-
throw error;
|
|
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;
|
|
3668
3719
|
}
|
|
3669
|
-
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
* @param {number} [distance=300] - 总滚动距离基础值 (px),±15% 抖动
|
|
3675
|
-
* @param {number} [baseSteps=5] - 分几步完成基础值,±1 随机
|
|
3676
|
-
*/
|
|
3677
|
-
async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
|
|
3678
|
-
const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
|
|
3679
|
-
const actualDistance = this.jitterMs(distance, 0.15);
|
|
3680
|
-
logger5.start("naturalScroll", `dir=${direction}, dist=${actualDistance}, steps=${steps}`);
|
|
3681
|
-
const sign = direction === "down" ? 1 : -1;
|
|
3682
|
-
const stepDistance = actualDistance / steps;
|
|
3683
|
-
try {
|
|
3684
|
-
for (let i = 0; i < steps; i++) {
|
|
3685
|
-
const factor = 1 - i / steps * 0.5;
|
|
3686
|
-
const jitter = 0.9 + Math.random() * 0.2;
|
|
3687
|
-
const scrollAmount = stepDistance * factor * sign * jitter;
|
|
3688
|
-
await page.mouse.wheel(0, scrollAmount);
|
|
3689
|
-
const baseDelay = 60 + i * 25;
|
|
3690
|
-
await delay2(this.jitterMs(baseDelay, 0.3));
|
|
3720
|
+
} finally {
|
|
3721
|
+
if (client && typeof client.detach === "function") {
|
|
3722
|
+
try {
|
|
3723
|
+
await client.detach();
|
|
3724
|
+
} catch {
|
|
3691
3725
|
}
|
|
3692
|
-
logger5.success("naturalScroll");
|
|
3693
|
-
} catch (error) {
|
|
3694
|
-
logger5.fail("naturalScroll", error);
|
|
3695
|
-
throw error;
|
|
3696
3726
|
}
|
|
3697
3727
|
}
|
|
3698
3728
|
};
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
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;
|
|
3715
3746
|
}
|
|
3716
|
-
return null;
|
|
3717
|
-
}
|
|
3718
|
-
return element;
|
|
3719
|
-
};
|
|
3720
|
-
var waitJitter = (base, jitterPercent = 0.3) => delay3(jitterMs(base, jitterPercent));
|
|
3721
|
-
|
|
3722
|
-
// src/internals/humanize/mobile.js
|
|
3723
|
-
var logger6 = createInternalLogger("Humanize.Mobile");
|
|
3724
|
-
var initializedPages = /* @__PURE__ */ new WeakSet();
|
|
3725
|
-
var DEFAULT_TAP_TIMEOUT_MS = 2500;
|
|
3726
|
-
var DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS = 1200;
|
|
3727
|
-
var DEFAULT_ACTIVATE_FALLBACK_TIMEOUT_MS = 900;
|
|
3728
|
-
var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
3729
|
-
var resolveViewport = (page) => page?.viewportSize?.() || { width: 390, height: 844 };
|
|
3730
|
-
var describeTarget = (target) => {
|
|
3731
|
-
if (target == null) return "Current Position";
|
|
3732
|
-
return typeof target === "string" ? target : "ElementHandle";
|
|
3733
|
-
};
|
|
3734
|
-
var clipBoxToViewport = (box, viewport) => {
|
|
3735
|
-
if (!box || !viewport) return box;
|
|
3736
|
-
const left = clamp(box.x, 0, viewport.width);
|
|
3737
|
-
const top = clamp(box.y, 0, viewport.height);
|
|
3738
|
-
const right = clamp(box.x + box.width, 0, viewport.width);
|
|
3739
|
-
const bottom = clamp(box.y + box.height, 0, viewport.height);
|
|
3740
|
-
if (right - left > 1 && bottom - top > 1) {
|
|
3741
|
-
return {
|
|
3742
|
-
x: left,
|
|
3743
|
-
y: top,
|
|
3744
|
-
width: right - left,
|
|
3745
|
-
height: bottom - top
|
|
3746
|
-
};
|
|
3747
|
-
}
|
|
3748
|
-
return box;
|
|
3749
|
-
};
|
|
3750
|
-
var centerPointInBox = (box) => ({
|
|
3751
|
-
x: box.x + box.width / 2,
|
|
3752
|
-
y: box.y + box.height / 2
|
|
3753
|
-
});
|
|
3754
|
-
var withTimeout = async (operation, timeoutMs, label) => {
|
|
3755
|
-
const safeTimeoutMs = Math.max(50, Number(timeoutMs || 0));
|
|
3756
|
-
let timeoutId = null;
|
|
3757
|
-
try {
|
|
3758
|
-
return await Promise.race([
|
|
3759
|
-
Promise.resolve().then(operation),
|
|
3760
|
-
new Promise((_, reject) => {
|
|
3761
|
-
timeoutId = setTimeout(() => {
|
|
3762
|
-
reject(new Error(`${label} timeout after ${safeTimeoutMs}ms`));
|
|
3763
|
-
}, safeTimeoutMs);
|
|
3764
|
-
})
|
|
3765
|
-
]);
|
|
3766
|
-
} finally {
|
|
3767
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
3768
3747
|
}
|
|
3748
|
+
await withTimeout(
|
|
3749
|
+
() => page.mouse.click(point.x, point.y),
|
|
3750
|
+
mouseFallbackTimeoutMs,
|
|
3751
|
+
"mouse.click fallback"
|
|
3752
|
+
);
|
|
3753
|
+
return { method: "mouse" };
|
|
3769
3754
|
};
|
|
3770
|
-
var
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
if (
|
|
3774
|
-
|
|
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
|
+
}
|
|
3775
3769
|
}
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
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 };
|
|
3779
3787
|
}
|
|
3780
|
-
const
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3788
|
+
const startTime = Date.now();
|
|
3789
|
+
let didScroll = false;
|
|
3790
|
+
for (let i = 0; i < maxSteps; i += 1) {
|
|
3791
|
+
const status = await checkElementVisibility(element);
|
|
3792
|
+
if (status.code === "VISIBLE") {
|
|
3793
|
+
if (status.isFixed) {
|
|
3794
|
+
logger5.info("humanScroll | fixed/sticky \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
3795
|
+
} else {
|
|
3796
|
+
logger5.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
|
|
3797
|
+
}
|
|
3798
|
+
logger5.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
3799
|
+
return { element, didScroll, restore: null };
|
|
3800
|
+
}
|
|
3801
|
+
if (status.code === "ZERO_DIMENSIONS" || status.code === "NOT_INTERACTABLE") {
|
|
3802
|
+
logger5.warn(`humanScroll | \u5143\u7D20\u4E0D\u53EF\u6EDA\u52A8\u81F3\u53EF\u70B9\u51FB\u72B6\u6001: ${status.reason || status.code}`);
|
|
3803
|
+
return { element, didScroll, restore: null };
|
|
3804
|
+
}
|
|
3805
|
+
const scrollRect = await getScrollableRect(element);
|
|
3806
|
+
if (!scrollRect && status.isFixed && status.code === "OUT_OF_VIEWPORT") {
|
|
3807
|
+
logger5.warn(`humanScroll | fixed/sticky \u76EE\u6807\u4E0D\u5728\u89C6\u53E3\u5185\uFF0C\u9875\u9762\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (direction=${status.direction || "unknown"})`);
|
|
3808
|
+
return { element, didScroll, restore: null, unscrollable: true };
|
|
3809
|
+
}
|
|
3810
|
+
if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
|
|
3811
|
+
logger5.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
|
|
3812
|
+
return { element, didScroll, restore: null, unscrollable: true };
|
|
3813
|
+
}
|
|
3814
|
+
if (scrollRect && status.code === "OBSTRUCTED" && status.obstruction?.isFixed) {
|
|
3815
|
+
const moved = await scrollAwayFromObstruction(element, status);
|
|
3816
|
+
if (moved.moved) {
|
|
3817
|
+
logger5.debug(`humanScroll | sticky/fixed \u906E\u6321\u8865\u507F\u6EDA\u52A8 top=${Math.round(moved.scrollTop || 0)}`);
|
|
3818
|
+
await waitJitter(90, 0.3);
|
|
3819
|
+
didScroll = true;
|
|
3820
|
+
continue;
|
|
3821
|
+
}
|
|
3795
3822
|
}
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
if (!style) continue;
|
|
3800
|
-
const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
|
|
3801
|
-
const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
|
|
3802
|
-
if (!clipsX && !clipsY) continue;
|
|
3803
|
-
const nodeRect = node.getBoundingClientRect();
|
|
3804
|
-
if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
|
|
3805
|
-
if (clipsX) {
|
|
3806
|
-
clipLeft = Math.max(clipLeft, nodeRect.left);
|
|
3807
|
-
clipRight = Math.min(clipRight, nodeRect.right);
|
|
3823
|
+
if (Date.now() - startTime > maxDurationMs) {
|
|
3824
|
+
logger5.warn(`humanScroll | mobile timeout (${maxDurationMs}ms, status=${status.code}, direction=${status.direction || "unknown"}, fixed=${Boolean(status.isFixed)})`);
|
|
3825
|
+
return { element, didScroll, restore: null };
|
|
3808
3826
|
}
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3827
|
+
const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
|
|
3828
|
+
const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
|
|
3829
|
+
logger5.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason || status.code} ${status.direction ? `(${status.direction})` : ""}`);
|
|
3830
|
+
const distance = stepMin + Math.random() * Math.max(1, stepMax - stepMin);
|
|
3831
|
+
let deltaY = status.direction === "up" ? -distance : distance;
|
|
3832
|
+
if (status.code === "OBSTRUCTED") {
|
|
3833
|
+
if (status.obstruction?.isFixed && status.obstruction.top != null) {
|
|
3834
|
+
const obstructionMiddle = (Number(status.obstruction.top || 0) + Number(status.obstruction.bottom || status.obstruction.top || 0)) / 2;
|
|
3835
|
+
const visibleMiddle = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
3836
|
+
deltaY = obstructionMiddle < visibleMiddle ? -distance : distance;
|
|
3837
|
+
} else {
|
|
3838
|
+
const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
3839
|
+
deltaY = status.cy > halfY ? distance : -distance;
|
|
3840
|
+
}
|
|
3812
3841
|
}
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3842
|
+
const beforeWindowState = scrollRect ? await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) : null;
|
|
3843
|
+
const beforeElementSnapshot = await getElementViewportSnapshot(element).catch(() => null);
|
|
3844
|
+
const beforeState = scrollRect ? await element.evaluate((el) => {
|
|
3845
|
+
const isScrollable = (node) => {
|
|
3846
|
+
const style = window.getComputedStyle(node);
|
|
3847
|
+
if (!style) return false;
|
|
3848
|
+
const overflowY = style.overflowY;
|
|
3849
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3850
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
3851
|
+
};
|
|
3852
|
+
let current = el;
|
|
3853
|
+
while (current && current !== document.body) {
|
|
3854
|
+
if (isScrollable(current)) {
|
|
3855
|
+
return {
|
|
3856
|
+
kind: "element",
|
|
3857
|
+
top: current.scrollTop,
|
|
3858
|
+
left: current.scrollLeft
|
|
3859
|
+
};
|
|
3860
|
+
}
|
|
3861
|
+
current = current.parentElement;
|
|
3862
|
+
}
|
|
3863
|
+
return { kind: "window", top: window.scrollY, left: window.scrollX };
|
|
3864
|
+
}) : null;
|
|
3865
|
+
await dispatchTouchSwipe(page, deltaY, { rect: scrollRect });
|
|
3866
|
+
if (scrollRect && beforeWindowState) {
|
|
3867
|
+
const afterWindowState = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
|
|
3868
|
+
if (Math.abs(afterWindowState.x - beforeWindowState.x) > 2 || Math.abs(afterWindowState.y - beforeWindowState.y) > 2) {
|
|
3869
|
+
await page.evaluate((state2) => window.scrollTo(state2.x, state2.y), beforeWindowState);
|
|
3870
|
+
logger5.debug(`humanScroll | \u7A97\u53E3\u6EDA\u52A8\u56DE\u6536 from=${Math.round(afterWindowState.y)} to=${Math.round(beforeWindowState.y)}`);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
let afterElementSnapshot = null;
|
|
3874
|
+
const readAfterElementSnapshot = async () => {
|
|
3875
|
+
if (!afterElementSnapshot) {
|
|
3876
|
+
afterElementSnapshot = await getElementViewportSnapshot(element).catch(() => null);
|
|
3877
|
+
}
|
|
3878
|
+
return afterElementSnapshot;
|
|
3831
3879
|
};
|
|
3880
|
+
if (!scrollRect && beforeElementSnapshot) {
|
|
3881
|
+
const afterSnapshot = await readAfterElementSnapshot();
|
|
3882
|
+
if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
|
|
3883
|
+
await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
|
|
3884
|
+
logger5.warn(`humanScroll | \u76EE\u6807\u4E0D\u968F\u9875\u9762\u6EDA\u52A8\u79FB\u52A8\uFF0C\u9875\u9762\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (status=${status.code}, direction=${status.direction || "unknown"})`);
|
|
3885
|
+
return { element, didScroll, restore: null, unscrollable: true };
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
if (scrollRect && beforeState) {
|
|
3889
|
+
const afterState = await element.evaluate((el) => {
|
|
3890
|
+
const isScrollable = (node) => {
|
|
3891
|
+
const style = window.getComputedStyle(node);
|
|
3892
|
+
if (!style) return false;
|
|
3893
|
+
const overflowY = style.overflowY;
|
|
3894
|
+
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
3895
|
+
return node.scrollHeight > node.clientHeight + 1;
|
|
3896
|
+
};
|
|
3897
|
+
let current = el;
|
|
3898
|
+
while (current && current !== document.body) {
|
|
3899
|
+
if (isScrollable(current)) {
|
|
3900
|
+
return {
|
|
3901
|
+
kind: "element",
|
|
3902
|
+
top: current.scrollTop,
|
|
3903
|
+
left: current.scrollLeft
|
|
3904
|
+
};
|
|
3905
|
+
}
|
|
3906
|
+
current = current.parentElement;
|
|
3907
|
+
}
|
|
3908
|
+
return { kind: "window", top: window.scrollY, left: window.scrollX };
|
|
3909
|
+
});
|
|
3910
|
+
const topDelta = Number(afterState.top || 0) - Number(beforeState.top || 0);
|
|
3911
|
+
const leftDelta = Number(afterState.left || 0) - Number(beforeState.left || 0);
|
|
3912
|
+
const expectedDelta = Number(deltaY || 0);
|
|
3913
|
+
const moved = beforeState.kind !== afterState.kind || Math.abs(topDelta) > 2 || Math.abs(leftDelta) > 2;
|
|
3914
|
+
if (!moved) {
|
|
3915
|
+
const fallback = await scrollScrollableAncestor(element, deltaY);
|
|
3916
|
+
logger5.debug(`humanScroll | \u5BB9\u5668\u89E6\u6478\u65E0\u6548\uFF0C\u76F4\u63A5\u6EDA\u52A8 fallback=${fallback.scroller ? "ancestor" : "window"} top=${Math.round(fallback.scrollTop || 0)}`);
|
|
3917
|
+
} else if (beforeState.kind === afterState.kind && Math.abs(expectedDelta) > 24 && Math.sign(topDelta || expectedDelta) === Math.sign(expectedDelta) && Math.abs(topDelta) < Math.min(Math.abs(expectedDelta) * 0.45, 96)) {
|
|
3918
|
+
const residualDelta = expectedDelta - topDelta;
|
|
3919
|
+
if (Math.sign(residualDelta) === Math.sign(expectedDelta) && Math.abs(residualDelta) > 24) {
|
|
3920
|
+
const fallback = await scrollScrollableAncestor(element, residualDelta);
|
|
3921
|
+
logger5.debug(`humanScroll | \u5BB9\u5668\u89E6\u6478\u8DDD\u79BB\u4E0D\u8DB3\uFF0C\u8865\u507F\u6EDA\u52A8 fallback=${fallback.scroller ? "ancestor" : "window"} top=${Math.round(fallback.scrollTop || 0)}`);
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
if (scrollRect && beforeElementSnapshot) {
|
|
3926
|
+
const afterSnapshot = await getElementViewportSnapshot(element).catch(() => null);
|
|
3927
|
+
if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
|
|
3928
|
+
await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
|
|
3929
|
+
logger5.warn(`humanScroll | \u76EE\u6807\u4E0D\u968F\u6EDA\u52A8\u5BB9\u5668\u79FB\u52A8\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (status=${status.code}, direction=${status.direction || "unknown"})`);
|
|
3930
|
+
return { element, didScroll, restore: null, unscrollable: true };
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
didScroll = true;
|
|
3832
3934
|
}
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3935
|
+
try {
|
|
3936
|
+
await element.scrollIntoViewIfNeeded?.();
|
|
3937
|
+
await waitJitter(80, 0.3);
|
|
3938
|
+
const finalStatus = await checkElementVisibility(element);
|
|
3939
|
+
if (finalStatus.code === "VISIBLE") {
|
|
3940
|
+
logger5.info("humanScroll | \u539F\u751F scrollIntoViewIfNeeded \u515C\u5E95\u6210\u529F");
|
|
3941
|
+
logger5.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
3942
|
+
return { element, didScroll: true, restore: null };
|
|
3837
3943
|
}
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3944
|
+
} catch (fallbackError) {
|
|
3945
|
+
logger5.debug(`humanScroll | native fallback failed: ${fallbackError?.message || fallbackError}`);
|
|
3946
|
+
}
|
|
3947
|
+
logger5.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
|
|
3948
|
+
return { element, didScroll, restore: null };
|
|
3949
|
+
},
|
|
3950
|
+
async humanClick(page, target, options = {}) {
|
|
3951
|
+
const {
|
|
3952
|
+
reactionDelay = 220,
|
|
3953
|
+
throwOnMissing = true,
|
|
3954
|
+
scrollIfNeeded = true,
|
|
3955
|
+
tapTimeoutMs = DEFAULT_TAP_TIMEOUT_MS,
|
|
3956
|
+
mouseFallbackTimeoutMs = DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS,
|
|
3957
|
+
activateFallbackTimeoutMs = DEFAULT_ACTIVATE_FALLBACK_TIMEOUT_MS,
|
|
3958
|
+
fallbackDomClick = true,
|
|
3959
|
+
fallbackDomClickOnTapError = true
|
|
3960
|
+
} = options;
|
|
3961
|
+
const targetDesc = describeTarget(target);
|
|
3962
|
+
logger5.start("humanClick", `target=${targetDesc}`);
|
|
3963
|
+
try {
|
|
3964
|
+
if (target == null) {
|
|
3965
|
+
const viewport = resolveViewport(page);
|
|
3966
|
+
await waitJitter(reactionDelay, 0.45);
|
|
3967
|
+
await tapPoint(page, {
|
|
3968
|
+
x: viewport.width * (0.45 + Math.random() * 0.1),
|
|
3969
|
+
y: viewport.height * (0.48 + Math.random() * 0.12)
|
|
3970
|
+
}, {
|
|
3971
|
+
timeoutMs: tapTimeoutMs,
|
|
3972
|
+
mouseFallbackTimeoutMs
|
|
3973
|
+
});
|
|
3974
|
+
logger5.success("humanClick", "Tapped current position");
|
|
3843
3975
|
return true;
|
|
3844
3976
|
}
|
|
3845
|
-
const
|
|
3846
|
-
if (!
|
|
3847
|
-
|
|
3848
|
-
if (!commonRect || commonRect.width <= 0 || commonRect.height <= 0) return false;
|
|
3849
|
-
const commonArea = commonRect.width * commonRect.height;
|
|
3850
|
-
const targetArea = Math.max(1, rect.width * rect.height);
|
|
3851
|
-
const maxSharedRegionArea = Math.max(targetArea * 12, 4096);
|
|
3852
|
-
if (commonArea > maxSharedRegionArea) return false;
|
|
3853
|
-
if (commonRect.width > Math.max(rect.width * 8, 120) || commonRect.height > Math.max(rect.height * 8, 120)) {
|
|
3977
|
+
const element = await resolveElement(page, target, { throwOnMissing });
|
|
3978
|
+
if (!element) {
|
|
3979
|
+
logger5.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
|
|
3854
3980
|
return false;
|
|
3855
3981
|
}
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
if (sameTapTarget(pointElement)) {
|
|
3886
|
-
return { code: "VISIBLE", isFixed, positioning };
|
|
3887
|
-
}
|
|
3888
|
-
obstruction = obstruction || describeElement(pointElement);
|
|
3889
|
-
}
|
|
3890
|
-
if (obstruction) {
|
|
3891
|
-
return {
|
|
3892
|
-
code: "OBSTRUCTED",
|
|
3893
|
-
reason: "\u88AB\u906E\u6321",
|
|
3894
|
-
direction: cy > viewH / 2 ? "down" : "up",
|
|
3895
|
-
obstruction,
|
|
3896
|
-
cy,
|
|
3897
|
-
viewH,
|
|
3898
|
-
isFixed,
|
|
3899
|
-
positioning
|
|
3900
|
-
};
|
|
3901
|
-
}
|
|
3902
|
-
return { code: "VISIBLE", isFixed, positioning };
|
|
3903
|
-
});
|
|
3904
|
-
};
|
|
3905
|
-
var activateElementFallback = async (element, point = null, options = {}) => {
|
|
3906
|
-
return element.evaluate((el, { innerOptions }) => {
|
|
3907
|
-
const isEditable = (node) => {
|
|
3908
|
-
if (!node || node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
3909
|
-
if (node.isContentEditable) return true;
|
|
3910
|
-
if (node instanceof HTMLTextAreaElement) return !node.disabled && !node.readOnly;
|
|
3911
|
-
if (node instanceof HTMLInputElement) {
|
|
3912
|
-
return !node.disabled && !node.readOnly && typeof node.select === "function";
|
|
3982
|
+
const scrollResult = scrollIfNeeded ? await MobileHumanize.humanScroll(page, element, { throwOnMissing }) : null;
|
|
3983
|
+
const status = await checkElementVisibility(element).catch(() => null);
|
|
3984
|
+
if (status && status.code !== "VISIBLE") {
|
|
3985
|
+
if (fallbackDomClick && (status.code === "OUT_OF_VIEWPORT" || status.code === "OBSTRUCTED") && (status.isFixed || scrollResult?.unscrollable)) {
|
|
3986
|
+
let fallback = await withTimeout(
|
|
3987
|
+
() => activateElementFallback(element, null, {
|
|
3988
|
+
editableOnly: true
|
|
3989
|
+
}),
|
|
3990
|
+
activateFallbackTimeoutMs,
|
|
3991
|
+
"focus fallback"
|
|
3992
|
+
).catch(() => null);
|
|
3993
|
+
if (!fallback?.activated) {
|
|
3994
|
+
fallback = await withTimeout(
|
|
3995
|
+
() => activateElementFallback(element, null, {
|
|
3996
|
+
editableOnly: false
|
|
3997
|
+
}),
|
|
3998
|
+
activateFallbackTimeoutMs,
|
|
3999
|
+
"activation fallback"
|
|
4000
|
+
).catch(() => null);
|
|
4001
|
+
}
|
|
4002
|
+
if (fallback?.activated) {
|
|
4003
|
+
logger5.warn(`humanClick: \u4E0D\u53EF\u6EDA\u52A8\u76EE\u6807\u4E0D\u53EF\u7269\u7406\u70B9\u51FB\uFF0C\u5DF2\u7528 ${fallback.method} \u6FC0\u6D3B`);
|
|
4004
|
+
return true;
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
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;
|
|
3913
4011
|
}
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
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;
|
|
3919
4017
|
}
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
4018
|
+
await waitJitter(reactionDelay, 0.45);
|
|
4019
|
+
const visibleBox = clipBoxToViewport(box, resolveViewport(page));
|
|
4020
|
+
const tapTarget = centerPointInBox(visibleBox);
|
|
4021
|
+
try {
|
|
4022
|
+
await tapPoint(page, tapTarget, {
|
|
4023
|
+
timeoutMs: tapTimeoutMs,
|
|
4024
|
+
mouseFallbackTimeoutMs
|
|
4025
|
+
});
|
|
4026
|
+
} catch (tapError) {
|
|
4027
|
+
if (!fallbackDomClickOnTapError) throw tapError;
|
|
4028
|
+
const fallback = await withTimeout(
|
|
4029
|
+
() => activateElementFallback(element, tapTarget, {
|
|
4030
|
+
editableOnly: false
|
|
4031
|
+
}),
|
|
4032
|
+
activateFallbackTimeoutMs,
|
|
4033
|
+
"activation fallback"
|
|
4034
|
+
).catch(() => null);
|
|
4035
|
+
if (!fallback?.activated) throw tapError;
|
|
4036
|
+
logger5.warn(`humanClick: tap \u5931\u8D25\u540E\u5DF2\u7528 ${fallback.method} \u515C\u5E95: ${tapError?.message || tapError}`);
|
|
3927
4037
|
}
|
|
4038
|
+
await waitJitter(120, 0.35);
|
|
4039
|
+
logger5.success("humanClick");
|
|
4040
|
+
return true;
|
|
4041
|
+
} catch (error) {
|
|
4042
|
+
logger5.fail("humanClick", error);
|
|
4043
|
+
throw error;
|
|
3928
4044
|
}
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
4045
|
+
},
|
|
4046
|
+
async randomSleep(baseMs, jitterPercent = 0.3) {
|
|
4047
|
+
await waitJitter(baseMs, jitterPercent);
|
|
4048
|
+
},
|
|
4049
|
+
async simulateGaze(page, baseDurationMs = 2500) {
|
|
4050
|
+
const durationMs = jitterMs(baseDurationMs, 0.4);
|
|
4051
|
+
const startTime = Date.now();
|
|
4052
|
+
while (Date.now() - startTime < durationMs) {
|
|
4053
|
+
if (Math.random() < 0.28) {
|
|
4054
|
+
const distance = 70 + Math.random() * 120;
|
|
4055
|
+
await dispatchTouchSwipe(page, Math.random() < 0.7 ? distance : -distance, {
|
|
4056
|
+
durationMs: 180,
|
|
4057
|
+
steps: 4
|
|
4058
|
+
});
|
|
4059
|
+
} else {
|
|
4060
|
+
await waitJitter(420, 0.55);
|
|
4061
|
+
}
|
|
3946
4062
|
}
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
return node.scrollHeight > node.clientHeight + 1;
|
|
3964
|
-
};
|
|
3965
|
-
let current = el;
|
|
3966
|
-
while (current && current !== document.body) {
|
|
3967
|
-
if (isScrollable(current)) {
|
|
3968
|
-
const rect = current.getBoundingClientRect();
|
|
3969
|
-
if (rect && rect.width > 0 && rect.height > 0) {
|
|
3970
|
-
return {
|
|
3971
|
-
x: rect.x,
|
|
3972
|
-
y: rect.y,
|
|
3973
|
-
width: rect.width,
|
|
3974
|
-
height: rect.height
|
|
3975
|
-
};
|
|
3976
|
-
}
|
|
4063
|
+
},
|
|
4064
|
+
async humanType(page, selector, text, options = {}) {
|
|
4065
|
+
const {
|
|
4066
|
+
baseDelay = 160,
|
|
4067
|
+
pauseProbability = 0.08,
|
|
4068
|
+
pauseBase = 700
|
|
4069
|
+
} = options;
|
|
4070
|
+
await MobileHumanize.humanClick(page, selector, { scrollIfNeeded: true });
|
|
4071
|
+
await waitJitter(220, 0.45);
|
|
4072
|
+
for (let i = 0; i < String(text).length; i += 1) {
|
|
4073
|
+
const char = String(text)[i];
|
|
4074
|
+
const charDelay = /[,.!?;:,。!?;:]/.test(char) ? jitterMs(baseDelay * 1.45, 0.4) : jitterMs(char === " " ? baseDelay * 0.65 : baseDelay, 0.4);
|
|
4075
|
+
await page.keyboard.type(char);
|
|
4076
|
+
await waitJitter(charDelay, 0.15);
|
|
4077
|
+
if (Math.random() < pauseProbability && i < String(text).length - 1) {
|
|
4078
|
+
await waitJitter(pauseBase, 0.5);
|
|
3977
4079
|
}
|
|
3978
|
-
current = current.parentElement;
|
|
3979
4080
|
}
|
|
3980
|
-
|
|
3981
|
-
})
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
const
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
};
|
|
4081
|
+
},
|
|
4082
|
+
async humanPress(page, targetOrKey, maybeKey, options = {}) {
|
|
4083
|
+
const hasTarget = typeof maybeKey === "string";
|
|
4084
|
+
const key = hasTarget ? maybeKey : targetOrKey;
|
|
4085
|
+
const pressOptions = hasTarget ? options : maybeKey || options;
|
|
4086
|
+
const {
|
|
4087
|
+
reactionDelay = 170,
|
|
4088
|
+
holdDelay = 42,
|
|
4089
|
+
focusDelay = 180,
|
|
4090
|
+
scrollIfNeeded = true,
|
|
4091
|
+
throwOnMissing = true,
|
|
4092
|
+
keyboardOptions = {}
|
|
4093
|
+
} = pressOptions || {};
|
|
4094
|
+
const targetDesc = hasTarget ? describeTarget(targetOrKey) : "current focus";
|
|
4095
|
+
logger5.start("humanPress", `key=${key}, target=${targetDesc}`);
|
|
4096
|
+
try {
|
|
4097
|
+
if (hasTarget) {
|
|
4098
|
+
await MobileHumanize.humanClick(page, targetOrKey, {
|
|
4099
|
+
reactionDelay: focusDelay,
|
|
4100
|
+
scrollIfNeeded,
|
|
4101
|
+
throwOnMissing
|
|
4102
|
+
});
|
|
4002
4103
|
}
|
|
4003
|
-
|
|
4104
|
+
await waitJitter(reactionDelay, 0.45);
|
|
4105
|
+
await page.keyboard.press(key, {
|
|
4106
|
+
...keyboardOptions,
|
|
4107
|
+
delay: jitterMs(holdDelay, 0.5)
|
|
4108
|
+
});
|
|
4109
|
+
logger5.success("humanPress");
|
|
4110
|
+
return true;
|
|
4111
|
+
} catch (error) {
|
|
4112
|
+
logger5.fail("humanPress", error);
|
|
4113
|
+
throw error;
|
|
4004
4114
|
}
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
}
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
const isScrollable = (node) => {
|
|
4017
|
-
const style = window.getComputedStyle(node);
|
|
4018
|
-
if (!style) return false;
|
|
4019
|
-
const overflowY = style.overflowY;
|
|
4020
|
-
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
4021
|
-
return node.scrollHeight > node.clientHeight + 1;
|
|
4115
|
+
},
|
|
4116
|
+
async humanClear(page, selector) {
|
|
4117
|
+
const locator = page.locator(selector);
|
|
4118
|
+
await MobileHumanize.humanClick(page, locator, { scrollIfNeeded: true });
|
|
4119
|
+
await waitJitter(160, 0.4);
|
|
4120
|
+
const readValue = async () => {
|
|
4121
|
+
try {
|
|
4122
|
+
return await locator.inputValue({ timeout: 600 });
|
|
4123
|
+
} catch {
|
|
4124
|
+
return await locator.evaluate((el) => "value" in el ? String(el.value || "") : String(el.textContent || "")).catch(() => "");
|
|
4125
|
+
}
|
|
4022
4126
|
};
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4127
|
+
const currentValue = await readValue();
|
|
4128
|
+
if (!currentValue) return;
|
|
4129
|
+
await page.keyboard.press("ControlOrMeta+A");
|
|
4130
|
+
await waitJitter(90, 0.35);
|
|
4131
|
+
await page.keyboard.press("Backspace");
|
|
4132
|
+
await waitJitter(120, 0.35);
|
|
4133
|
+
if (!await readValue()) return;
|
|
4134
|
+
await locator.evaluate((el) => {
|
|
4135
|
+
if ("value" in el) {
|
|
4136
|
+
el.value = "";
|
|
4137
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
4138
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
el.textContent = "";
|
|
4142
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
4143
|
+
});
|
|
4144
|
+
},
|
|
4145
|
+
async warmUpBrowsing(page, baseDuration = 3500) {
|
|
4146
|
+
const durationMs = jitterMs(baseDuration, 0.4);
|
|
4147
|
+
const startTime = Date.now();
|
|
4148
|
+
while (Date.now() - startTime < durationMs) {
|
|
4149
|
+
const action = Math.random();
|
|
4150
|
+
if (action < 0.5) {
|
|
4151
|
+
await dispatchTouchSwipe(page, 120 + Math.random() * 220, {
|
|
4152
|
+
durationMs: 240,
|
|
4153
|
+
steps: 5
|
|
4154
|
+
});
|
|
4155
|
+
} else if (action < 0.7) {
|
|
4156
|
+
await dispatchTouchSwipe(page, -(80 + Math.random() * 160), {
|
|
4157
|
+
durationMs: 220,
|
|
4158
|
+
steps: 4
|
|
4159
|
+
});
|
|
4160
|
+
} else {
|
|
4161
|
+
await waitJitter(560, 0.55);
|
|
4162
|
+
}
|
|
4030
4163
|
}
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
const
|
|
4034
|
-
const
|
|
4035
|
-
const
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
} else if (Number.isFinite(obstructionBottom) && rect.top < obstructionBottom && obstructionBottom >= scrollerRect.top && obstructionBottom <= scrollerRect.bottom && obstructionMiddle < scrollerMiddle) {
|
|
4043
|
-
deltaY = rect.top - obstructionBottom - padding;
|
|
4044
|
-
} else {
|
|
4045
|
-
const fallbackDistance = Math.max(48, Math.min(180, scrollerRect.height * 0.45));
|
|
4046
|
-
deltaY = obstructionMiddle >= scrollerMiddle ? fallbackDistance : -fallbackDistance;
|
|
4164
|
+
},
|
|
4165
|
+
async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
|
|
4166
|
+
const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
|
|
4167
|
+
const actualDistance = jitterMs(distance, 0.15);
|
|
4168
|
+
const sign = direction === "down" ? 1 : -1;
|
|
4169
|
+
for (let i = 0; i < steps; i += 1) {
|
|
4170
|
+
const factor = 1 - i / steps * 0.45;
|
|
4171
|
+
await dispatchTouchSwipe(page, actualDistance / steps * factor * sign, {
|
|
4172
|
+
durationMs: 150 + i * 20,
|
|
4173
|
+
steps: 4
|
|
4174
|
+
});
|
|
4047
4175
|
}
|
|
4048
|
-
const beforeTop = scroller.scrollTop;
|
|
4049
|
-
scroller.scrollTop = beforeTop + deltaY;
|
|
4050
|
-
return {
|
|
4051
|
-
moved: Math.abs(scroller.scrollTop - beforeTop) > 2,
|
|
4052
|
-
scrollTop: scroller.scrollTop,
|
|
4053
|
-
deltaY
|
|
4054
|
-
};
|
|
4055
|
-
}, status);
|
|
4056
|
-
};
|
|
4057
|
-
var getElementViewportSnapshot = async (element) => {
|
|
4058
|
-
return element.evaluate((el) => {
|
|
4059
|
-
const rect = el.getBoundingClientRect();
|
|
4060
|
-
return {
|
|
4061
|
-
top: rect.top,
|
|
4062
|
-
bottom: rect.bottom,
|
|
4063
|
-
left: rect.left,
|
|
4064
|
-
right: rect.right,
|
|
4065
|
-
width: rect.width,
|
|
4066
|
-
height: rect.height,
|
|
4067
|
-
scrollX: window.scrollX,
|
|
4068
|
-
scrollY: window.scrollY
|
|
4069
|
-
};
|
|
4070
|
-
});
|
|
4071
|
-
};
|
|
4072
|
-
var isTargetImmobileAfterScroll = (before, after) => {
|
|
4073
|
-
if (!before || !after) return false;
|
|
4074
|
-
const rectDeltaY = Number(after.top || 0) - Number(before.top || 0);
|
|
4075
|
-
const rectDeltaX = Number(after.left || 0) - Number(before.left || 0);
|
|
4076
|
-
const scrollDeltaY = Number(after.scrollY || 0) - Number(before.scrollY || 0);
|
|
4077
|
-
const scrollDeltaX = Number(after.scrollX || 0) - Number(before.scrollX || 0);
|
|
4078
|
-
const rectMoved = Math.abs(rectDeltaY) > 3 || Math.abs(rectDeltaX) > 3;
|
|
4079
|
-
const pageMoved = Math.abs(scrollDeltaY) > 3 || Math.abs(scrollDeltaX) > 3;
|
|
4080
|
-
if (!rectMoved && !pageMoved) return true;
|
|
4081
|
-
if (pageMoved && !rectMoved) return true;
|
|
4082
|
-
if (Math.abs(scrollDeltaY) > 12 && Math.abs(rectDeltaY) < Math.min(12, Math.abs(scrollDeltaY) * 0.2)) {
|
|
4083
|
-
return true;
|
|
4084
4176
|
}
|
|
4085
|
-
return false;
|
|
4086
4177
|
};
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4178
|
+
|
|
4179
|
+
// src/internals/humanize/cloakbrowser.js
|
|
4180
|
+
var FORCE_CLICK_OPTIONS = Object.freeze({ force: true });
|
|
4181
|
+
var resolveDeviceFromPage2 = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
|
|
4182
|
+
var isPoint2 = (value) => value && typeof value === "object" && Number.isFinite(Number(value.x)) && Number.isFinite(Number(value.y));
|
|
4183
|
+
var resolveTarget = (page, target) => typeof target === "string" ? page.locator(target).first() : target;
|
|
4184
|
+
var resolveTargetPoint = async (page, target) => {
|
|
4185
|
+
if (typeof target === "string") {
|
|
4186
|
+
return await DeviceInput.selectorCenterPoint(page, target).catch(() => null);
|
|
4091
4187
|
}
|
|
4092
|
-
await
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
const viewport = resolveViewport(page);
|
|
4100
|
-
const rawRect = options.rect || null;
|
|
4101
|
-
const rect = rawRect ? {
|
|
4102
|
-
x: clamp(rawRect.x, 0, viewport.width),
|
|
4103
|
-
y: clamp(rawRect.y, 0, viewport.height),
|
|
4104
|
-
width: clamp(rawRect.width, 0, viewport.width),
|
|
4105
|
-
height: clamp(rawRect.height, 0, viewport.height)
|
|
4106
|
-
} : null;
|
|
4107
|
-
const area = rect && rect.width > 24 && rect.height > 48 ? {
|
|
4108
|
-
left: rect.x,
|
|
4109
|
-
right: Math.min(viewport.width, rect.x + rect.width),
|
|
4110
|
-
top: rect.y,
|
|
4111
|
-
bottom: Math.min(viewport.height, rect.y + rect.height)
|
|
4112
|
-
} : {
|
|
4113
|
-
left: 0,
|
|
4114
|
-
right: viewport.width,
|
|
4115
|
-
top: 0,
|
|
4116
|
-
bottom: viewport.height
|
|
4188
|
+
const box = await target?.boundingBox?.().catch(() => null);
|
|
4189
|
+
if (!box) {
|
|
4190
|
+
return null;
|
|
4191
|
+
}
|
|
4192
|
+
return {
|
|
4193
|
+
x: box.x + box.width / 2,
|
|
4194
|
+
y: box.y + box.height / 2
|
|
4117
4195
|
};
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
try {
|
|
4135
|
-
if (page.context && typeof page.context().newCDPSession === "function") {
|
|
4136
|
-
client = await page.context().newCDPSession(page);
|
|
4196
|
+
};
|
|
4197
|
+
var sleep = (baseMs, jitterPercent = 0.3) => delay3(jitterMs(baseMs, jitterPercent));
|
|
4198
|
+
var buildKeyboardOptions = (options = {}) => ({
|
|
4199
|
+
...options.holdDelay != null ? { delay: Math.max(0, Number(options.holdDelay) || 0) } : {},
|
|
4200
|
+
...options.keyboardOptions || {}
|
|
4201
|
+
});
|
|
4202
|
+
var CloakBrowserDeviceHumanize = {
|
|
4203
|
+
jitterMs(base, jitterPercent = 0.3) {
|
|
4204
|
+
return jitterMs(base, jitterPercent);
|
|
4205
|
+
},
|
|
4206
|
+
async initializeCursor(page) {
|
|
4207
|
+
return Boolean(page);
|
|
4208
|
+
},
|
|
4209
|
+
async humanMove(page, target) {
|
|
4210
|
+
if (target == null) {
|
|
4211
|
+
return false;
|
|
4137
4212
|
}
|
|
4138
|
-
if (
|
|
4139
|
-
|
|
4140
|
-
x: startX,
|
|
4141
|
-
y: startY,
|
|
4142
|
-
yDistance: -direction * distance,
|
|
4143
|
-
xOverscroll: (Math.random() - 0.5) * 4,
|
|
4144
|
-
yOverscroll: (Math.random() - 0.5) * 8,
|
|
4145
|
-
speed: 700 + Math.round(Math.random() * 350),
|
|
4146
|
-
gestureSourceType: "touch"
|
|
4147
|
-
});
|
|
4148
|
-
await waitJitter(160, 0.35);
|
|
4149
|
-
const scrollAfterSynth = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })).catch(() => null);
|
|
4150
|
-
if (scrollBefore && scrollAfterSynth && (Math.abs(scrollAfterSynth.y - scrollBefore.y) > 2 || Math.abs(scrollAfterSynth.x - scrollBefore.x) > 2)) {
|
|
4213
|
+
if (isPoint2(target)) {
|
|
4214
|
+
await DeviceInput.move(page, target, void 0, { forceMouse: true });
|
|
4151
4215
|
return true;
|
|
4152
4216
|
}
|
|
4153
|
-
await
|
|
4154
|
-
|
|
4155
|
-
|
|
4217
|
+
const point = await resolveTargetPoint(page, target);
|
|
4218
|
+
if (!point) {
|
|
4219
|
+
return false;
|
|
4220
|
+
}
|
|
4221
|
+
await DeviceInput.move(page, point, void 0, { forceMouse: true });
|
|
4222
|
+
return true;
|
|
4223
|
+
},
|
|
4224
|
+
async humanScroll(page, target) {
|
|
4225
|
+
const element = resolveTarget(page, target);
|
|
4226
|
+
if (!element) {
|
|
4227
|
+
return { element: null, didScroll: false, restore: null };
|
|
4228
|
+
}
|
|
4229
|
+
await element.scrollIntoViewIfNeeded?.().catch(() => {
|
|
4156
4230
|
});
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
await client.send("Input.dispatchTouchEvent", {
|
|
4164
|
-
type: "touchMove",
|
|
4165
|
-
touchPoints: [{ x, y, id: 1 }]
|
|
4166
|
-
});
|
|
4231
|
+
return { element, didScroll: true, restore: null };
|
|
4232
|
+
},
|
|
4233
|
+
async humanClick(page, target) {
|
|
4234
|
+
if (target == null) {
|
|
4235
|
+
await DeviceInput.clickPoint(page, { x: 0, y: 0 }, void 0, { forceMouse: true });
|
|
4236
|
+
return true;
|
|
4167
4237
|
}
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4238
|
+
if (isPoint2(target)) {
|
|
4239
|
+
await DeviceInput.clickPoint(page, target, void 0, { forceMouse: true });
|
|
4240
|
+
return true;
|
|
4241
|
+
}
|
|
4242
|
+
await DeviceInput.click(page, target, {
|
|
4243
|
+
clickOptions: FORCE_CLICK_OPTIONS
|
|
4171
4244
|
});
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
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);
|
|
4177
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) } : {}
|
|
4268
|
+
});
|
|
4178
4269
|
return true;
|
|
4179
|
-
}
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
await
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
} catch {
|
|
4186
|
-
await page.mouse.wheel(0, deltaY);
|
|
4187
|
-
await waitJitter(120, 0.35);
|
|
4270
|
+
},
|
|
4271
|
+
async humanPress(page, targetOrKey, maybeKey, options = {}) {
|
|
4272
|
+
if (typeof maybeKey !== "string") {
|
|
4273
|
+
await DeviceInput.press(page, targetOrKey, {
|
|
4274
|
+
keyboardOptions: buildKeyboardOptions(maybeKey || options)
|
|
4275
|
+
});
|
|
4188
4276
|
return true;
|
|
4189
4277
|
}
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
}
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
await
|
|
4208
|
-
() => page.touchscreen.tap(point.x, point.y),
|
|
4209
|
-
timeoutMs,
|
|
4210
|
-
"touchscreen.tap"
|
|
4211
|
-
);
|
|
4212
|
-
return { method: "touchscreen" };
|
|
4213
|
-
} catch (error) {
|
|
4214
|
-
logger6.warn(`tapPoint | touchscreen.tap \u5931\u8D25\u6216\u8D85\u65F6\uFF0C\u5C1D\u8BD5\u9F20\u6807\u515C\u5E95: ${error?.message || error}`);
|
|
4215
|
-
if (!allowMouseFallback) throw error;
|
|
4278
|
+
await DeviceInput.press(page, targetOrKey, maybeKey, {
|
|
4279
|
+
clickOptions: FORCE_CLICK_OPTIONS,
|
|
4280
|
+
keyboardOptions: buildKeyboardOptions(options)
|
|
4281
|
+
});
|
|
4282
|
+
return true;
|
|
4283
|
+
},
|
|
4284
|
+
async humanClear(page, target) {
|
|
4285
|
+
await DeviceInput.fill(page, target, "", { force: true });
|
|
4286
|
+
},
|
|
4287
|
+
async warmUpBrowsing(page, baseDuration = 3500) {
|
|
4288
|
+
await this.simulateGaze(page, Math.min(baseDuration, 900));
|
|
4289
|
+
},
|
|
4290
|
+
async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
|
|
4291
|
+
const steps = Math.max(1, Number(baseSteps) || 1);
|
|
4292
|
+
const delta = (Number(distance) || 0) / steps * (direction === "down" ? 1 : -1);
|
|
4293
|
+
for (let index = 0; index < steps; index += 1) {
|
|
4294
|
+
await page.mouse.wheel(0, delta);
|
|
4295
|
+
await sleep(60 + index * 20, 0.3);
|
|
4216
4296
|
}
|
|
4217
4297
|
}
|
|
4218
|
-
await withTimeout(
|
|
4219
|
-
() => page.mouse.click(point.x, point.y),
|
|
4220
|
-
mouseFallbackTimeoutMs,
|
|
4221
|
-
"mouse.click fallback"
|
|
4222
|
-
);
|
|
4223
|
-
return { method: "mouse" };
|
|
4224
4298
|
};
|
|
4225
|
-
var
|
|
4226
|
-
|
|
4299
|
+
var resolveCloakBrowserHumanizeDelegate = (page) => resolveDeviceFromPage2(page) === Device.Mobile ? MobileHumanize : CloakBrowserDeviceHumanize;
|
|
4300
|
+
var CloakBrowserHumanizeContext = Object.freeze({
|
|
4301
|
+
desktopDelegate: CloakBrowserDeviceHumanize,
|
|
4302
|
+
resolveDelegate: resolveCloakBrowserHumanizeDelegate
|
|
4303
|
+
});
|
|
4304
|
+
|
|
4305
|
+
// src/internals/humanize/desktop.js
|
|
4306
|
+
import delay4 from "delay";
|
|
4307
|
+
import { createCursor } from "ghost-cursor-playwright";
|
|
4308
|
+
var logger6 = createInternalLogger("Humanize");
|
|
4309
|
+
var $CursorWeakMap = /* @__PURE__ */ new WeakMap();
|
|
4310
|
+
function $GetCursor(page) {
|
|
4311
|
+
const cursor = $CursorWeakMap.get(page);
|
|
4312
|
+
if (!cursor) {
|
|
4313
|
+
throw new Error("Cursor \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 Humanize.initializeCursor(page)");
|
|
4314
|
+
}
|
|
4315
|
+
return cursor;
|
|
4316
|
+
}
|
|
4317
|
+
var Humanize = {
|
|
4318
|
+
/**
|
|
4319
|
+
* 生成带抖动的毫秒数 - 基于基础值添加随机浮动 (±30% 默认)
|
|
4320
|
+
* @param {number} base - 基础延迟 (ms)
|
|
4321
|
+
* @param {number} [jitterPercent=0.3] - 抖动百分比 (0.3 = ±30%)
|
|
4322
|
+
* @returns {number} 抖动后的毫秒数
|
|
4323
|
+
*/
|
|
4324
|
+
jitterMs(base, jitterPercent = 0.3) {
|
|
4325
|
+
const jitter = base * jitterPercent * (Math.random() * 2 - 1);
|
|
4326
|
+
return Math.max(10, Math.round(base + jitter));
|
|
4327
|
+
},
|
|
4328
|
+
/**
|
|
4329
|
+
* 初始化页面的 Ghost Cursor(必须在使用其他 cursor 相关方法前调用)
|
|
4330
|
+
*
|
|
4331
|
+
* @param {import('playwright').Page} page
|
|
4332
|
+
* @returns {Promise<void>}
|
|
4333
|
+
*/
|
|
4227
4334
|
async initializeCursor(page) {
|
|
4228
|
-
if (
|
|
4229
|
-
|
|
4230
|
-
|
|
4335
|
+
if ($CursorWeakMap.has(page)) {
|
|
4336
|
+
logger6.debug("initializeCursor: cursor already exists, skipping");
|
|
4337
|
+
return;
|
|
4338
|
+
}
|
|
4339
|
+
logger6.start("initializeCursor", "creating cursor");
|
|
4340
|
+
const cursor = await createCursor(page);
|
|
4341
|
+
$CursorWeakMap.set(page, cursor);
|
|
4342
|
+
logger6.success("initializeCursor", "cursor initialized");
|
|
4231
4343
|
},
|
|
4344
|
+
/**
|
|
4345
|
+
* 人类化鼠标移动 - 使用 ghost-cursor 移动到指定位置或元素
|
|
4346
|
+
*
|
|
4347
|
+
* @param {import('playwright').Page} page
|
|
4348
|
+
* @param {string|{x: number, y: number}|import('playwright').ElementHandle} target - CSS选择器、坐标对象或元素句柄
|
|
4349
|
+
*/
|
|
4232
4350
|
async humanMove(page, target) {
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
if (
|
|
4237
|
-
|
|
4351
|
+
const cursor = $GetCursor(page);
|
|
4352
|
+
logger6.start("humanMove", `target=${typeof target === "string" ? target : "element/coords"}`);
|
|
4353
|
+
try {
|
|
4354
|
+
if (typeof target === "string") {
|
|
4355
|
+
const element = await page.$(target);
|
|
4356
|
+
if (!element) {
|
|
4357
|
+
logger6.warn(`humanMove: \u5143\u7D20\u4E0D\u5B58\u5728 ${target}`);
|
|
4358
|
+
return false;
|
|
4359
|
+
}
|
|
4360
|
+
const box = await element.boundingBox();
|
|
4361
|
+
if (!box) {
|
|
4362
|
+
logger6.warn(`humanMove: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E ${target}`);
|
|
4363
|
+
return false;
|
|
4364
|
+
}
|
|
4365
|
+
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
|
|
4366
|
+
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
|
|
4367
|
+
await cursor.actions.move({ x, y });
|
|
4368
|
+
} else if (target && typeof target.x === "number" && typeof target.y === "number") {
|
|
4369
|
+
await cursor.actions.move(target);
|
|
4370
|
+
} else if (target && typeof target.boundingBox === "function") {
|
|
4371
|
+
const box = await target.boundingBox();
|
|
4372
|
+
if (box) {
|
|
4373
|
+
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
|
|
4374
|
+
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
|
|
4375
|
+
await cursor.actions.move({ x, y });
|
|
4376
|
+
}
|
|
4238
4377
|
}
|
|
4378
|
+
logger6.success("humanMove");
|
|
4379
|
+
return true;
|
|
4380
|
+
} catch (error) {
|
|
4381
|
+
logger6.fail("humanMove", error);
|
|
4382
|
+
throw error;
|
|
4239
4383
|
}
|
|
4240
|
-
await waitJitter(80, 0.4);
|
|
4241
|
-
return true;
|
|
4242
4384
|
},
|
|
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
|
+
*/
|
|
4243
4397
|
async humanScroll(page, target, options = {}) {
|
|
4244
4398
|
const {
|
|
4245
4399
|
maxSteps = 20,
|
|
4246
|
-
minStep =
|
|
4247
|
-
maxStep =
|
|
4248
|
-
maxDurationMs = maxSteps *
|
|
4249
|
-
throwOnMissing = false
|
|
4400
|
+
minStep = 260,
|
|
4401
|
+
maxStep = 800,
|
|
4402
|
+
maxDurationMs = maxSteps * 220 + 800
|
|
4250
4403
|
} = options;
|
|
4251
|
-
const targetDesc =
|
|
4404
|
+
const targetDesc = typeof target === "string" ? target : "ElementHandle";
|
|
4252
4405
|
logger6.start("humanScroll", `target=${targetDesc}`);
|
|
4253
|
-
|
|
4254
|
-
if (
|
|
4255
|
-
|
|
4256
|
-
|
|
4406
|
+
let element;
|
|
4407
|
+
if (typeof target === "string") {
|
|
4408
|
+
element = await page.$(target);
|
|
4409
|
+
if (!element) {
|
|
4410
|
+
logger6.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
|
|
4411
|
+
return { element: null, didScroll: false };
|
|
4412
|
+
}
|
|
4413
|
+
} else {
|
|
4414
|
+
element = target;
|
|
4257
4415
|
}
|
|
4258
|
-
const
|
|
4416
|
+
const cursor = $GetCursor(page);
|
|
4259
4417
|
let didScroll = false;
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
if (
|
|
4264
|
-
|
|
4265
|
-
} else {
|
|
4266
|
-
logger6.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
|
|
4418
|
+
const checkVisibility = async () => {
|
|
4419
|
+
return await element.evaluate((el) => {
|
|
4420
|
+
const rect = el.getBoundingClientRect();
|
|
4421
|
+
if (!rect || rect.width === 0 || rect.height === 0) {
|
|
4422
|
+
return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6" };
|
|
4267
4423
|
}
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
}
|
|
4280
|
-
if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
|
|
4281
|
-
logger6.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
|
|
4282
|
-
return { element, didScroll, restore: null, unscrollable: true };
|
|
4283
|
-
}
|
|
4284
|
-
if (scrollRect && status.code === "OBSTRUCTED" && status.obstruction?.isFixed) {
|
|
4285
|
-
const moved = await scrollAwayFromObstruction(element, status);
|
|
4286
|
-
if (moved.moved) {
|
|
4287
|
-
logger6.debug(`humanScroll | sticky/fixed \u906E\u6321\u8865\u507F\u6EDA\u52A8 top=${Math.round(moved.scrollTop || 0)}`);
|
|
4288
|
-
await waitJitter(90, 0.3);
|
|
4289
|
-
didScroll = true;
|
|
4290
|
-
continue;
|
|
4424
|
+
const cx = rect.left + rect.width / 2;
|
|
4425
|
+
const cy = rect.top + rect.height / 2;
|
|
4426
|
+
const viewH = window.innerHeight;
|
|
4427
|
+
const viewW = window.innerWidth;
|
|
4428
|
+
let isFixed = false;
|
|
4429
|
+
for (let node = el; node && node !== document.body; node = node.parentElement) {
|
|
4430
|
+
const style = window.getComputedStyle(node);
|
|
4431
|
+
if (style && style.position === "fixed") {
|
|
4432
|
+
isFixed = true;
|
|
4433
|
+
break;
|
|
4434
|
+
}
|
|
4291
4435
|
}
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
return { element, didScroll, restore: null };
|
|
4296
|
-
}
|
|
4297
|
-
const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
|
|
4298
|
-
const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
|
|
4299
|
-
logger6.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason || status.code} ${status.direction ? `(${status.direction})` : ""}`);
|
|
4300
|
-
const distance = stepMin + Math.random() * Math.max(1, stepMax - stepMin);
|
|
4301
|
-
let deltaY = status.direction === "up" ? -distance : distance;
|
|
4302
|
-
if (status.code === "OBSTRUCTED") {
|
|
4303
|
-
if (status.obstruction?.isFixed && status.obstruction.top != null) {
|
|
4304
|
-
const obstructionMiddle = (Number(status.obstruction.top || 0) + Number(status.obstruction.bottom || status.obstruction.top || 0)) / 2;
|
|
4305
|
-
const visibleMiddle = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
4306
|
-
deltaY = obstructionMiddle < visibleMiddle ? -distance : distance;
|
|
4307
|
-
} else {
|
|
4308
|
-
const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
4309
|
-
deltaY = status.cy > halfY ? distance : -distance;
|
|
4436
|
+
if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
|
|
4437
|
+
const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
|
|
4438
|
+
return { code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy, viewH, isFixed };
|
|
4310
4439
|
}
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4440
|
+
const pointElement = document.elementFromPoint(cx, cy);
|
|
4441
|
+
if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
|
|
4442
|
+
return {
|
|
4443
|
+
code: "OBSTRUCTED",
|
|
4444
|
+
reason: "\u88AB\u906E\u6321",
|
|
4445
|
+
obstruction: {
|
|
4446
|
+
tag: pointElement.tagName,
|
|
4447
|
+
id: pointElement.id,
|
|
4448
|
+
className: pointElement.className
|
|
4449
|
+
},
|
|
4450
|
+
cy,
|
|
4451
|
+
// Return Center Y for smart direction calculation
|
|
4452
|
+
viewH,
|
|
4453
|
+
isFixed
|
|
4454
|
+
};
|
|
4455
|
+
}
|
|
4456
|
+
return { code: "VISIBLE", isFixed };
|
|
4457
|
+
});
|
|
4458
|
+
};
|
|
4459
|
+
const getScrollableRect2 = async () => {
|
|
4460
|
+
return await element.evaluate((el) => {
|
|
4315
4461
|
const isScrollable = (node) => {
|
|
4316
4462
|
const style = window.getComputedStyle(node);
|
|
4317
4463
|
if (!style) return false;
|
|
@@ -4321,260 +4467,260 @@ var MobileHumanize = {
|
|
|
4321
4467
|
};
|
|
4322
4468
|
let current = el;
|
|
4323
4469
|
while (current && current !== document.body) {
|
|
4324
|
-
if (isScrollable(current)) {
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
left: current.scrollLeft
|
|
4329
|
-
};
|
|
4330
|
-
}
|
|
4331
|
-
current = current.parentElement;
|
|
4332
|
-
}
|
|
4333
|
-
return { kind: "window", top: window.scrollY, left: window.scrollX };
|
|
4334
|
-
}) : null;
|
|
4335
|
-
await dispatchTouchSwipe(page, deltaY, { rect: scrollRect });
|
|
4336
|
-
if (scrollRect && beforeWindowState) {
|
|
4337
|
-
const afterWindowState = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
|
|
4338
|
-
if (Math.abs(afterWindowState.x - beforeWindowState.x) > 2 || Math.abs(afterWindowState.y - beforeWindowState.y) > 2) {
|
|
4339
|
-
await page.evaluate((state2) => window.scrollTo(state2.x, state2.y), beforeWindowState);
|
|
4340
|
-
logger6.debug(`humanScroll | \u7A97\u53E3\u6EDA\u52A8\u56DE\u6536 from=${Math.round(afterWindowState.y)} to=${Math.round(beforeWindowState.y)}`);
|
|
4341
|
-
}
|
|
4342
|
-
}
|
|
4343
|
-
let afterElementSnapshot = null;
|
|
4344
|
-
const readAfterElementSnapshot = async () => {
|
|
4345
|
-
if (!afterElementSnapshot) {
|
|
4346
|
-
afterElementSnapshot = await getElementViewportSnapshot(element).catch(() => null);
|
|
4347
|
-
}
|
|
4348
|
-
return afterElementSnapshot;
|
|
4349
|
-
};
|
|
4350
|
-
if (!scrollRect && beforeElementSnapshot) {
|
|
4351
|
-
const afterSnapshot = await readAfterElementSnapshot();
|
|
4352
|
-
if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
|
|
4353
|
-
await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
|
|
4354
|
-
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"})`);
|
|
4355
|
-
return { element, didScroll, restore: null, unscrollable: true };
|
|
4356
|
-
}
|
|
4357
|
-
}
|
|
4358
|
-
if (scrollRect && beforeState) {
|
|
4359
|
-
const afterState = await element.evaluate((el) => {
|
|
4360
|
-
const isScrollable = (node) => {
|
|
4361
|
-
const style = window.getComputedStyle(node);
|
|
4362
|
-
if (!style) return false;
|
|
4363
|
-
const overflowY = style.overflowY;
|
|
4364
|
-
if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
|
|
4365
|
-
return node.scrollHeight > node.clientHeight + 1;
|
|
4366
|
-
};
|
|
4367
|
-
let current = el;
|
|
4368
|
-
while (current && current !== document.body) {
|
|
4369
|
-
if (isScrollable(current)) {
|
|
4370
|
-
return {
|
|
4371
|
-
kind: "element",
|
|
4372
|
-
top: current.scrollTop,
|
|
4373
|
-
left: current.scrollLeft
|
|
4374
|
-
};
|
|
4470
|
+
if (isScrollable(current)) {
|
|
4471
|
+
const rect = current.getBoundingClientRect();
|
|
4472
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
4473
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
4375
4474
|
}
|
|
4376
|
-
current = current.parentElement;
|
|
4377
4475
|
}
|
|
4378
|
-
|
|
4379
|
-
}
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4476
|
+
current = current.parentElement;
|
|
4477
|
+
}
|
|
4478
|
+
return null;
|
|
4479
|
+
});
|
|
4480
|
+
};
|
|
4481
|
+
const startTime = Date.now();
|
|
4482
|
+
try {
|
|
4483
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
4484
|
+
if (Date.now() - startTime > maxDurationMs) {
|
|
4485
|
+
logger6.warn(`humanScroll | \u8D85\u65F6\u4FDD\u62A4\u89E6\u53D1 (${maxDurationMs}ms)`);
|
|
4486
|
+
return { element, didScroll };
|
|
4487
|
+
}
|
|
4488
|
+
const status = await checkVisibility();
|
|
4489
|
+
if (status.code === "VISIBLE") {
|
|
4490
|
+
if (status.isFixed) {
|
|
4491
|
+
logger6.info("humanScroll | fixed \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
4492
|
+
} else {
|
|
4493
|
+
logger6.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
|
|
4392
4494
|
}
|
|
4495
|
+
logger6.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
4496
|
+
return { element, didScroll };
|
|
4393
4497
|
}
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
|
|
4398
|
-
await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
|
|
4399
|
-
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"})`);
|
|
4400
|
-
return { element, didScroll, restore: null, unscrollable: true };
|
|
4498
|
+
logger6.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${status.direction ? `(${status.direction})` : ""}`);
|
|
4499
|
+
if (status.code === "OBSTRUCTED" && status.obstruction) {
|
|
4500
|
+
logger6.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
|
|
4401
4501
|
}
|
|
4502
|
+
const scrollRect = await getScrollableRect2();
|
|
4503
|
+
if (!scrollRect && status.isFixed) {
|
|
4504
|
+
logger6.warn("humanScroll | fixed \u5BB9\u5668\u5185\u4E14\u65E0\u53EF\u6EDA\u52A8\u7956\u5148\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
|
|
4505
|
+
return { element, didScroll };
|
|
4506
|
+
}
|
|
4507
|
+
const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
|
|
4508
|
+
const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
|
|
4509
|
+
let deltaY = 0;
|
|
4510
|
+
if (status.code === "OUT_OF_VIEWPORT") {
|
|
4511
|
+
if (status.direction === "down") {
|
|
4512
|
+
deltaY = stepMin + Math.random() * (stepMax - stepMin);
|
|
4513
|
+
} else if (status.direction === "up") {
|
|
4514
|
+
deltaY = -(stepMin + Math.random() * (stepMax - stepMin));
|
|
4515
|
+
} else {
|
|
4516
|
+
deltaY = 100;
|
|
4517
|
+
}
|
|
4518
|
+
} else if (status.code === "OBSTRUCTED") {
|
|
4519
|
+
const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
|
|
4520
|
+
const isBottomHalf = status.cy > halfY;
|
|
4521
|
+
const direction = isBottomHalf ? 1 : -1;
|
|
4522
|
+
deltaY = direction * (stepMin + Math.random() * 50);
|
|
4523
|
+
}
|
|
4524
|
+
if (i === 0) {
|
|
4525
|
+
const viewSize = page.viewportSize();
|
|
4526
|
+
if (scrollRect) {
|
|
4527
|
+
const safeX = scrollRect.x + scrollRect.width * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.width * 0.4);
|
|
4528
|
+
const safeY = scrollRect.y + scrollRect.height * 0.5 + (Math.random() - 0.5) * Math.min(80, scrollRect.height * 0.4);
|
|
4529
|
+
await cursor.actions.move({ x: safeX, y: safeY });
|
|
4530
|
+
} else if (viewSize) {
|
|
4531
|
+
const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 80;
|
|
4532
|
+
const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 80;
|
|
4533
|
+
await cursor.actions.move({ x: safeX, y: safeY });
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
await page.mouse.wheel(0, deltaY);
|
|
4537
|
+
didScroll = true;
|
|
4538
|
+
await delay4(this.jitterMs(20 + Math.random() * 40, 0.2));
|
|
4402
4539
|
}
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
const finalStatus = await checkElementVisibility(element);
|
|
4409
|
-
if (finalStatus.code === "VISIBLE") {
|
|
4410
|
-
logger6.info("humanScroll | \u539F\u751F scrollIntoViewIfNeeded \u515C\u5E95\u6210\u529F");
|
|
4411
|
-
logger6.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
|
|
4412
|
-
return { element, didScroll: true, restore: null };
|
|
4413
|
-
}
|
|
4414
|
-
} catch (fallbackError) {
|
|
4415
|
-
logger6.debug(`humanScroll | native fallback failed: ${fallbackError?.message || fallbackError}`);
|
|
4540
|
+
logger6.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
|
|
4541
|
+
return { element, didScroll };
|
|
4542
|
+
} catch (error) {
|
|
4543
|
+
logger6.fail("humanScroll", error);
|
|
4544
|
+
throw error;
|
|
4416
4545
|
}
|
|
4417
|
-
logger6.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
|
|
4418
|
-
return { element, didScroll, restore: null };
|
|
4419
4546
|
},
|
|
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
|
+
*/
|
|
4420
4557
|
async humanClick(page, target, options = {}) {
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
scrollIfNeeded = true,
|
|
4425
|
-
tapTimeoutMs = DEFAULT_TAP_TIMEOUT_MS,
|
|
4426
|
-
mouseFallbackTimeoutMs = DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS,
|
|
4427
|
-
activateFallbackTimeoutMs = DEFAULT_ACTIVATE_FALLBACK_TIMEOUT_MS,
|
|
4428
|
-
fallbackDomClick = true,
|
|
4429
|
-
fallbackDomClickOnTapError = true
|
|
4430
|
-
} = options;
|
|
4431
|
-
const targetDesc = describeTarget(target);
|
|
4558
|
+
const cursor = $GetCursor(page);
|
|
4559
|
+
const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
|
|
4560
|
+
const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
|
|
4432
4561
|
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
|
+
};
|
|
4433
4573
|
try {
|
|
4434
4574
|
if (target == null) {
|
|
4435
|
-
|
|
4436
|
-
await
|
|
4437
|
-
|
|
4438
|
-
x: viewport.width * (0.45 + Math.random() * 0.1),
|
|
4439
|
-
y: viewport.height * (0.48 + Math.random() * 0.12)
|
|
4440
|
-
}, {
|
|
4441
|
-
timeoutMs: tapTimeoutMs,
|
|
4442
|
-
mouseFallbackTimeoutMs
|
|
4443
|
-
});
|
|
4444
|
-
logger6.success("humanClick", "Tapped current position");
|
|
4575
|
+
await delay4(this.jitterMs(reactionDelay, 0.4));
|
|
4576
|
+
await cursor.actions.click();
|
|
4577
|
+
logger6.success("humanClick", "Clicked current position");
|
|
4445
4578
|
return true;
|
|
4446
4579
|
}
|
|
4447
|
-
|
|
4448
|
-
if (
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
const status = await checkElementVisibility(element).catch(() => null);
|
|
4454
|
-
if (status && status.code !== "VISIBLE") {
|
|
4455
|
-
if (fallbackDomClick && (status.code === "OUT_OF_VIEWPORT" || status.code === "OBSTRUCTED") && (status.isFixed || scrollResult?.unscrollable)) {
|
|
4456
|
-
let fallback = await withTimeout(
|
|
4457
|
-
() => activateElementFallback(element, null, {
|
|
4458
|
-
editableOnly: true
|
|
4459
|
-
}),
|
|
4460
|
-
activateFallbackTimeoutMs,
|
|
4461
|
-
"focus fallback"
|
|
4462
|
-
).catch(() => null);
|
|
4463
|
-
if (!fallback?.activated) {
|
|
4464
|
-
fallback = await withTimeout(
|
|
4465
|
-
() => activateElementFallback(element, null, {
|
|
4466
|
-
editableOnly: false
|
|
4467
|
-
}),
|
|
4468
|
-
activateFallbackTimeoutMs,
|
|
4469
|
-
"activation fallback"
|
|
4470
|
-
).catch(() => null);
|
|
4471
|
-
}
|
|
4472
|
-
if (fallback?.activated) {
|
|
4473
|
-
logger6.warn(`humanClick: \u4E0D\u53EF\u6EDA\u52A8\u76EE\u6807\u4E0D\u53EF\u7269\u7406\u70B9\u51FB\uFF0C\u5DF2\u7528 ${fallback.method} \u6FC0\u6D3B`);
|
|
4474
|
-
return true;
|
|
4580
|
+
let element;
|
|
4581
|
+
if (typeof target === "string") {
|
|
4582
|
+
element = await page.$(target);
|
|
4583
|
+
if (!element) {
|
|
4584
|
+
if (throwOnMissing) {
|
|
4585
|
+
throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${target}`);
|
|
4475
4586
|
}
|
|
4587
|
+
logger6.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
|
|
4588
|
+
return false;
|
|
4476
4589
|
}
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4590
|
+
} else {
|
|
4591
|
+
element = target;
|
|
4592
|
+
}
|
|
4593
|
+
if (scrollIfNeeded) {
|
|
4594
|
+
const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
|
|
4595
|
+
restoreOnce.do = didScroll && restore ? restoreFn : null;
|
|
4481
4596
|
}
|
|
4482
4597
|
const box = await element.boundingBox();
|
|
4483
4598
|
if (!box) {
|
|
4484
|
-
|
|
4599
|
+
await restoreOnce();
|
|
4600
|
+
if (throwOnMissing) {
|
|
4601
|
+
throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
4602
|
+
}
|
|
4485
4603
|
logger6.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
|
|
4486
4604
|
return false;
|
|
4487
4605
|
}
|
|
4488
|
-
|
|
4489
|
-
const
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
mouseFallbackTimeoutMs
|
|
4495
|
-
});
|
|
4496
|
-
} catch (tapError) {
|
|
4497
|
-
if (!fallbackDomClickOnTapError) throw tapError;
|
|
4498
|
-
const fallback = await withTimeout(
|
|
4499
|
-
() => activateElementFallback(element, tapTarget, {
|
|
4500
|
-
editableOnly: false
|
|
4501
|
-
}),
|
|
4502
|
-
activateFallbackTimeoutMs,
|
|
4503
|
-
"activation fallback"
|
|
4504
|
-
).catch(() => null);
|
|
4505
|
-
if (!fallback?.activated) throw tapError;
|
|
4506
|
-
logger6.warn(`humanClick: tap \u5931\u8D25\u540E\u5DF2\u7528 ${fallback.method} \u515C\u5E95: ${tapError?.message || tapError}`);
|
|
4507
|
-
}
|
|
4508
|
-
await waitJitter(120, 0.35);
|
|
4606
|
+
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
|
|
4607
|
+
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
|
|
4608
|
+
await cursor.actions.move({ x, y });
|
|
4609
|
+
await delay4(this.jitterMs(reactionDelay, 0.4));
|
|
4610
|
+
await cursor.actions.click();
|
|
4611
|
+
await restoreOnce();
|
|
4509
4612
|
logger6.success("humanClick");
|
|
4510
4613
|
return true;
|
|
4511
4614
|
} catch (error) {
|
|
4615
|
+
await restoreOnce();
|
|
4512
4616
|
logger6.fail("humanClick", error);
|
|
4513
4617
|
throw error;
|
|
4514
4618
|
}
|
|
4515
4619
|
},
|
|
4620
|
+
/**
|
|
4621
|
+
* 随机延迟一段毫秒数(带 ±30% 抖动)
|
|
4622
|
+
* @param {number} baseMs - 基础延迟毫秒数
|
|
4623
|
+
* @param {number} [jitterPercent=0.3] - 抖动百分比
|
|
4624
|
+
*/
|
|
4516
4625
|
async randomSleep(baseMs, jitterPercent = 0.3) {
|
|
4517
|
-
|
|
4626
|
+
const ms = this.jitterMs(baseMs, jitterPercent);
|
|
4627
|
+
logger6.start("randomSleep", `base=${baseMs}, actual=${ms}ms`);
|
|
4628
|
+
await delay4(ms);
|
|
4629
|
+
logger6.success("randomSleep");
|
|
4518
4630
|
},
|
|
4631
|
+
/**
|
|
4632
|
+
* 模拟人类"注视"或"阅读"行为:鼠标在页面上随机微动
|
|
4633
|
+
* @param {import('playwright').Page} page
|
|
4634
|
+
* @param {number} [baseDurationMs=2500] - 基础持续时间 (±40% 抖动)
|
|
4635
|
+
*/
|
|
4519
4636
|
async simulateGaze(page, baseDurationMs = 2500) {
|
|
4520
|
-
const
|
|
4637
|
+
const cursor = $GetCursor(page);
|
|
4638
|
+
const durationMs = this.jitterMs(baseDurationMs, 0.4);
|
|
4639
|
+
logger6.start("simulateGaze", `duration=${durationMs}ms`);
|
|
4521
4640
|
const startTime = Date.now();
|
|
4641
|
+
const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
|
|
4522
4642
|
while (Date.now() - startTime < durationMs) {
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
steps: 4
|
|
4528
|
-
});
|
|
4529
|
-
} else {
|
|
4530
|
-
await waitJitter(420, 0.55);
|
|
4531
|
-
}
|
|
4643
|
+
const x = 100 + Math.random() * (viewportSize.width - 200);
|
|
4644
|
+
const y = 100 + Math.random() * (viewportSize.height - 200);
|
|
4645
|
+
await cursor.actions.move({ x, y });
|
|
4646
|
+
await delay4(this.jitterMs(600, 0.5));
|
|
4532
4647
|
}
|
|
4648
|
+
logger6.success("simulateGaze");
|
|
4533
4649
|
},
|
|
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
|
+
*/
|
|
4534
4660
|
async humanType(page, selector, text, options = {}) {
|
|
4661
|
+
logger6.start("humanType", `selector=${selector}, textLen=${text.length}`);
|
|
4535
4662
|
const {
|
|
4536
|
-
baseDelay =
|
|
4663
|
+
baseDelay = 180,
|
|
4537
4664
|
pauseProbability = 0.08,
|
|
4538
|
-
pauseBase =
|
|
4539
|
-
} = options;
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4665
|
+
pauseBase = 800
|
|
4666
|
+
} = options;
|
|
4667
|
+
try {
|
|
4668
|
+
const locator = page.locator(selector);
|
|
4669
|
+
await Humanize.humanClick(page, locator);
|
|
4670
|
+
await delay4(this.jitterMs(200, 0.4));
|
|
4671
|
+
for (let i = 0; i < text.length; i++) {
|
|
4672
|
+
const char = text[i];
|
|
4673
|
+
let charDelay;
|
|
4674
|
+
if (char === " ") {
|
|
4675
|
+
charDelay = this.jitterMs(baseDelay * 0.6, 0.3);
|
|
4676
|
+
} else if (/[,.!?;:,。!?;:]/.test(char)) {
|
|
4677
|
+
charDelay = this.jitterMs(baseDelay * 1.5, 0.4);
|
|
4678
|
+
} else {
|
|
4679
|
+
charDelay = this.jitterMs(baseDelay, 0.4);
|
|
4680
|
+
}
|
|
4681
|
+
await page.keyboard.type(char);
|
|
4682
|
+
await delay4(charDelay);
|
|
4683
|
+
if (Math.random() < pauseProbability && i < text.length - 1) {
|
|
4684
|
+
const pauseTime = this.jitterMs(pauseBase, 0.5);
|
|
4685
|
+
logger6.debug(`\u505C\u987F ${pauseTime}ms...`);
|
|
4686
|
+
await delay4(pauseTime);
|
|
4687
|
+
}
|
|
4549
4688
|
}
|
|
4689
|
+
logger6.success("humanType");
|
|
4690
|
+
} catch (error) {
|
|
4691
|
+
logger6.fail("humanType", error);
|
|
4692
|
+
throw error;
|
|
4550
4693
|
}
|
|
4551
4694
|
},
|
|
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
|
+
*/
|
|
4552
4702
|
async humanPress(page, targetOrKey, maybeKey, options = {}) {
|
|
4553
4703
|
const hasTarget = typeof maybeKey === "string";
|
|
4554
4704
|
const key = hasTarget ? maybeKey : targetOrKey;
|
|
4555
4705
|
const pressOptions = hasTarget ? options : maybeKey || options;
|
|
4556
4706
|
const {
|
|
4557
|
-
reactionDelay =
|
|
4558
|
-
holdDelay =
|
|
4707
|
+
reactionDelay = 180,
|
|
4708
|
+
holdDelay = 45,
|
|
4559
4709
|
focusDelay = 180,
|
|
4560
4710
|
scrollIfNeeded = true,
|
|
4561
4711
|
throwOnMissing = true,
|
|
4562
4712
|
keyboardOptions = {}
|
|
4563
4713
|
} = pressOptions || {};
|
|
4564
|
-
const targetDesc = hasTarget ?
|
|
4714
|
+
const targetDesc = hasTarget ? typeof targetOrKey === "string" ? targetOrKey : "ElementHandle" : "current focus";
|
|
4565
4715
|
logger6.start("humanPress", `key=${key}, target=${targetDesc}`);
|
|
4566
4716
|
try {
|
|
4567
4717
|
if (hasTarget) {
|
|
4568
|
-
await
|
|
4569
|
-
reactionDelay: focusDelay,
|
|
4570
|
-
scrollIfNeeded,
|
|
4571
|
-
throwOnMissing
|
|
4572
|
-
});
|
|
4718
|
+
await this.humanClick(page, targetOrKey, { reactionDelay: focusDelay, scrollIfNeeded, throwOnMissing });
|
|
4573
4719
|
}
|
|
4574
|
-
await
|
|
4720
|
+
await delay4(this.jitterMs(reactionDelay, 0.45));
|
|
4575
4721
|
await page.keyboard.press(key, {
|
|
4576
4722
|
...keyboardOptions,
|
|
4577
|
-
delay: jitterMs(holdDelay, 0.5)
|
|
4723
|
+
delay: this.jitterMs(holdDelay, 0.5)
|
|
4578
4724
|
});
|
|
4579
4725
|
logger6.success("humanPress");
|
|
4580
4726
|
return true;
|
|
@@ -4583,285 +4729,164 @@ var MobileHumanize = {
|
|
|
4583
4729
|
throw error;
|
|
4584
4730
|
}
|
|
4585
4731
|
},
|
|
4732
|
+
/**
|
|
4733
|
+
* 人类化清空输入框 - 模拟人类删除文本的行为
|
|
4734
|
+
* @param {import('playwright').Page} page
|
|
4735
|
+
* @param {string} selector - 输入框选择器
|
|
4736
|
+
*/
|
|
4586
4737
|
async humanClear(page, selector) {
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
}
|
|
4596
|
-
};
|
|
4597
|
-
const currentValue = await readValue();
|
|
4598
|
-
if (!currentValue) return;
|
|
4599
|
-
await page.keyboard.press("ControlOrMeta+A");
|
|
4600
|
-
await waitJitter(90, 0.35);
|
|
4601
|
-
await page.keyboard.press("Backspace");
|
|
4602
|
-
await waitJitter(120, 0.35);
|
|
4603
|
-
if (!await readValue()) return;
|
|
4604
|
-
await locator.evaluate((el) => {
|
|
4605
|
-
if ("value" in el) {
|
|
4606
|
-
el.value = "";
|
|
4607
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
4608
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
4738
|
+
logger6.start("humanClear", `selector=${selector}`);
|
|
4739
|
+
try {
|
|
4740
|
+
const locator = page.locator(selector);
|
|
4741
|
+
await locator.click();
|
|
4742
|
+
await delay4(this.jitterMs(200, 0.4));
|
|
4743
|
+
const currentValue = await locator.inputValue();
|
|
4744
|
+
if (!currentValue || currentValue.length === 0) {
|
|
4745
|
+
logger6.success("humanClear", "already empty");
|
|
4609
4746
|
return;
|
|
4610
4747
|
}
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4748
|
+
await page.keyboard.press("Meta+A");
|
|
4749
|
+
await delay4(this.jitterMs(100, 0.4));
|
|
4750
|
+
await page.keyboard.press("Backspace");
|
|
4751
|
+
logger6.success("humanClear");
|
|
4752
|
+
} catch (error) {
|
|
4753
|
+
logger6.fail("humanClear", error);
|
|
4754
|
+
throw error;
|
|
4755
|
+
}
|
|
4614
4756
|
},
|
|
4757
|
+
/**
|
|
4758
|
+
* 页面预热浏览 - 模拟人类进入页面后的探索行为
|
|
4759
|
+
* @param {import('playwright').Page} page
|
|
4760
|
+
* @param {number} [baseDuration=3500] - 预热时长基础值 (±40% 抖动)
|
|
4761
|
+
*/
|
|
4615
4762
|
async warmUpBrowsing(page, baseDuration = 3500) {
|
|
4616
|
-
const
|
|
4763
|
+
const cursor = $GetCursor(page);
|
|
4764
|
+
const durationMs = this.jitterMs(baseDuration, 0.4);
|
|
4765
|
+
logger6.start("warmUpBrowsing", `duration=${durationMs}ms`);
|
|
4617
4766
|
const startTime = Date.now();
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4767
|
+
const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
|
|
4768
|
+
try {
|
|
4769
|
+
while (Date.now() - startTime < durationMs) {
|
|
4770
|
+
const action = Math.random();
|
|
4771
|
+
if (action < 0.4) {
|
|
4772
|
+
const x = 100 + Math.random() * (viewportSize.width - 200);
|
|
4773
|
+
const y = 100 + Math.random() * (viewportSize.height - 200);
|
|
4774
|
+
await cursor.actions.move({ x, y });
|
|
4775
|
+
await delay4(this.jitterMs(350, 0.4));
|
|
4776
|
+
} else if (action < 0.7) {
|
|
4777
|
+
const scrollY = (Math.random() - 0.5) * 200;
|
|
4778
|
+
await page.mouse.wheel(0, scrollY);
|
|
4779
|
+
await delay4(this.jitterMs(500, 0.4));
|
|
4780
|
+
} else {
|
|
4781
|
+
await delay4(this.jitterMs(800, 0.5));
|
|
4782
|
+
}
|
|
4632
4783
|
}
|
|
4784
|
+
logger6.success("warmUpBrowsing");
|
|
4785
|
+
} catch (error) {
|
|
4786
|
+
logger6.fail("warmUpBrowsing", error);
|
|
4787
|
+
throw error;
|
|
4633
4788
|
}
|
|
4634
4789
|
},
|
|
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
|
+
*/
|
|
4635
4797
|
async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
|
|
4636
4798
|
const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
|
|
4637
|
-
const actualDistance = jitterMs(distance, 0.15);
|
|
4799
|
+
const actualDistance = this.jitterMs(distance, 0.15);
|
|
4800
|
+
logger6.start("naturalScroll", `dir=${direction}, dist=${actualDistance}, steps=${steps}`);
|
|
4638
4801
|
const sign = direction === "down" ? 1 : -1;
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4802
|
+
const stepDistance = actualDistance / steps;
|
|
4803
|
+
try {
|
|
4804
|
+
for (let i = 0; i < steps; i++) {
|
|
4805
|
+
const factor = 1 - i / steps * 0.5;
|
|
4806
|
+
const jitter = 0.9 + Math.random() * 0.2;
|
|
4807
|
+
const scrollAmount = stepDistance * factor * sign * jitter;
|
|
4808
|
+
await page.mouse.wheel(0, scrollAmount);
|
|
4809
|
+
const baseDelay = 60 + i * 25;
|
|
4810
|
+
await delay4(this.jitterMs(baseDelay, 0.3));
|
|
4811
|
+
}
|
|
4812
|
+
logger6.success("naturalScroll");
|
|
4813
|
+
} catch (error) {
|
|
4814
|
+
logger6.fail("naturalScroll", error);
|
|
4815
|
+
throw error;
|
|
4645
4816
|
}
|
|
4646
4817
|
}
|
|
4647
4818
|
};
|
|
4648
4819
|
|
|
4649
4820
|
// src/internals/humanize/default.js
|
|
4650
|
-
var resolveDeviceFromPage2 = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
|
|
4651
|
-
var resolveDelegate = (page) => {
|
|
4652
|
-
return resolveDeviceFromPage2(page) === Device.Mobile ? MobileHumanize : Humanize;
|
|
4653
|
-
};
|
|
4654
|
-
var callDelegate = (method, page, args) => {
|
|
4655
|
-
const delegate = resolveDelegate(page);
|
|
4656
|
-
return delegate[method](page, ...args);
|
|
4657
|
-
};
|
|
4658
|
-
var DefaultHumanize = {
|
|
4659
|
-
jitterMs(base, jitterPercent = 0.3) {
|
|
4660
|
-
return Humanize.jitterMs(base, jitterPercent);
|
|
4661
|
-
},
|
|
4662
|
-
initializeCursor(page) {
|
|
4663
|
-
return callDelegate("initializeCursor", page, []);
|
|
4664
|
-
},
|
|
4665
|
-
humanMove(page, target) {
|
|
4666
|
-
return callDelegate("humanMove", page, [target]);
|
|
4667
|
-
},
|
|
4668
|
-
humanScroll(page, target, options = {}) {
|
|
4669
|
-
return callDelegate("humanScroll", page, [target, options]);
|
|
4670
|
-
},
|
|
4671
|
-
humanClick(page, target, options = {}) {
|
|
4672
|
-
return callDelegate("humanClick", page, [target, options]);
|
|
4673
|
-
},
|
|
4674
|
-
randomSleep(pageOrBaseMs, maybeBaseMs, maybeJitterPercent) {
|
|
4675
|
-
if (pageOrBaseMs && typeof pageOrBaseMs === "object" && typeof pageOrBaseMs.evaluate === "function") {
|
|
4676
|
-
const delegate = resolveDelegate(pageOrBaseMs);
|
|
4677
|
-
return delegate.randomSleep(maybeBaseMs, maybeJitterPercent);
|
|
4678
|
-
}
|
|
4679
|
-
return Humanize.randomSleep(pageOrBaseMs, maybeBaseMs);
|
|
4680
|
-
},
|
|
4681
|
-
simulateGaze(page, baseDurationMs = 2500) {
|
|
4682
|
-
return callDelegate("simulateGaze", page, [baseDurationMs]);
|
|
4683
|
-
},
|
|
4684
|
-
humanType(page, selector, text, options = {}) {
|
|
4685
|
-
return callDelegate("humanType", page, [selector, text, options]);
|
|
4686
|
-
},
|
|
4687
|
-
humanPress(page, targetOrKey, maybeKey, options = {}) {
|
|
4688
|
-
if (typeof maybeKey === "string") {
|
|
4689
|
-
return callDelegate("humanPress", page, [targetOrKey, maybeKey, options]);
|
|
4690
|
-
}
|
|
4691
|
-
return callDelegate("humanPress", page, [targetOrKey, maybeKey || options]);
|
|
4692
|
-
},
|
|
4693
|
-
humanClear(page, selector) {
|
|
4694
|
-
return callDelegate("humanClear", page, [selector]);
|
|
4695
|
-
},
|
|
4696
|
-
warmUpBrowsing(page, baseDuration = 3500) {
|
|
4697
|
-
return callDelegate("warmUpBrowsing", page, [baseDuration]);
|
|
4698
|
-
},
|
|
4699
|
-
naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
|
|
4700
|
-
return callDelegate("naturalScroll", page, [direction, distance, baseSteps]);
|
|
4701
|
-
}
|
|
4702
|
-
};
|
|
4703
|
-
|
|
4704
|
-
// src/internals/humanize/cloakbrowser.js
|
|
4705
|
-
import delay4 from "delay";
|
|
4706
4821
|
var resolveDeviceFromPage3 = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
|
|
4707
|
-
var
|
|
4822
|
+
var resolveDefaultHumanizeDelegate = (page) => resolveDeviceFromPage3(page) === Device.Mobile ? MobileHumanize : Humanize;
|
|
4823
|
+
var DefaultHumanizeContext = Object.freeze({
|
|
4824
|
+
desktopDelegate: Humanize,
|
|
4825
|
+
resolveDelegate: resolveDefaultHumanizeDelegate
|
|
4826
|
+
});
|
|
4827
|
+
|
|
4828
|
+
// src/internals/humanize/index.js
|
|
4829
|
+
var HUMANIZE_DELEGATED_METHODS = Object.freeze({
|
|
4830
|
+
initializeCursor: { enumerable: true },
|
|
4831
|
+
humanMove: { enumerable: true },
|
|
4832
|
+
humanScroll: { enumerable: true },
|
|
4833
|
+
humanClick: { enumerable: true },
|
|
4834
|
+
simulateGaze: { enumerable: true },
|
|
4835
|
+
humanType: { enumerable: true },
|
|
4836
|
+
humanPress: { enumerable: true },
|
|
4837
|
+
humanClear: { enumerable: true },
|
|
4838
|
+
warmUpBrowsing: { enumerable: true },
|
|
4839
|
+
naturalScroll: { enumerable: true }
|
|
4840
|
+
});
|
|
4708
4841
|
var isPageLike2 = (value) => value && typeof value === "object" && typeof value.evaluate === "function";
|
|
4709
|
-
var
|
|
4710
|
-
var resolveTarget = (page, target) => typeof target === "string" ? page.locator(target).first() : target;
|
|
4711
|
-
var sleep = (base, jitter = 0.3) => delay4(jitterMs(base, jitter));
|
|
4712
|
-
var FORCE_OPTIONS = Object.freeze({ force: true });
|
|
4713
|
-
var DesktopCloakHumanize = {
|
|
4714
|
-
jitterMs(base, jitterPercent = 0.3) {
|
|
4715
|
-
return jitterMs(base, jitterPercent);
|
|
4716
|
-
},
|
|
4717
|
-
async initializeCursor(page) {
|
|
4718
|
-
return Boolean(page);
|
|
4719
|
-
},
|
|
4720
|
-
async humanMove(page, target) {
|
|
4721
|
-
if (target == null) return false;
|
|
4722
|
-
if (isPoint2(target)) {
|
|
4723
|
-
await page.mouse.move(Number(target.x), Number(target.y));
|
|
4724
|
-
return true;
|
|
4725
|
-
}
|
|
4726
|
-
await resolveTarget(page, target).hover(FORCE_OPTIONS);
|
|
4727
|
-
return true;
|
|
4728
|
-
},
|
|
4729
|
-
async humanScroll(page, target) {
|
|
4730
|
-
const element = resolveTarget(page, target);
|
|
4731
|
-
if (!element) return { element: null, didScroll: false, restore: null };
|
|
4732
|
-
await element.scrollIntoViewIfNeeded?.();
|
|
4733
|
-
return { element, didScroll: true, restore: null };
|
|
4734
|
-
},
|
|
4735
|
-
async humanClick(page, target) {
|
|
4736
|
-
if (target == null) {
|
|
4737
|
-
await page.mouse.click(0, 0);
|
|
4738
|
-
return true;
|
|
4739
|
-
}
|
|
4740
|
-
if (isPoint2(target)) {
|
|
4741
|
-
await page.mouse.click(Number(target.x), Number(target.y));
|
|
4742
|
-
return true;
|
|
4743
|
-
}
|
|
4744
|
-
if (typeof target === "string") {
|
|
4745
|
-
await page.click(target, FORCE_OPTIONS);
|
|
4746
|
-
return true;
|
|
4747
|
-
}
|
|
4748
|
-
await target.click(FORCE_OPTIONS);
|
|
4749
|
-
return true;
|
|
4750
|
-
},
|
|
4751
|
-
async randomSleep(baseMs, jitterPercent = 0.3) {
|
|
4752
|
-
await sleep(baseMs, jitterPercent);
|
|
4753
|
-
},
|
|
4754
|
-
async simulateGaze(page, baseDurationMs = 2500) {
|
|
4755
|
-
const durationMs = jitterMs(baseDurationMs, 0.4);
|
|
4756
|
-
const startedAt = Date.now();
|
|
4757
|
-
const viewport = page.viewportSize() || { width: 1365, height: 900 };
|
|
4758
|
-
while (Date.now() - startedAt < durationMs) {
|
|
4759
|
-
await page.mouse.move(
|
|
4760
|
-
100 + Math.random() * Math.max(120, viewport.width - 200),
|
|
4761
|
-
100 + Math.random() * Math.max(120, viewport.height - 200)
|
|
4762
|
-
);
|
|
4763
|
-
await sleep(300, 0.5);
|
|
4764
|
-
}
|
|
4765
|
-
},
|
|
4766
|
-
async humanType(page, selector, text) {
|
|
4767
|
-
const target = resolveTarget(page, selector);
|
|
4768
|
-
await target.click(FORCE_OPTIONS);
|
|
4769
|
-
await page.keyboard.type(text);
|
|
4770
|
-
},
|
|
4771
|
-
async humanPress(page, targetOrKey, maybeKey) {
|
|
4772
|
-
if (typeof maybeKey !== "string") {
|
|
4773
|
-
await page.keyboard.press(targetOrKey);
|
|
4774
|
-
return true;
|
|
4775
|
-
}
|
|
4776
|
-
await resolveTarget(page, targetOrKey).click(FORCE_OPTIONS);
|
|
4777
|
-
await page.keyboard.press(maybeKey);
|
|
4778
|
-
return true;
|
|
4779
|
-
},
|
|
4780
|
-
async humanClear(page, selector) {
|
|
4781
|
-
if (typeof selector === "string") {
|
|
4782
|
-
await page.fill(selector, "", FORCE_OPTIONS);
|
|
4783
|
-
return;
|
|
4784
|
-
}
|
|
4785
|
-
await selector.fill("", FORCE_OPTIONS);
|
|
4786
|
-
},
|
|
4787
|
-
async warmUpBrowsing(page, baseDuration = 3500) {
|
|
4788
|
-
await this.simulateGaze(page, Math.min(baseDuration, 900));
|
|
4789
|
-
},
|
|
4790
|
-
async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
|
|
4791
|
-
const steps = Math.max(1, Number(baseSteps) || 1);
|
|
4792
|
-
const delta = (Number(distance) || 0) / steps * (direction === "down" ? 1 : -1);
|
|
4793
|
-
for (let index = 0; index < steps; index += 1) {
|
|
4794
|
-
await page.mouse.wheel(0, delta);
|
|
4795
|
-
await sleep(60 + index * 20, 0.3);
|
|
4796
|
-
}
|
|
4797
|
-
}
|
|
4798
|
-
};
|
|
4799
|
-
var resolveDelegate2 = (page) => isMobilePage(page) ? MobileHumanize : DesktopCloakHumanize;
|
|
4800
|
-
var callDelegate2 = (method, page, args) => resolveDelegate2(page)[method](page, ...args);
|
|
4801
|
-
var CloakBrowserHumanize = {
|
|
4842
|
+
var createSharedHumanizeMethods = ({ desktopDelegate, resolveDelegate }) => ({
|
|
4802
4843
|
jitterMs(base, jitterPercent = 0.3) {
|
|
4803
|
-
return jitterMs(base, jitterPercent);
|
|
4804
|
-
},
|
|
4805
|
-
initializeCursor(page) {
|
|
4806
|
-
return callDelegate2("initializeCursor", page, []);
|
|
4807
|
-
},
|
|
4808
|
-
humanMove(page, target) {
|
|
4809
|
-
return callDelegate2("humanMove", page, [target]);
|
|
4810
|
-
},
|
|
4811
|
-
humanScroll(page, target, options = {}) {
|
|
4812
|
-
return callDelegate2("humanScroll", page, [target, options]);
|
|
4813
|
-
},
|
|
4814
|
-
humanClick(page, target, options = {}) {
|
|
4815
|
-
return callDelegate2("humanClick", page, [target, options]);
|
|
4844
|
+
return desktopDelegate.jitterMs(base, jitterPercent);
|
|
4816
4845
|
},
|
|
4817
4846
|
randomSleep(pageOrBaseMs, maybeBaseMs, maybeJitterPercent) {
|
|
4818
4847
|
if (isPageLike2(pageOrBaseMs)) {
|
|
4819
|
-
return
|
|
4820
|
-
}
|
|
4821
|
-
return DesktopCloakHumanize.randomSleep(pageOrBaseMs, maybeBaseMs);
|
|
4822
|
-
},
|
|
4823
|
-
simulateGaze(page, baseDurationMs = 2500) {
|
|
4824
|
-
return callDelegate2("simulateGaze", page, [baseDurationMs]);
|
|
4825
|
-
},
|
|
4826
|
-
humanType(page, selector, text, options = {}) {
|
|
4827
|
-
return callDelegate2("humanType", page, [selector, text, options]);
|
|
4828
|
-
},
|
|
4829
|
-
humanPress(page, targetOrKey, maybeKey, options = {}) {
|
|
4830
|
-
if (typeof maybeKey === "string") {
|
|
4831
|
-
return callDelegate2("humanPress", page, [targetOrKey, maybeKey, options]);
|
|
4848
|
+
return resolveDelegate(pageOrBaseMs).randomSleep(maybeBaseMs, maybeJitterPercent);
|
|
4832
4849
|
}
|
|
4833
|
-
return
|
|
4834
|
-
},
|
|
4835
|
-
humanClear(page, selector) {
|
|
4836
|
-
return callDelegate2("humanClear", page, [selector]);
|
|
4837
|
-
},
|
|
4838
|
-
warmUpBrowsing(page, baseDuration = 3500) {
|
|
4839
|
-
return callDelegate2("warmUpBrowsing", page, [baseDuration]);
|
|
4840
|
-
},
|
|
4841
|
-
naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
|
|
4842
|
-
return callDelegate2("naturalScroll", page, [direction, distance, baseSteps]);
|
|
4850
|
+
return desktopDelegate.randomSleep(pageOrBaseMs, maybeBaseMs);
|
|
4843
4851
|
}
|
|
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
|
+
);
|
|
4844
4866
|
};
|
|
4867
|
+
var DefaultHumanize = createHumanizeExport(DefaultHumanizeContext);
|
|
4868
|
+
var CloakBrowserHumanize = createHumanizeExport(CloakBrowserHumanizeContext);
|
|
4845
4869
|
|
|
4846
4870
|
// src/humanize.js
|
|
4847
4871
|
var humanizeStrategies = {
|
|
4848
4872
|
[Mode.Default]: DefaultHumanize,
|
|
4849
4873
|
[Mode.CloakBrowser]: CloakBrowserHumanize
|
|
4850
4874
|
};
|
|
4851
|
-
var
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4875
|
+
var humanizeFacadeMethods = Object.freeze({
|
|
4876
|
+
jitterMs: { enumerable: true },
|
|
4877
|
+
initializeCursor: { enumerable: true },
|
|
4878
|
+
humanMove: { enumerable: true },
|
|
4879
|
+
humanScroll: { enumerable: true },
|
|
4880
|
+
humanClick: { enumerable: true },
|
|
4881
|
+
randomSleep: { enumerable: true },
|
|
4882
|
+
simulateGaze: { enumerable: true },
|
|
4883
|
+
humanType: { enumerable: true },
|
|
4884
|
+
humanPress: { enumerable: true },
|
|
4885
|
+
humanClear: { enumerable: true },
|
|
4886
|
+
warmUpBrowsing: { enumerable: true },
|
|
4887
|
+
naturalScroll: { enumerable: true }
|
|
4888
|
+
});
|
|
4889
|
+
var Humanize2 = createDelegatedFacade("Humanize", humanizeStrategies, humanizeFacadeMethods);
|
|
4865
4890
|
|
|
4866
4891
|
// src/internals/launch/default.js
|
|
4867
4892
|
import { execFileSync } from "node:child_process";
|
|
@@ -5551,9 +5576,10 @@ var launchStrategies = {
|
|
|
5551
5576
|
[Mode.Default]: DefaultLaunch,
|
|
5552
5577
|
[Mode.CloakBrowser]: CloakBrowserLaunch
|
|
5553
5578
|
};
|
|
5554
|
-
var
|
|
5555
|
-
|
|
5556
|
-
|
|
5579
|
+
var launchFacadeMethods = Object.freeze({
|
|
5580
|
+
getPlaywrightCrawlerOptions: { enumerable: true }
|
|
5581
|
+
});
|
|
5582
|
+
var Launch = createDelegatedFacade("Launch", launchStrategies, launchFacadeMethods);
|
|
5557
5583
|
|
|
5558
5584
|
// src/live-view.js
|
|
5559
5585
|
import express from "express";
|