@skrillex1224/playwright-toolkit 2.1.36 → 2.1.38
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/README.md +0 -3
- package/dist/index.cjs +285 -248
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +285 -248
- package/dist/index.js.map +4 -4
- package/index.d.ts +54 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -183,38 +183,87 @@ async function createApifyKit() {
|
|
|
183
183
|
const { Actor: Actor2 } = apify;
|
|
184
184
|
return {
|
|
185
185
|
/**
|
|
186
|
-
*
|
|
186
|
+
* 核心封装:执行步骤,带自动日志确认、失败截图处理和重试机制
|
|
187
|
+
*
|
|
188
|
+
* @param {string} step - 步骤名称
|
|
189
|
+
* @param {import('playwright').Page} page - Playwright page 对象
|
|
190
|
+
* @param {Function} actionFn - 执行的异步操作
|
|
191
|
+
* @param {Object} [options] - 配置选项
|
|
192
|
+
* @param {boolean} [options.failActor=true] - 失败时是否调用 Actor.fail
|
|
193
|
+
* @param {Object} [options.retry] - 重试配置
|
|
194
|
+
* @param {number} [options.retry.times=0] - 重试次数
|
|
195
|
+
* @param {'direct'|'refresh'} [options.retry.mode='direct'] - 重试模式
|
|
196
|
+
* @param {Function} [options.retry.before] - 重试前钩子,可覆盖默认等待行为
|
|
187
197
|
*/
|
|
188
198
|
async runStep(step, page, actionFn, options = {}) {
|
|
189
|
-
const { failActor = true } = options;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
logger.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
});
|
|
213
|
-
await Actor2.fail(`Run Step ${step} \u5931\u8D25: ${error.message}`);
|
|
199
|
+
const { failActor = true, retry = {} } = options;
|
|
200
|
+
const { times: retryTimes = 0, mode: retryMode = "direct", before: beforeRetry } = retry;
|
|
201
|
+
const executeAction = async (attemptNumber) => {
|
|
202
|
+
const attemptLabel = attemptNumber > 0 ? ` (\u91CD\u8BD5 #${attemptNumber})` : "";
|
|
203
|
+
logger.start(`[Step] ${step}${attemptLabel}`);
|
|
204
|
+
try {
|
|
205
|
+
const result = await actionFn();
|
|
206
|
+
logger.success(`[Step] ${step}${attemptLabel}`);
|
|
207
|
+
return { success: true, result };
|
|
208
|
+
} catch (error) {
|
|
209
|
+
logger.fail(`[Step] ${step}${attemptLabel}`, error);
|
|
210
|
+
return { success: false, error };
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const prepareForRetry = async (attemptNumber) => {
|
|
214
|
+
if (typeof beforeRetry === "function") {
|
|
215
|
+
logger.start(`[RetryStep] \u6267\u884C\u81EA\u5B9A\u4E49 before \u94A9\u5B50 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
|
|
216
|
+
await beforeRetry(page, attemptNumber);
|
|
217
|
+
logger.success(`[RetryStep] before \u94A9\u5B50\u5B8C\u6210`);
|
|
218
|
+
} else if (retryMode === "refresh") {
|
|
219
|
+
logger.start(`[RetryStep] \u5237\u65B0\u9875\u9762 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
|
|
220
|
+
await page.reload({ waitUntil: "domcontentloaded" });
|
|
221
|
+
logger.success(`[RetryStep] \u9875\u9762\u5237\u65B0\u5B8C\u6210`);
|
|
214
222
|
} else {
|
|
215
|
-
|
|
223
|
+
logger.start(`[RetryStep] \u7B49\u5F85 3 \u79D2 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
|
|
224
|
+
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
225
|
+
logger.success(`[RetryStep] \u7B49\u5F85\u5B8C\u6210`);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
let lastResult = await executeAction(0);
|
|
229
|
+
if (lastResult.success) {
|
|
230
|
+
return lastResult.result;
|
|
231
|
+
}
|
|
232
|
+
for (let attempt = 1; attempt <= retryTimes; attempt++) {
|
|
233
|
+
logger.start(`[RetryStep] \u51C6\u5907\u7B2C ${attempt}/${retryTimes} \u6B21\u91CD\u8BD5: ${step}`);
|
|
234
|
+
try {
|
|
235
|
+
await prepareForRetry(attempt);
|
|
236
|
+
} catch (prepareError) {
|
|
237
|
+
logger.warn(`[RetryStep] \u91CD\u8BD5\u51C6\u5907\u5931\u8D25: ${prepareError.message}`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
lastResult = await executeAction(attempt);
|
|
241
|
+
if (lastResult.success) {
|
|
242
|
+
return lastResult.result;
|
|
216
243
|
}
|
|
217
244
|
}
|
|
245
|
+
const finalError = lastResult.error;
|
|
246
|
+
if (failActor) {
|
|
247
|
+
let base64 = "\u622A\u56FE\u5931\u8D25";
|
|
248
|
+
try {
|
|
249
|
+
if (page) {
|
|
250
|
+
const buffer = await page.screenshot({ fullPage: true, type: "jpeg", quality: 60 });
|
|
251
|
+
base64 = `data:image/jpeg;base64,${buffer.toString("base64")}`;
|
|
252
|
+
}
|
|
253
|
+
} catch (snapErr) {
|
|
254
|
+
logger.warn(`\u622A\u56FE\u751F\u6210\u5931\u8D25: ${snapErr.message}`);
|
|
255
|
+
}
|
|
256
|
+
await this.pushFailed(finalError, {
|
|
257
|
+
step,
|
|
258
|
+
page,
|
|
259
|
+
options,
|
|
260
|
+
base64,
|
|
261
|
+
retryAttempts: retryTimes
|
|
262
|
+
});
|
|
263
|
+
await Actor2.fail(`Run Step ${step} \u5931\u8D25 (\u5DF2\u91CD\u8BD5 ${retryTimes} \u6B21): ${finalError.message}`);
|
|
264
|
+
} else {
|
|
265
|
+
throw finalError;
|
|
266
|
+
}
|
|
218
267
|
},
|
|
219
268
|
/**
|
|
220
269
|
* 宽松版runStep:失败时不调用Actor.fail,只抛出异常
|
|
@@ -308,6 +357,8 @@ var Utils = {
|
|
|
308
357
|
* @param {import('playwright').Page} page - Playwright page 对象
|
|
309
358
|
* @param {Object} [options] - 配置选项
|
|
310
359
|
* @param {number} [options.buffer] - 额外缓冲高度 (默认: 视口高度的一半)
|
|
360
|
+
* @param {boolean} [options.restore] - 截图后是否恢复页面高度和样式 (默认: false)
|
|
361
|
+
* @param {number} [options.maxHeight] - 最大截图高度 (默认: 8000px)
|
|
311
362
|
* @returns {Promise<string>} - base64 编码的 PNG 图片
|
|
312
363
|
*/
|
|
313
364
|
async fullPageScreenshot(page, options = {}) {
|
|
@@ -315,15 +366,17 @@ var Utils = {
|
|
|
315
366
|
const originalViewport = page.viewportSize();
|
|
316
367
|
const defaultBuffer = Math.round((originalViewport?.height || 1080) / 2);
|
|
317
368
|
const buffer = options.buffer ?? defaultBuffer;
|
|
369
|
+
const restore = options.restore ?? false;
|
|
370
|
+
const maxHeight = options.maxHeight ?? 8e3;
|
|
318
371
|
try {
|
|
319
372
|
const maxScrollHeight = await page.evaluate(() => {
|
|
320
|
-
let
|
|
373
|
+
let maxHeight2 = document.body.scrollHeight;
|
|
321
374
|
document.querySelectorAll("*").forEach((el) => {
|
|
322
375
|
const style = window.getComputedStyle(el);
|
|
323
376
|
const overflowY = style.overflowY;
|
|
324
377
|
if ((overflowY === "auto" || overflowY === "scroll") && el.scrollHeight > el.clientHeight) {
|
|
325
|
-
if (el.scrollHeight >
|
|
326
|
-
|
|
378
|
+
if (el.scrollHeight > maxHeight2) {
|
|
379
|
+
maxHeight2 = el.scrollHeight;
|
|
327
380
|
}
|
|
328
381
|
el.dataset.pkOrigOverflow = el.style.overflow;
|
|
329
382
|
el.dataset.pkOrigHeight = el.style.height;
|
|
@@ -334,11 +387,12 @@ var Utils = {
|
|
|
334
387
|
el.style.maxHeight = "none";
|
|
335
388
|
}
|
|
336
389
|
});
|
|
337
|
-
return
|
|
390
|
+
return maxHeight2;
|
|
338
391
|
});
|
|
392
|
+
const targetHeight = Math.min(maxScrollHeight + buffer, maxHeight);
|
|
339
393
|
await page.setViewportSize({
|
|
340
394
|
width: originalViewport?.width || 1280,
|
|
341
|
-
height:
|
|
395
|
+
height: targetHeight
|
|
342
396
|
});
|
|
343
397
|
await (0, import_delay.default)(1e3);
|
|
344
398
|
const buffer_ = await page.screenshot({
|
|
@@ -348,19 +402,21 @@ var Utils = {
|
|
|
348
402
|
logger2.success("fullPageScreenshot", `captured ${Math.round(buffer_.length / 1024)} KB`);
|
|
349
403
|
return buffer_.toString("base64");
|
|
350
404
|
} finally {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
405
|
+
if (restore) {
|
|
406
|
+
await page.evaluate(() => {
|
|
407
|
+
document.querySelectorAll(".__pk_expanded__").forEach((el) => {
|
|
408
|
+
el.style.overflow = el.dataset.pkOrigOverflow || "";
|
|
409
|
+
el.style.height = el.dataset.pkOrigHeight || "";
|
|
410
|
+
el.style.maxHeight = el.dataset.pkOrigMaxHeight || "";
|
|
411
|
+
delete el.dataset.pkOrigOverflow;
|
|
412
|
+
delete el.dataset.pkOrigHeight;
|
|
413
|
+
delete el.dataset.pkOrigMaxHeight;
|
|
414
|
+
el.classList.remove("__pk_expanded__");
|
|
415
|
+
});
|
|
360
416
|
});
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
417
|
+
if (originalViewport) {
|
|
418
|
+
await page.setViewportSize(originalViewport);
|
|
419
|
+
}
|
|
364
420
|
}
|
|
365
421
|
}
|
|
366
422
|
}
|
|
@@ -376,25 +432,12 @@ var BASE_CONFIG = Object.freeze({
|
|
|
376
432
|
geolocation: null
|
|
377
433
|
});
|
|
378
434
|
var DEFAULT_LAUNCH_ARGS = [
|
|
379
|
-
|
|
435
|
+
// '--disable-blink-features=AutomationControlled', // Crawlee 可能会自动处理,过多干预反而会被识别
|
|
380
436
|
"--no-sandbox",
|
|
381
437
|
"--disable-setuid-sandbox",
|
|
382
438
|
"--window-position=0,0",
|
|
383
439
|
`--lang=${BASE_CONFIG.locale}`
|
|
384
440
|
];
|
|
385
|
-
var ADVANCED_LAUNCH_ARGS = [
|
|
386
|
-
...DEFAULT_LAUNCH_ARGS,
|
|
387
|
-
"--disable-dev-shm-usage",
|
|
388
|
-
"--disable-background-networking",
|
|
389
|
-
"--disable-default-apps",
|
|
390
|
-
"--disable-extensions",
|
|
391
|
-
"--disable-sync",
|
|
392
|
-
"--disable-translate",
|
|
393
|
-
"--metrics-recording-only",
|
|
394
|
-
"--mute-audio",
|
|
395
|
-
"--no-first-run"
|
|
396
|
-
];
|
|
397
|
-
var CONTEXT_CONFIG_CACHE = /* @__PURE__ */ new WeakMap();
|
|
398
441
|
function buildFingerprintOptions(locale) {
|
|
399
442
|
return {
|
|
400
443
|
browsers: [{ name: "chrome", minVersion: 110 }],
|
|
@@ -403,95 +446,9 @@ function buildFingerprintOptions(locale) {
|
|
|
403
446
|
locales: [locale]
|
|
404
447
|
};
|
|
405
448
|
}
|
|
406
|
-
function parseAcceptLanguage(acceptLanguage) {
|
|
407
|
-
if (!acceptLanguage) return [];
|
|
408
|
-
return acceptLanguage.split(",").map((part) => part.trim().split(";")[0]).filter(Boolean);
|
|
409
|
-
}
|
|
410
|
-
function normalizeLanguages(acceptLanguage, fallbackLocale) {
|
|
411
|
-
const languages = parseAcceptLanguage(acceptLanguage);
|
|
412
|
-
if (languages.length === 0) return [fallbackLocale];
|
|
413
|
-
if (!languages.includes(fallbackLocale)) {
|
|
414
|
-
return [fallbackLocale, ...languages];
|
|
415
|
-
}
|
|
416
|
-
return languages;
|
|
417
|
-
}
|
|
418
|
-
function getOperatingSystemsFromUserAgent(userAgent) {
|
|
419
|
-
const lowerUA = userAgent.toLowerCase();
|
|
420
|
-
if (lowerUA.includes("windows")) return ["windows"];
|
|
421
|
-
if (lowerUA.includes("mac os") || lowerUA.includes("macintosh")) return ["macos"];
|
|
422
|
-
if (lowerUA.includes("linux")) return ["linux"];
|
|
423
|
-
return [];
|
|
424
|
-
}
|
|
425
|
-
function buildContextConfigKey(config) {
|
|
426
|
-
return JSON.stringify({
|
|
427
|
-
locale: config.locale,
|
|
428
|
-
acceptLanguage: config.acceptLanguage,
|
|
429
|
-
timezoneId: config.timezoneId,
|
|
430
|
-
timezoneOffset: config.timezoneOffset
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
async function applyContextSettings(context, config, languages, permissions, injectLocaleTimezone) {
|
|
434
|
-
const contextKey = buildContextConfigKey(config);
|
|
435
|
-
const cached = CONTEXT_CONFIG_CACHE.get(context);
|
|
436
|
-
const isFirstInit = !cached;
|
|
437
|
-
const effectiveConfig = cached?.config || config;
|
|
438
|
-
const effectiveLanguages = cached?.languages || languages;
|
|
439
|
-
if (isFirstInit) {
|
|
440
|
-
CONTEXT_CONFIG_CACHE.set(context, {
|
|
441
|
-
key: contextKey,
|
|
442
|
-
config,
|
|
443
|
-
languages
|
|
444
|
-
});
|
|
445
|
-
} else if (cached.key !== contextKey) {
|
|
446
|
-
logger3.warn("applyContext", "Context already initialized; ignore conflicting locale/timezone.");
|
|
447
|
-
}
|
|
448
|
-
await context.setExtraHTTPHeaders({
|
|
449
|
-
"accept-language": effectiveConfig.acceptLanguage
|
|
450
|
-
});
|
|
451
|
-
if (isFirstInit) {
|
|
452
|
-
await context.addInitScript(({ locale, timezoneId, timezoneOffset, languages: languages2, applyLocaleTimezone }) => {
|
|
453
|
-
const originalDateTimeFormat = Intl.DateTimeFormat;
|
|
454
|
-
if (applyLocaleTimezone) {
|
|
455
|
-
Intl.DateTimeFormat = function(locales, initOptions) {
|
|
456
|
-
const nextLocales = locales || locale;
|
|
457
|
-
const nextOptions = initOptions ? { ...initOptions } : {};
|
|
458
|
-
nextOptions.timeZone = nextOptions.timeZone || timezoneId;
|
|
459
|
-
return new originalDateTimeFormat(nextLocales, nextOptions);
|
|
460
|
-
};
|
|
461
|
-
Intl.DateTimeFormat.prototype = originalDateTimeFormat.prototype;
|
|
462
|
-
Date.prototype.getTimezoneOffset = function() {
|
|
463
|
-
return timezoneOffset;
|
|
464
|
-
};
|
|
465
|
-
Object.defineProperty(navigator, "language", { get: () => languages2[0] });
|
|
466
|
-
Object.defineProperty(navigator, "languages", { get: () => languages2 });
|
|
467
|
-
}
|
|
468
|
-
Object.defineProperty(navigator, "webdriver", { get: () => void 0 });
|
|
469
|
-
}, {
|
|
470
|
-
locale: effectiveConfig.locale,
|
|
471
|
-
timezoneId: effectiveConfig.timezoneId,
|
|
472
|
-
timezoneOffset: effectiveConfig.timezoneOffset,
|
|
473
|
-
languages: effectiveLanguages,
|
|
474
|
-
applyLocaleTimezone: injectLocaleTimezone
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
if (effectiveConfig.geolocation) {
|
|
478
|
-
await context.setGeolocation(effectiveConfig.geolocation);
|
|
479
|
-
await context.grantPermissions(["geolocation"]);
|
|
480
|
-
}
|
|
481
|
-
if (permissions?.length) {
|
|
482
|
-
await context.grantPermissions(permissions);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
function resolveConfig(overrides = {}) {
|
|
486
|
-
return {
|
|
487
|
-
...BASE_CONFIG,
|
|
488
|
-
...overrides,
|
|
489
|
-
geolocation: overrides.geolocation === null ? null : overrides.geolocation || BASE_CONFIG.geolocation
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
449
|
var AntiCheat = {
|
|
493
450
|
/**
|
|
494
|
-
*
|
|
451
|
+
* 获取统一的基础配置
|
|
495
452
|
*/
|
|
496
453
|
getBaseConfig() {
|
|
497
454
|
return { ...BASE_CONFIG };
|
|
@@ -508,100 +465,18 @@ var AntiCheat = {
|
|
|
508
465
|
getLaunchArgs() {
|
|
509
466
|
return [...DEFAULT_LAUNCH_ARGS];
|
|
510
467
|
},
|
|
511
|
-
/**
|
|
512
|
-
* 获取增强启动参数(高风险场景)。
|
|
513
|
-
*/
|
|
514
|
-
getAdvancedLaunchArgs() {
|
|
515
|
-
return [...ADVANCED_LAUNCH_ARGS];
|
|
516
|
-
},
|
|
517
|
-
/**
|
|
518
|
-
* 统一应用到 BrowserContext(时区/语言/权限/地理位置)。
|
|
519
|
-
*
|
|
520
|
-
* @param {import('playwright').BrowserContext} context
|
|
521
|
-
* @param {Object} [options]
|
|
522
|
-
* @param {string} [options.locale]
|
|
523
|
-
* @param {string} [options.acceptLanguage]
|
|
524
|
-
* @param {string} [options.timezoneId]
|
|
525
|
-
* @param {number} [options.timezoneOffset]
|
|
526
|
-
* @param {import('playwright').Geolocation|null} [options.geolocation]
|
|
527
|
-
* @param {string[]} [options.permissions]
|
|
528
|
-
*/
|
|
529
|
-
async applyContext(context, options = {}) {
|
|
530
|
-
const config = resolveConfig(options);
|
|
531
|
-
const languages = normalizeLanguages(config.acceptLanguage, config.locale);
|
|
532
|
-
const permissions = Array.isArray(options.permissions) ? options.permissions : [];
|
|
533
|
-
await applyContextSettings(context, config, languages, permissions, true);
|
|
534
|
-
logger3.success("applyContext", `${config.locale} | ${config.timezoneId}`);
|
|
535
|
-
},
|
|
536
|
-
/**
|
|
537
|
-
* 统一应用到 Page(Context + 视口同步)。
|
|
538
|
-
*
|
|
539
|
-
* @param {import('playwright').Page} page
|
|
540
|
-
* @param {Object} [options] - 传递给 applyContext 的选项
|
|
541
|
-
*/
|
|
542
|
-
async applyPage(page, options = {}) {
|
|
543
|
-
const config = resolveConfig(options);
|
|
544
|
-
const languages = normalizeLanguages(config.acceptLanguage, config.locale);
|
|
545
|
-
const permissions = Array.isArray(options.permissions) ? options.permissions : [];
|
|
546
|
-
let injectLocaleTimezone = true;
|
|
547
|
-
try {
|
|
548
|
-
const env = await page.evaluate(() => ({
|
|
549
|
-
language: navigator.language,
|
|
550
|
-
languages: Array.isArray(navigator.languages) ? navigator.languages : [],
|
|
551
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
552
|
-
tzOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset()
|
|
553
|
-
}));
|
|
554
|
-
const languageMatch = env.language === languages[0];
|
|
555
|
-
const timeZoneMatch = env.timeZone === config.timezoneId && env.tzOffset === config.timezoneOffset;
|
|
556
|
-
injectLocaleTimezone = !(languageMatch && timeZoneMatch);
|
|
557
|
-
} catch (e) {
|
|
558
|
-
injectLocaleTimezone = true;
|
|
559
|
-
}
|
|
560
|
-
await applyContextSettings(page.context(), config, languages, permissions, injectLocaleTimezone);
|
|
561
|
-
await this.syncViewportWithScreen(page);
|
|
562
|
-
},
|
|
563
|
-
/**
|
|
564
|
-
* 同步 Page 视口到 window.screen,避免视口/屏幕不一致检测。
|
|
565
|
-
*/
|
|
566
|
-
async syncViewportWithScreen(page) {
|
|
567
|
-
try {
|
|
568
|
-
const screen = await page.evaluate(() => ({
|
|
569
|
-
width: window.screen.width,
|
|
570
|
-
height: window.screen.height
|
|
571
|
-
}));
|
|
572
|
-
await page.setViewportSize({
|
|
573
|
-
width: screen.width,
|
|
574
|
-
height: screen.height
|
|
575
|
-
});
|
|
576
|
-
logger3.success("syncViewport", `size=${screen.width}x${screen.height}`);
|
|
577
|
-
} catch (e) {
|
|
578
|
-
logger3.warn(`syncViewport \u5931\u8D25: ${e.message}\uFF0C\u56DE\u9000\u5230 1920x1080`);
|
|
579
|
-
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
580
|
-
}
|
|
581
|
-
},
|
|
582
468
|
/**
|
|
583
469
|
* 为 got-scraping 生成与浏览器一致的 TLS 指纹配置(桌面端)。
|
|
584
|
-
*
|
|
585
|
-
* @param {string} [userAgent]
|
|
586
470
|
*/
|
|
587
471
|
getTlsFingerprintOptions(userAgent = "", acceptLanguage = "") {
|
|
588
|
-
|
|
589
|
-
const fingerprint = buildFingerprintOptions(primaryLocale);
|
|
590
|
-
const os = getOperatingSystemsFromUserAgent(userAgent);
|
|
591
|
-
if (os.length > 0) fingerprint.operatingSystems = os;
|
|
592
|
-
return fingerprint;
|
|
472
|
+
return buildFingerprintOptions(BASE_CONFIG.locale);
|
|
593
473
|
},
|
|
594
474
|
/**
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
* @param {Record<string, string>} headers
|
|
598
|
-
* @returns {Record<string, string>}
|
|
475
|
+
* 规范化请求头
|
|
599
476
|
*/
|
|
600
477
|
applyLocaleHeaders(headers, acceptLanguage = "") {
|
|
601
|
-
if (
|
|
602
|
-
headers["accept-language"] = acceptLanguage;
|
|
603
|
-
} else if (!headers["accept-language"]) {
|
|
604
|
-
headers["accept-language"] = BASE_CONFIG.acceptLanguage;
|
|
478
|
+
if (!headers["accept-language"]) {
|
|
479
|
+
headers["accept-language"] = acceptLanguage || BASE_CONFIG.acceptLanguage;
|
|
605
480
|
}
|
|
606
481
|
return headers;
|
|
607
482
|
}
|
|
@@ -1037,18 +912,6 @@ var Launch = {
|
|
|
1037
912
|
ignoreDefaultArgs: ["--enable-automation"]
|
|
1038
913
|
};
|
|
1039
914
|
},
|
|
1040
|
-
/**
|
|
1041
|
-
* 获取增强版启动选项(用于高风险反爬场景)
|
|
1042
|
-
*/
|
|
1043
|
-
getAdvancedLaunchOptions(customArgs = []) {
|
|
1044
|
-
return {
|
|
1045
|
-
args: [
|
|
1046
|
-
...AntiCheat.getAdvancedLaunchArgs(),
|
|
1047
|
-
...customArgs
|
|
1048
|
-
],
|
|
1049
|
-
ignoreDefaultArgs: ["--enable-automation"]
|
|
1050
|
-
};
|
|
1051
|
-
},
|
|
1052
915
|
/**
|
|
1053
916
|
* 推荐的 Fingerprint Generator 选项
|
|
1054
917
|
* 确保生成的是桌面端、较新的 Chrome,以匹配我们的脚本逻辑
|
|
@@ -1582,8 +1445,6 @@ var Interception = {
|
|
|
1582
1445
|
try {
|
|
1583
1446
|
const reqHeaders = await request.allHeaders();
|
|
1584
1447
|
delete reqHeaders["host"];
|
|
1585
|
-
const currentAcceptLanguage = reqHeaders["accept-language"] || "";
|
|
1586
|
-
AntiCheat.applyLocaleHeaders(reqHeaders, currentAcceptLanguage);
|
|
1587
1448
|
const resolvedAcceptLanguage = reqHeaders["accept-language"] || "";
|
|
1588
1449
|
const userAgent = reqHeaders["user-agent"] || "";
|
|
1589
1450
|
const method = request.method();
|
|
@@ -1597,8 +1458,8 @@ var Interception = {
|
|
|
1597
1458
|
body: postData,
|
|
1598
1459
|
responseType: "buffer",
|
|
1599
1460
|
// 强制获取 Buffer
|
|
1600
|
-
//
|
|
1601
|
-
headerGeneratorOptions:
|
|
1461
|
+
// 移除手动 TLS 指纹配置,使用 got-scraping 默认的高质量指纹
|
|
1462
|
+
// headerGeneratorOptions: ...
|
|
1602
1463
|
// 使用共享的 Agent 单例(keepAlive: false,不会池化连接)
|
|
1603
1464
|
agent: {
|
|
1604
1465
|
http: SHARED_HTTP_AGENT,
|
|
@@ -1678,6 +1539,181 @@ function isIgnorableError(error) {
|
|
|
1678
1539
|
return msg.includes("already handled") || msg.includes("Target closed") || msg.includes("closed");
|
|
1679
1540
|
}
|
|
1680
1541
|
|
|
1542
|
+
// src/mutation.js
|
|
1543
|
+
var import_uuid2 = require("uuid");
|
|
1544
|
+
var logger9 = createLogger("Mutation");
|
|
1545
|
+
function generateKey(prefix) {
|
|
1546
|
+
return `__${prefix}_${(0, import_uuid2.v4)().replace(/-/g, "_")}`;
|
|
1547
|
+
}
|
|
1548
|
+
var Mutation = {
|
|
1549
|
+
/**
|
|
1550
|
+
* 等待 DOM 元素稳定(无变化)
|
|
1551
|
+
* 使用 MutationObserver 监控指定元素,当元素持续一段时间无变化时 resolve
|
|
1552
|
+
*
|
|
1553
|
+
* @param {import('playwright').Page} page - Playwright page 对象
|
|
1554
|
+
* @param {string | string[]} selectors - 要监控的 CSS 选择器,单个或多个
|
|
1555
|
+
* @param {Object} [options] - 配置选项
|
|
1556
|
+
* @param {number} [options.stableTime] - 无变化持续时间后 resolve (毫秒, 默认: 5000)
|
|
1557
|
+
* @param {number} [options.timeout] - 整体超时时间 (毫秒, 默认: 60000)
|
|
1558
|
+
* @param {Function} [options.onMutation] - 变化时的回调钩子 (mutationCount: number) => void
|
|
1559
|
+
* @returns {Promise<{ mutationCount: number, stableTime: number }>} - 返回变化次数和稳定时长
|
|
1560
|
+
*/
|
|
1561
|
+
async waitForStable(page, selectors, options = {}) {
|
|
1562
|
+
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
1563
|
+
const stableTime = options.stableTime ?? 5e3;
|
|
1564
|
+
const timeout = options.timeout ?? 6e4;
|
|
1565
|
+
const onMutation = options.onMutation;
|
|
1566
|
+
logger9.start("waitForStable", `\u76D1\u63A7 ${selectorList.length} \u4E2A\u9009\u62E9\u5668, \u7A33\u5B9A\u65F6\u95F4=${stableTime}ms`);
|
|
1567
|
+
const eventName = generateKey("pk_mut_evt");
|
|
1568
|
+
const callbackName = generateKey("pk_mut_cb");
|
|
1569
|
+
if (onMutation) {
|
|
1570
|
+
try {
|
|
1571
|
+
await page.exposeFunction(callbackName, (count) => {
|
|
1572
|
+
try {
|
|
1573
|
+
onMutation(count);
|
|
1574
|
+
} catch (e) {
|
|
1575
|
+
}
|
|
1576
|
+
});
|
|
1577
|
+
} catch (e) {
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
const result = await page.evaluate(
|
|
1581
|
+
async ({ selectorList: selectorList2, stableTime: stableTime2, timeout: timeout2, eventName: eventName2, callbackName: callbackName2, hasCallback }) => {
|
|
1582
|
+
return new Promise((resolve, reject) => {
|
|
1583
|
+
let mutationCount = 0;
|
|
1584
|
+
let stableTimer = null;
|
|
1585
|
+
let timeoutTimer = null;
|
|
1586
|
+
const observers = [];
|
|
1587
|
+
const cleanup = () => {
|
|
1588
|
+
observers.forEach((obs) => obs.disconnect());
|
|
1589
|
+
if (stableTimer) clearTimeout(stableTimer);
|
|
1590
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
1591
|
+
};
|
|
1592
|
+
const resetStableTimer = () => {
|
|
1593
|
+
if (stableTimer) clearTimeout(stableTimer);
|
|
1594
|
+
stableTimer = setTimeout(() => {
|
|
1595
|
+
cleanup();
|
|
1596
|
+
resolve({ mutationCount, stableTime: stableTime2 });
|
|
1597
|
+
}, stableTime2);
|
|
1598
|
+
};
|
|
1599
|
+
timeoutTimer = setTimeout(() => {
|
|
1600
|
+
cleanup();
|
|
1601
|
+
reject(new Error(`waitForStable \u8D85\u65F6 (${timeout2}ms), \u5DF2\u68C0\u6D4B\u5230 ${mutationCount} \u6B21\u53D8\u5316`));
|
|
1602
|
+
}, timeout2);
|
|
1603
|
+
selectorList2.forEach((selector) => {
|
|
1604
|
+
const elements = document.querySelectorAll(selector);
|
|
1605
|
+
elements.forEach((element) => {
|
|
1606
|
+
const observer = new MutationObserver((mutations) => {
|
|
1607
|
+
mutationCount += mutations.length;
|
|
1608
|
+
if (hasCallback && window[callbackName2]) {
|
|
1609
|
+
window[callbackName2](mutationCount);
|
|
1610
|
+
}
|
|
1611
|
+
resetStableTimer();
|
|
1612
|
+
});
|
|
1613
|
+
observer.observe(element, {
|
|
1614
|
+
childList: true,
|
|
1615
|
+
subtree: true,
|
|
1616
|
+
characterData: true,
|
|
1617
|
+
attributes: true
|
|
1618
|
+
});
|
|
1619
|
+
observers.push(observer);
|
|
1620
|
+
});
|
|
1621
|
+
});
|
|
1622
|
+
if (observers.length === 0) {
|
|
1623
|
+
cleanup();
|
|
1624
|
+
resolve({ mutationCount: 0, stableTime: 0 });
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
resetStableTimer();
|
|
1628
|
+
});
|
|
1629
|
+
},
|
|
1630
|
+
{ selectorList, stableTime, timeout, eventName, callbackName, hasCallback: !!onMutation }
|
|
1631
|
+
);
|
|
1632
|
+
logger9.success("waitForStable", `DOM \u7A33\u5B9A, \u603B\u5171 ${result.mutationCount} \u6B21\u53D8\u5316`);
|
|
1633
|
+
return result;
|
|
1634
|
+
},
|
|
1635
|
+
/**
|
|
1636
|
+
* 创建一个持续监控 DOM 变化的监控器
|
|
1637
|
+
*
|
|
1638
|
+
* @param {import('playwright').Page} page - Playwright page 对象
|
|
1639
|
+
* @param {string | string[]} selectors - 要监控的 CSS 选择器
|
|
1640
|
+
* @param {Object} [options] - 配置选项
|
|
1641
|
+
* @param {Function} [options.onMutation] - 变化时的回调 (mutationCount: number) => void
|
|
1642
|
+
* @returns {Promise<{ stop: () => Promise<{ totalMutations: number }> }>} - 返回停止函数
|
|
1643
|
+
*/
|
|
1644
|
+
async createMonitor(page, selectors, options = {}) {
|
|
1645
|
+
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
1646
|
+
const onMutation = options.onMutation;
|
|
1647
|
+
logger9.start("createMonitor", `\u76D1\u63A7 ${selectorList.length} \u4E2A\u9009\u62E9\u5668`);
|
|
1648
|
+
const monitorKey = generateKey("pk_mon");
|
|
1649
|
+
const callbackName = generateKey("pk_mon_cb");
|
|
1650
|
+
const cleanerName = generateKey("pk_mon_clean");
|
|
1651
|
+
if (onMutation) {
|
|
1652
|
+
try {
|
|
1653
|
+
await page.exposeFunction(callbackName, (count) => {
|
|
1654
|
+
try {
|
|
1655
|
+
onMutation(count);
|
|
1656
|
+
} catch (e) {
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
} catch (e) {
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
await page.evaluate(({ selectorList: selectorList2, monitorKey: monitorKey2, callbackName: callbackName2, cleanerName: cleanerName2, hasCallback }) => {
|
|
1663
|
+
const monitor = {
|
|
1664
|
+
observers: [],
|
|
1665
|
+
totalMutations: 0,
|
|
1666
|
+
running: true
|
|
1667
|
+
};
|
|
1668
|
+
selectorList2.forEach((selector) => {
|
|
1669
|
+
const elements = document.querySelectorAll(selector);
|
|
1670
|
+
elements.forEach((element) => {
|
|
1671
|
+
const observer = new MutationObserver((mutations) => {
|
|
1672
|
+
if (!monitor.running) return;
|
|
1673
|
+
monitor.totalMutations += mutations.length;
|
|
1674
|
+
if (hasCallback && window[callbackName2]) {
|
|
1675
|
+
window[callbackName2](monitor.totalMutations);
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
observer.observe(element, {
|
|
1679
|
+
childList: true,
|
|
1680
|
+
subtree: true,
|
|
1681
|
+
characterData: true,
|
|
1682
|
+
attributes: true
|
|
1683
|
+
});
|
|
1684
|
+
monitor.observers.push(observer);
|
|
1685
|
+
});
|
|
1686
|
+
});
|
|
1687
|
+
window[monitorKey2] = monitor;
|
|
1688
|
+
window[cleanerName2] = () => {
|
|
1689
|
+
monitor.running = false;
|
|
1690
|
+
monitor.observers.forEach((obs) => obs.disconnect());
|
|
1691
|
+
const total = monitor.totalMutations;
|
|
1692
|
+
delete window[monitorKey2];
|
|
1693
|
+
delete window[cleanerName2];
|
|
1694
|
+
return total;
|
|
1695
|
+
};
|
|
1696
|
+
}, { selectorList, monitorKey, callbackName, cleanerName, hasCallback: !!onMutation });
|
|
1697
|
+
logger9.success("createMonitor", "\u76D1\u63A7\u5668\u5DF2\u542F\u52A8");
|
|
1698
|
+
return {
|
|
1699
|
+
stop: async () => {
|
|
1700
|
+
let totalMutations = 0;
|
|
1701
|
+
try {
|
|
1702
|
+
totalMutations = await page.evaluate((cleanerName2) => {
|
|
1703
|
+
if (window[cleanerName2]) {
|
|
1704
|
+
return window[cleanerName2]();
|
|
1705
|
+
}
|
|
1706
|
+
return 0;
|
|
1707
|
+
}, cleanerName);
|
|
1708
|
+
} catch (e) {
|
|
1709
|
+
}
|
|
1710
|
+
logger9.success("createMonitor.stop", `\u76D1\u63A7\u5DF2\u505C\u6B62, \u5171 ${totalMutations} \u6B21\u53D8\u5316`);
|
|
1711
|
+
return { totalMutations };
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1681
1717
|
// index.js
|
|
1682
1718
|
var usePlaywrightToolKit = () => {
|
|
1683
1719
|
return {
|
|
@@ -1691,7 +1727,8 @@ var usePlaywrightToolKit = () => {
|
|
|
1691
1727
|
Captcha,
|
|
1692
1728
|
Sse,
|
|
1693
1729
|
Errors: errors_exports,
|
|
1694
|
-
Interception
|
|
1730
|
+
Interception,
|
|
1731
|
+
Mutation
|
|
1695
1732
|
};
|
|
1696
1733
|
};
|
|
1697
1734
|
// Annotate the CommonJS export names for ESM import in node:
|