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