@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/dist/index.js
CHANGED
|
@@ -154,38 +154,87 @@ async function createApifyKit() {
|
|
|
154
154
|
const { Actor: Actor2 } = apify;
|
|
155
155
|
return {
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
157
|
+
* 核心封装:执行步骤,带自动日志确认、失败截图处理和重试机制
|
|
158
|
+
*
|
|
159
|
+
* @param {string} step - 步骤名称
|
|
160
|
+
* @param {import('playwright').Page} page - Playwright page 对象
|
|
161
|
+
* @param {Function} actionFn - 执行的异步操作
|
|
162
|
+
* @param {Object} [options] - 配置选项
|
|
163
|
+
* @param {boolean} [options.failActor=true] - 失败时是否调用 Actor.fail
|
|
164
|
+
* @param {Object} [options.retry] - 重试配置
|
|
165
|
+
* @param {number} [options.retry.times=0] - 重试次数
|
|
166
|
+
* @param {'direct'|'refresh'} [options.retry.mode='direct'] - 重试模式
|
|
167
|
+
* @param {Function} [options.retry.before] - 重试前钩子,可覆盖默认等待行为
|
|
158
168
|
*/
|
|
159
169
|
async runStep(step, page, actionFn, options = {}) {
|
|
160
|
-
const { failActor = true } = options;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
logger.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
});
|
|
184
|
-
await Actor2.fail(`Run Step ${step} \u5931\u8D25: ${error.message}`);
|
|
170
|
+
const { failActor = true, retry = {} } = options;
|
|
171
|
+
const { times: retryTimes = 0, mode: retryMode = "direct", before: beforeRetry } = retry;
|
|
172
|
+
const executeAction = async (attemptNumber) => {
|
|
173
|
+
const attemptLabel = attemptNumber > 0 ? ` (\u91CD\u8BD5 #${attemptNumber})` : "";
|
|
174
|
+
logger.start(`[Step] ${step}${attemptLabel}`);
|
|
175
|
+
try {
|
|
176
|
+
const result = await actionFn();
|
|
177
|
+
logger.success(`[Step] ${step}${attemptLabel}`);
|
|
178
|
+
return { success: true, result };
|
|
179
|
+
} catch (error) {
|
|
180
|
+
logger.fail(`[Step] ${step}${attemptLabel}`, error);
|
|
181
|
+
return { success: false, error };
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const prepareForRetry = async (attemptNumber) => {
|
|
185
|
+
if (typeof beforeRetry === "function") {
|
|
186
|
+
logger.start(`[RetryStep] \u6267\u884C\u81EA\u5B9A\u4E49 before \u94A9\u5B50 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
|
|
187
|
+
await beforeRetry(page, attemptNumber);
|
|
188
|
+
logger.success(`[RetryStep] before \u94A9\u5B50\u5B8C\u6210`);
|
|
189
|
+
} else if (retryMode === "refresh") {
|
|
190
|
+
logger.start(`[RetryStep] \u5237\u65B0\u9875\u9762 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
|
|
191
|
+
await page.reload({ waitUntil: "domcontentloaded" });
|
|
192
|
+
logger.success(`[RetryStep] \u9875\u9762\u5237\u65B0\u5B8C\u6210`);
|
|
185
193
|
} else {
|
|
186
|
-
|
|
194
|
+
logger.start(`[RetryStep] \u7B49\u5F85 3 \u79D2 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
196
|
+
logger.success(`[RetryStep] \u7B49\u5F85\u5B8C\u6210`);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
let lastResult = await executeAction(0);
|
|
200
|
+
if (lastResult.success) {
|
|
201
|
+
return lastResult.result;
|
|
202
|
+
}
|
|
203
|
+
for (let attempt = 1; attempt <= retryTimes; attempt++) {
|
|
204
|
+
logger.start(`[RetryStep] \u51C6\u5907\u7B2C ${attempt}/${retryTimes} \u6B21\u91CD\u8BD5: ${step}`);
|
|
205
|
+
try {
|
|
206
|
+
await prepareForRetry(attempt);
|
|
207
|
+
} catch (prepareError) {
|
|
208
|
+
logger.warn(`[RetryStep] \u91CD\u8BD5\u51C6\u5907\u5931\u8D25: ${prepareError.message}`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
lastResult = await executeAction(attempt);
|
|
212
|
+
if (lastResult.success) {
|
|
213
|
+
return lastResult.result;
|
|
187
214
|
}
|
|
188
215
|
}
|
|
216
|
+
const finalError = lastResult.error;
|
|
217
|
+
if (failActor) {
|
|
218
|
+
let base64 = "\u622A\u56FE\u5931\u8D25";
|
|
219
|
+
try {
|
|
220
|
+
if (page) {
|
|
221
|
+
const buffer = await page.screenshot({ fullPage: true, type: "jpeg", quality: 60 });
|
|
222
|
+
base64 = `data:image/jpeg;base64,${buffer.toString("base64")}`;
|
|
223
|
+
}
|
|
224
|
+
} catch (snapErr) {
|
|
225
|
+
logger.warn(`\u622A\u56FE\u751F\u6210\u5931\u8D25: ${snapErr.message}`);
|
|
226
|
+
}
|
|
227
|
+
await this.pushFailed(finalError, {
|
|
228
|
+
step,
|
|
229
|
+
page,
|
|
230
|
+
options,
|
|
231
|
+
base64,
|
|
232
|
+
retryAttempts: retryTimes
|
|
233
|
+
});
|
|
234
|
+
await Actor2.fail(`Run Step ${step} \u5931\u8D25 (\u5DF2\u91CD\u8BD5 ${retryTimes} \u6B21): ${finalError.message}`);
|
|
235
|
+
} else {
|
|
236
|
+
throw finalError;
|
|
237
|
+
}
|
|
189
238
|
},
|
|
190
239
|
/**
|
|
191
240
|
* 宽松版runStep:失败时不调用Actor.fail,只抛出异常
|
|
@@ -279,6 +328,8 @@ var Utils = {
|
|
|
279
328
|
* @param {import('playwright').Page} page - Playwright page 对象
|
|
280
329
|
* @param {Object} [options] - 配置选项
|
|
281
330
|
* @param {number} [options.buffer] - 额外缓冲高度 (默认: 视口高度的一半)
|
|
331
|
+
* @param {boolean} [options.restore] - 截图后是否恢复页面高度和样式 (默认: false)
|
|
332
|
+
* @param {number} [options.maxHeight] - 最大截图高度 (默认: 8000px)
|
|
282
333
|
* @returns {Promise<string>} - base64 编码的 PNG 图片
|
|
283
334
|
*/
|
|
284
335
|
async fullPageScreenshot(page, options = {}) {
|
|
@@ -286,15 +337,17 @@ var Utils = {
|
|
|
286
337
|
const originalViewport = page.viewportSize();
|
|
287
338
|
const defaultBuffer = Math.round((originalViewport?.height || 1080) / 2);
|
|
288
339
|
const buffer = options.buffer ?? defaultBuffer;
|
|
340
|
+
const restore = options.restore ?? false;
|
|
341
|
+
const maxHeight = options.maxHeight ?? 8e3;
|
|
289
342
|
try {
|
|
290
343
|
const maxScrollHeight = await page.evaluate(() => {
|
|
291
|
-
let
|
|
344
|
+
let maxHeight2 = document.body.scrollHeight;
|
|
292
345
|
document.querySelectorAll("*").forEach((el) => {
|
|
293
346
|
const style = window.getComputedStyle(el);
|
|
294
347
|
const overflowY = style.overflowY;
|
|
295
348
|
if ((overflowY === "auto" || overflowY === "scroll") && el.scrollHeight > el.clientHeight) {
|
|
296
|
-
if (el.scrollHeight >
|
|
297
|
-
|
|
349
|
+
if (el.scrollHeight > maxHeight2) {
|
|
350
|
+
maxHeight2 = el.scrollHeight;
|
|
298
351
|
}
|
|
299
352
|
el.dataset.pkOrigOverflow = el.style.overflow;
|
|
300
353
|
el.dataset.pkOrigHeight = el.style.height;
|
|
@@ -305,11 +358,12 @@ var Utils = {
|
|
|
305
358
|
el.style.maxHeight = "none";
|
|
306
359
|
}
|
|
307
360
|
});
|
|
308
|
-
return
|
|
361
|
+
return maxHeight2;
|
|
309
362
|
});
|
|
363
|
+
const targetHeight = Math.min(maxScrollHeight + buffer, maxHeight);
|
|
310
364
|
await page.setViewportSize({
|
|
311
365
|
width: originalViewport?.width || 1280,
|
|
312
|
-
height:
|
|
366
|
+
height: targetHeight
|
|
313
367
|
});
|
|
314
368
|
await delay(1e3);
|
|
315
369
|
const buffer_ = await page.screenshot({
|
|
@@ -319,19 +373,21 @@ var Utils = {
|
|
|
319
373
|
logger2.success("fullPageScreenshot", `captured ${Math.round(buffer_.length / 1024)} KB`);
|
|
320
374
|
return buffer_.toString("base64");
|
|
321
375
|
} finally {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
376
|
+
if (restore) {
|
|
377
|
+
await page.evaluate(() => {
|
|
378
|
+
document.querySelectorAll(".__pk_expanded__").forEach((el) => {
|
|
379
|
+
el.style.overflow = el.dataset.pkOrigOverflow || "";
|
|
380
|
+
el.style.height = el.dataset.pkOrigHeight || "";
|
|
381
|
+
el.style.maxHeight = el.dataset.pkOrigMaxHeight || "";
|
|
382
|
+
delete el.dataset.pkOrigOverflow;
|
|
383
|
+
delete el.dataset.pkOrigHeight;
|
|
384
|
+
delete el.dataset.pkOrigMaxHeight;
|
|
385
|
+
el.classList.remove("__pk_expanded__");
|
|
386
|
+
});
|
|
331
387
|
});
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
388
|
+
if (originalViewport) {
|
|
389
|
+
await page.setViewportSize(originalViewport);
|
|
390
|
+
}
|
|
335
391
|
}
|
|
336
392
|
}
|
|
337
393
|
}
|
|
@@ -347,25 +403,12 @@ var BASE_CONFIG = Object.freeze({
|
|
|
347
403
|
geolocation: null
|
|
348
404
|
});
|
|
349
405
|
var DEFAULT_LAUNCH_ARGS = [
|
|
350
|
-
|
|
406
|
+
// '--disable-blink-features=AutomationControlled', // Crawlee 可能会自动处理,过多干预反而会被识别
|
|
351
407
|
"--no-sandbox",
|
|
352
408
|
"--disable-setuid-sandbox",
|
|
353
409
|
"--window-position=0,0",
|
|
354
410
|
`--lang=${BASE_CONFIG.locale}`
|
|
355
411
|
];
|
|
356
|
-
var ADVANCED_LAUNCH_ARGS = [
|
|
357
|
-
...DEFAULT_LAUNCH_ARGS,
|
|
358
|
-
"--disable-dev-shm-usage",
|
|
359
|
-
"--disable-background-networking",
|
|
360
|
-
"--disable-default-apps",
|
|
361
|
-
"--disable-extensions",
|
|
362
|
-
"--disable-sync",
|
|
363
|
-
"--disable-translate",
|
|
364
|
-
"--metrics-recording-only",
|
|
365
|
-
"--mute-audio",
|
|
366
|
-
"--no-first-run"
|
|
367
|
-
];
|
|
368
|
-
var CONTEXT_CONFIG_CACHE = /* @__PURE__ */ new WeakMap();
|
|
369
412
|
function buildFingerprintOptions(locale) {
|
|
370
413
|
return {
|
|
371
414
|
browsers: [{ name: "chrome", minVersion: 110 }],
|
|
@@ -374,95 +417,9 @@ function buildFingerprintOptions(locale) {
|
|
|
374
417
|
locales: [locale]
|
|
375
418
|
};
|
|
376
419
|
}
|
|
377
|
-
function parseAcceptLanguage(acceptLanguage) {
|
|
378
|
-
if (!acceptLanguage) return [];
|
|
379
|
-
return acceptLanguage.split(",").map((part) => part.trim().split(";")[0]).filter(Boolean);
|
|
380
|
-
}
|
|
381
|
-
function normalizeLanguages(acceptLanguage, fallbackLocale) {
|
|
382
|
-
const languages = parseAcceptLanguage(acceptLanguage);
|
|
383
|
-
if (languages.length === 0) return [fallbackLocale];
|
|
384
|
-
if (!languages.includes(fallbackLocale)) {
|
|
385
|
-
return [fallbackLocale, ...languages];
|
|
386
|
-
}
|
|
387
|
-
return languages;
|
|
388
|
-
}
|
|
389
|
-
function getOperatingSystemsFromUserAgent(userAgent) {
|
|
390
|
-
const lowerUA = userAgent.toLowerCase();
|
|
391
|
-
if (lowerUA.includes("windows")) return ["windows"];
|
|
392
|
-
if (lowerUA.includes("mac os") || lowerUA.includes("macintosh")) return ["macos"];
|
|
393
|
-
if (lowerUA.includes("linux")) return ["linux"];
|
|
394
|
-
return [];
|
|
395
|
-
}
|
|
396
|
-
function buildContextConfigKey(config) {
|
|
397
|
-
return JSON.stringify({
|
|
398
|
-
locale: config.locale,
|
|
399
|
-
acceptLanguage: config.acceptLanguage,
|
|
400
|
-
timezoneId: config.timezoneId,
|
|
401
|
-
timezoneOffset: config.timezoneOffset
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
async function applyContextSettings(context, config, languages, permissions, injectLocaleTimezone) {
|
|
405
|
-
const contextKey = buildContextConfigKey(config);
|
|
406
|
-
const cached = CONTEXT_CONFIG_CACHE.get(context);
|
|
407
|
-
const isFirstInit = !cached;
|
|
408
|
-
const effectiveConfig = cached?.config || config;
|
|
409
|
-
const effectiveLanguages = cached?.languages || languages;
|
|
410
|
-
if (isFirstInit) {
|
|
411
|
-
CONTEXT_CONFIG_CACHE.set(context, {
|
|
412
|
-
key: contextKey,
|
|
413
|
-
config,
|
|
414
|
-
languages
|
|
415
|
-
});
|
|
416
|
-
} else if (cached.key !== contextKey) {
|
|
417
|
-
logger3.warn("applyContext", "Context already initialized; ignore conflicting locale/timezone.");
|
|
418
|
-
}
|
|
419
|
-
await context.setExtraHTTPHeaders({
|
|
420
|
-
"accept-language": effectiveConfig.acceptLanguage
|
|
421
|
-
});
|
|
422
|
-
if (isFirstInit) {
|
|
423
|
-
await context.addInitScript(({ locale, timezoneId, timezoneOffset, languages: languages2, applyLocaleTimezone }) => {
|
|
424
|
-
const originalDateTimeFormat = Intl.DateTimeFormat;
|
|
425
|
-
if (applyLocaleTimezone) {
|
|
426
|
-
Intl.DateTimeFormat = function(locales, initOptions) {
|
|
427
|
-
const nextLocales = locales || locale;
|
|
428
|
-
const nextOptions = initOptions ? { ...initOptions } : {};
|
|
429
|
-
nextOptions.timeZone = nextOptions.timeZone || timezoneId;
|
|
430
|
-
return new originalDateTimeFormat(nextLocales, nextOptions);
|
|
431
|
-
};
|
|
432
|
-
Intl.DateTimeFormat.prototype = originalDateTimeFormat.prototype;
|
|
433
|
-
Date.prototype.getTimezoneOffset = function() {
|
|
434
|
-
return timezoneOffset;
|
|
435
|
-
};
|
|
436
|
-
Object.defineProperty(navigator, "language", { get: () => languages2[0] });
|
|
437
|
-
Object.defineProperty(navigator, "languages", { get: () => languages2 });
|
|
438
|
-
}
|
|
439
|
-
Object.defineProperty(navigator, "webdriver", { get: () => void 0 });
|
|
440
|
-
}, {
|
|
441
|
-
locale: effectiveConfig.locale,
|
|
442
|
-
timezoneId: effectiveConfig.timezoneId,
|
|
443
|
-
timezoneOffset: effectiveConfig.timezoneOffset,
|
|
444
|
-
languages: effectiveLanguages,
|
|
445
|
-
applyLocaleTimezone: injectLocaleTimezone
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
if (effectiveConfig.geolocation) {
|
|
449
|
-
await context.setGeolocation(effectiveConfig.geolocation);
|
|
450
|
-
await context.grantPermissions(["geolocation"]);
|
|
451
|
-
}
|
|
452
|
-
if (permissions?.length) {
|
|
453
|
-
await context.grantPermissions(permissions);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
function resolveConfig(overrides = {}) {
|
|
457
|
-
return {
|
|
458
|
-
...BASE_CONFIG,
|
|
459
|
-
...overrides,
|
|
460
|
-
geolocation: overrides.geolocation === null ? null : overrides.geolocation || BASE_CONFIG.geolocation
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
420
|
var AntiCheat = {
|
|
464
421
|
/**
|
|
465
|
-
*
|
|
422
|
+
* 获取统一的基础配置
|
|
466
423
|
*/
|
|
467
424
|
getBaseConfig() {
|
|
468
425
|
return { ...BASE_CONFIG };
|
|
@@ -479,100 +436,18 @@ var AntiCheat = {
|
|
|
479
436
|
getLaunchArgs() {
|
|
480
437
|
return [...DEFAULT_LAUNCH_ARGS];
|
|
481
438
|
},
|
|
482
|
-
/**
|
|
483
|
-
* 获取增强启动参数(高风险场景)。
|
|
484
|
-
*/
|
|
485
|
-
getAdvancedLaunchArgs() {
|
|
486
|
-
return [...ADVANCED_LAUNCH_ARGS];
|
|
487
|
-
},
|
|
488
|
-
/**
|
|
489
|
-
* 统一应用到 BrowserContext(时区/语言/权限/地理位置)。
|
|
490
|
-
*
|
|
491
|
-
* @param {import('playwright').BrowserContext} context
|
|
492
|
-
* @param {Object} [options]
|
|
493
|
-
* @param {string} [options.locale]
|
|
494
|
-
* @param {string} [options.acceptLanguage]
|
|
495
|
-
* @param {string} [options.timezoneId]
|
|
496
|
-
* @param {number} [options.timezoneOffset]
|
|
497
|
-
* @param {import('playwright').Geolocation|null} [options.geolocation]
|
|
498
|
-
* @param {string[]} [options.permissions]
|
|
499
|
-
*/
|
|
500
|
-
async applyContext(context, options = {}) {
|
|
501
|
-
const config = resolveConfig(options);
|
|
502
|
-
const languages = normalizeLanguages(config.acceptLanguage, config.locale);
|
|
503
|
-
const permissions = Array.isArray(options.permissions) ? options.permissions : [];
|
|
504
|
-
await applyContextSettings(context, config, languages, permissions, true);
|
|
505
|
-
logger3.success("applyContext", `${config.locale} | ${config.timezoneId}`);
|
|
506
|
-
},
|
|
507
|
-
/**
|
|
508
|
-
* 统一应用到 Page(Context + 视口同步)。
|
|
509
|
-
*
|
|
510
|
-
* @param {import('playwright').Page} page
|
|
511
|
-
* @param {Object} [options] - 传递给 applyContext 的选项
|
|
512
|
-
*/
|
|
513
|
-
async applyPage(page, options = {}) {
|
|
514
|
-
const config = resolveConfig(options);
|
|
515
|
-
const languages = normalizeLanguages(config.acceptLanguage, config.locale);
|
|
516
|
-
const permissions = Array.isArray(options.permissions) ? options.permissions : [];
|
|
517
|
-
let injectLocaleTimezone = true;
|
|
518
|
-
try {
|
|
519
|
-
const env = await page.evaluate(() => ({
|
|
520
|
-
language: navigator.language,
|
|
521
|
-
languages: Array.isArray(navigator.languages) ? navigator.languages : [],
|
|
522
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
523
|
-
tzOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset()
|
|
524
|
-
}));
|
|
525
|
-
const languageMatch = env.language === languages[0];
|
|
526
|
-
const timeZoneMatch = env.timeZone === config.timezoneId && env.tzOffset === config.timezoneOffset;
|
|
527
|
-
injectLocaleTimezone = !(languageMatch && timeZoneMatch);
|
|
528
|
-
} catch (e) {
|
|
529
|
-
injectLocaleTimezone = true;
|
|
530
|
-
}
|
|
531
|
-
await applyContextSettings(page.context(), config, languages, permissions, injectLocaleTimezone);
|
|
532
|
-
await this.syncViewportWithScreen(page);
|
|
533
|
-
},
|
|
534
|
-
/**
|
|
535
|
-
* 同步 Page 视口到 window.screen,避免视口/屏幕不一致检测。
|
|
536
|
-
*/
|
|
537
|
-
async syncViewportWithScreen(page) {
|
|
538
|
-
try {
|
|
539
|
-
const screen = await page.evaluate(() => ({
|
|
540
|
-
width: window.screen.width,
|
|
541
|
-
height: window.screen.height
|
|
542
|
-
}));
|
|
543
|
-
await page.setViewportSize({
|
|
544
|
-
width: screen.width,
|
|
545
|
-
height: screen.height
|
|
546
|
-
});
|
|
547
|
-
logger3.success("syncViewport", `size=${screen.width}x${screen.height}`);
|
|
548
|
-
} catch (e) {
|
|
549
|
-
logger3.warn(`syncViewport \u5931\u8D25: ${e.message}\uFF0C\u56DE\u9000\u5230 1920x1080`);
|
|
550
|
-
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
551
|
-
}
|
|
552
|
-
},
|
|
553
439
|
/**
|
|
554
440
|
* 为 got-scraping 生成与浏览器一致的 TLS 指纹配置(桌面端)。
|
|
555
|
-
*
|
|
556
|
-
* @param {string} [userAgent]
|
|
557
441
|
*/
|
|
558
442
|
getTlsFingerprintOptions(userAgent = "", acceptLanguage = "") {
|
|
559
|
-
|
|
560
|
-
const fingerprint = buildFingerprintOptions(primaryLocale);
|
|
561
|
-
const os = getOperatingSystemsFromUserAgent(userAgent);
|
|
562
|
-
if (os.length > 0) fingerprint.operatingSystems = os;
|
|
563
|
-
return fingerprint;
|
|
443
|
+
return buildFingerprintOptions(BASE_CONFIG.locale);
|
|
564
444
|
},
|
|
565
445
|
/**
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
* @param {Record<string, string>} headers
|
|
569
|
-
* @returns {Record<string, string>}
|
|
446
|
+
* 规范化请求头
|
|
570
447
|
*/
|
|
571
448
|
applyLocaleHeaders(headers, acceptLanguage = "") {
|
|
572
|
-
if (
|
|
573
|
-
headers["accept-language"] = acceptLanguage;
|
|
574
|
-
} else if (!headers["accept-language"]) {
|
|
575
|
-
headers["accept-language"] = BASE_CONFIG.acceptLanguage;
|
|
449
|
+
if (!headers["accept-language"]) {
|
|
450
|
+
headers["accept-language"] = acceptLanguage || BASE_CONFIG.acceptLanguage;
|
|
576
451
|
}
|
|
577
452
|
return headers;
|
|
578
453
|
}
|
|
@@ -1008,18 +883,6 @@ var Launch = {
|
|
|
1008
883
|
ignoreDefaultArgs: ["--enable-automation"]
|
|
1009
884
|
};
|
|
1010
885
|
},
|
|
1011
|
-
/**
|
|
1012
|
-
* 获取增强版启动选项(用于高风险反爬场景)
|
|
1013
|
-
*/
|
|
1014
|
-
getAdvancedLaunchOptions(customArgs = []) {
|
|
1015
|
-
return {
|
|
1016
|
-
args: [
|
|
1017
|
-
...AntiCheat.getAdvancedLaunchArgs(),
|
|
1018
|
-
...customArgs
|
|
1019
|
-
],
|
|
1020
|
-
ignoreDefaultArgs: ["--enable-automation"]
|
|
1021
|
-
};
|
|
1022
|
-
},
|
|
1023
886
|
/**
|
|
1024
887
|
* 推荐的 Fingerprint Generator 选项
|
|
1025
888
|
* 确保生成的是桌面端、较新的 Chrome,以匹配我们的脚本逻辑
|
|
@@ -1553,8 +1416,6 @@ var Interception = {
|
|
|
1553
1416
|
try {
|
|
1554
1417
|
const reqHeaders = await request.allHeaders();
|
|
1555
1418
|
delete reqHeaders["host"];
|
|
1556
|
-
const currentAcceptLanguage = reqHeaders["accept-language"] || "";
|
|
1557
|
-
AntiCheat.applyLocaleHeaders(reqHeaders, currentAcceptLanguage);
|
|
1558
1419
|
const resolvedAcceptLanguage = reqHeaders["accept-language"] || "";
|
|
1559
1420
|
const userAgent = reqHeaders["user-agent"] || "";
|
|
1560
1421
|
const method = request.method();
|
|
@@ -1568,8 +1429,8 @@ var Interception = {
|
|
|
1568
1429
|
body: postData,
|
|
1569
1430
|
responseType: "buffer",
|
|
1570
1431
|
// 强制获取 Buffer
|
|
1571
|
-
//
|
|
1572
|
-
headerGeneratorOptions:
|
|
1432
|
+
// 移除手动 TLS 指纹配置,使用 got-scraping 默认的高质量指纹
|
|
1433
|
+
// headerGeneratorOptions: ...
|
|
1573
1434
|
// 使用共享的 Agent 单例(keepAlive: false,不会池化连接)
|
|
1574
1435
|
agent: {
|
|
1575
1436
|
http: SHARED_HTTP_AGENT,
|
|
@@ -1649,6 +1510,181 @@ function isIgnorableError(error) {
|
|
|
1649
1510
|
return msg.includes("already handled") || msg.includes("Target closed") || msg.includes("closed");
|
|
1650
1511
|
}
|
|
1651
1512
|
|
|
1513
|
+
// src/mutation.js
|
|
1514
|
+
import { v4 as uuidv42 } from "uuid";
|
|
1515
|
+
var logger9 = createLogger("Mutation");
|
|
1516
|
+
function generateKey(prefix) {
|
|
1517
|
+
return `__${prefix}_${uuidv42().replace(/-/g, "_")}`;
|
|
1518
|
+
}
|
|
1519
|
+
var Mutation = {
|
|
1520
|
+
/**
|
|
1521
|
+
* 等待 DOM 元素稳定(无变化)
|
|
1522
|
+
* 使用 MutationObserver 监控指定元素,当元素持续一段时间无变化时 resolve
|
|
1523
|
+
*
|
|
1524
|
+
* @param {import('playwright').Page} page - Playwright page 对象
|
|
1525
|
+
* @param {string | string[]} selectors - 要监控的 CSS 选择器,单个或多个
|
|
1526
|
+
* @param {Object} [options] - 配置选项
|
|
1527
|
+
* @param {number} [options.stableTime] - 无变化持续时间后 resolve (毫秒, 默认: 5000)
|
|
1528
|
+
* @param {number} [options.timeout] - 整体超时时间 (毫秒, 默认: 60000)
|
|
1529
|
+
* @param {Function} [options.onMutation] - 变化时的回调钩子 (mutationCount: number) => void
|
|
1530
|
+
* @returns {Promise<{ mutationCount: number, stableTime: number }>} - 返回变化次数和稳定时长
|
|
1531
|
+
*/
|
|
1532
|
+
async waitForStable(page, selectors, options = {}) {
|
|
1533
|
+
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
1534
|
+
const stableTime = options.stableTime ?? 5e3;
|
|
1535
|
+
const timeout = options.timeout ?? 6e4;
|
|
1536
|
+
const onMutation = options.onMutation;
|
|
1537
|
+
logger9.start("waitForStable", `\u76D1\u63A7 ${selectorList.length} \u4E2A\u9009\u62E9\u5668, \u7A33\u5B9A\u65F6\u95F4=${stableTime}ms`);
|
|
1538
|
+
const eventName = generateKey("pk_mut_evt");
|
|
1539
|
+
const callbackName = generateKey("pk_mut_cb");
|
|
1540
|
+
if (onMutation) {
|
|
1541
|
+
try {
|
|
1542
|
+
await page.exposeFunction(callbackName, (count) => {
|
|
1543
|
+
try {
|
|
1544
|
+
onMutation(count);
|
|
1545
|
+
} catch (e) {
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
} catch (e) {
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const result = await page.evaluate(
|
|
1552
|
+
async ({ selectorList: selectorList2, stableTime: stableTime2, timeout: timeout2, eventName: eventName2, callbackName: callbackName2, hasCallback }) => {
|
|
1553
|
+
return new Promise((resolve, reject) => {
|
|
1554
|
+
let mutationCount = 0;
|
|
1555
|
+
let stableTimer = null;
|
|
1556
|
+
let timeoutTimer = null;
|
|
1557
|
+
const observers = [];
|
|
1558
|
+
const cleanup = () => {
|
|
1559
|
+
observers.forEach((obs) => obs.disconnect());
|
|
1560
|
+
if (stableTimer) clearTimeout(stableTimer);
|
|
1561
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
1562
|
+
};
|
|
1563
|
+
const resetStableTimer = () => {
|
|
1564
|
+
if (stableTimer) clearTimeout(stableTimer);
|
|
1565
|
+
stableTimer = setTimeout(() => {
|
|
1566
|
+
cleanup();
|
|
1567
|
+
resolve({ mutationCount, stableTime: stableTime2 });
|
|
1568
|
+
}, stableTime2);
|
|
1569
|
+
};
|
|
1570
|
+
timeoutTimer = setTimeout(() => {
|
|
1571
|
+
cleanup();
|
|
1572
|
+
reject(new Error(`waitForStable \u8D85\u65F6 (${timeout2}ms), \u5DF2\u68C0\u6D4B\u5230 ${mutationCount} \u6B21\u53D8\u5316`));
|
|
1573
|
+
}, timeout2);
|
|
1574
|
+
selectorList2.forEach((selector) => {
|
|
1575
|
+
const elements = document.querySelectorAll(selector);
|
|
1576
|
+
elements.forEach((element) => {
|
|
1577
|
+
const observer = new MutationObserver((mutations) => {
|
|
1578
|
+
mutationCount += mutations.length;
|
|
1579
|
+
if (hasCallback && window[callbackName2]) {
|
|
1580
|
+
window[callbackName2](mutationCount);
|
|
1581
|
+
}
|
|
1582
|
+
resetStableTimer();
|
|
1583
|
+
});
|
|
1584
|
+
observer.observe(element, {
|
|
1585
|
+
childList: true,
|
|
1586
|
+
subtree: true,
|
|
1587
|
+
characterData: true,
|
|
1588
|
+
attributes: true
|
|
1589
|
+
});
|
|
1590
|
+
observers.push(observer);
|
|
1591
|
+
});
|
|
1592
|
+
});
|
|
1593
|
+
if (observers.length === 0) {
|
|
1594
|
+
cleanup();
|
|
1595
|
+
resolve({ mutationCount: 0, stableTime: 0 });
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
resetStableTimer();
|
|
1599
|
+
});
|
|
1600
|
+
},
|
|
1601
|
+
{ selectorList, stableTime, timeout, eventName, callbackName, hasCallback: !!onMutation }
|
|
1602
|
+
);
|
|
1603
|
+
logger9.success("waitForStable", `DOM \u7A33\u5B9A, \u603B\u5171 ${result.mutationCount} \u6B21\u53D8\u5316`);
|
|
1604
|
+
return result;
|
|
1605
|
+
},
|
|
1606
|
+
/**
|
|
1607
|
+
* 创建一个持续监控 DOM 变化的监控器
|
|
1608
|
+
*
|
|
1609
|
+
* @param {import('playwright').Page} page - Playwright page 对象
|
|
1610
|
+
* @param {string | string[]} selectors - 要监控的 CSS 选择器
|
|
1611
|
+
* @param {Object} [options] - 配置选项
|
|
1612
|
+
* @param {Function} [options.onMutation] - 变化时的回调 (mutationCount: number) => void
|
|
1613
|
+
* @returns {Promise<{ stop: () => Promise<{ totalMutations: number }> }>} - 返回停止函数
|
|
1614
|
+
*/
|
|
1615
|
+
async createMonitor(page, selectors, options = {}) {
|
|
1616
|
+
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
1617
|
+
const onMutation = options.onMutation;
|
|
1618
|
+
logger9.start("createMonitor", `\u76D1\u63A7 ${selectorList.length} \u4E2A\u9009\u62E9\u5668`);
|
|
1619
|
+
const monitorKey = generateKey("pk_mon");
|
|
1620
|
+
const callbackName = generateKey("pk_mon_cb");
|
|
1621
|
+
const cleanerName = generateKey("pk_mon_clean");
|
|
1622
|
+
if (onMutation) {
|
|
1623
|
+
try {
|
|
1624
|
+
await page.exposeFunction(callbackName, (count) => {
|
|
1625
|
+
try {
|
|
1626
|
+
onMutation(count);
|
|
1627
|
+
} catch (e) {
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
} catch (e) {
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
await page.evaluate(({ selectorList: selectorList2, monitorKey: monitorKey2, callbackName: callbackName2, cleanerName: cleanerName2, hasCallback }) => {
|
|
1634
|
+
const monitor = {
|
|
1635
|
+
observers: [],
|
|
1636
|
+
totalMutations: 0,
|
|
1637
|
+
running: true
|
|
1638
|
+
};
|
|
1639
|
+
selectorList2.forEach((selector) => {
|
|
1640
|
+
const elements = document.querySelectorAll(selector);
|
|
1641
|
+
elements.forEach((element) => {
|
|
1642
|
+
const observer = new MutationObserver((mutations) => {
|
|
1643
|
+
if (!monitor.running) return;
|
|
1644
|
+
monitor.totalMutations += mutations.length;
|
|
1645
|
+
if (hasCallback && window[callbackName2]) {
|
|
1646
|
+
window[callbackName2](monitor.totalMutations);
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
observer.observe(element, {
|
|
1650
|
+
childList: true,
|
|
1651
|
+
subtree: true,
|
|
1652
|
+
characterData: true,
|
|
1653
|
+
attributes: true
|
|
1654
|
+
});
|
|
1655
|
+
monitor.observers.push(observer);
|
|
1656
|
+
});
|
|
1657
|
+
});
|
|
1658
|
+
window[monitorKey2] = monitor;
|
|
1659
|
+
window[cleanerName2] = () => {
|
|
1660
|
+
monitor.running = false;
|
|
1661
|
+
monitor.observers.forEach((obs) => obs.disconnect());
|
|
1662
|
+
const total = monitor.totalMutations;
|
|
1663
|
+
delete window[monitorKey2];
|
|
1664
|
+
delete window[cleanerName2];
|
|
1665
|
+
return total;
|
|
1666
|
+
};
|
|
1667
|
+
}, { selectorList, monitorKey, callbackName, cleanerName, hasCallback: !!onMutation });
|
|
1668
|
+
logger9.success("createMonitor", "\u76D1\u63A7\u5668\u5DF2\u542F\u52A8");
|
|
1669
|
+
return {
|
|
1670
|
+
stop: async () => {
|
|
1671
|
+
let totalMutations = 0;
|
|
1672
|
+
try {
|
|
1673
|
+
totalMutations = await page.evaluate((cleanerName2) => {
|
|
1674
|
+
if (window[cleanerName2]) {
|
|
1675
|
+
return window[cleanerName2]();
|
|
1676
|
+
}
|
|
1677
|
+
return 0;
|
|
1678
|
+
}, cleanerName);
|
|
1679
|
+
} catch (e) {
|
|
1680
|
+
}
|
|
1681
|
+
logger9.success("createMonitor.stop", `\u76D1\u63A7\u5DF2\u505C\u6B62, \u5171 ${totalMutations} \u6B21\u53D8\u5316`);
|
|
1682
|
+
return { totalMutations };
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1652
1688
|
// index.js
|
|
1653
1689
|
var usePlaywrightToolKit = () => {
|
|
1654
1690
|
return {
|
|
@@ -1662,7 +1698,8 @@ var usePlaywrightToolKit = () => {
|
|
|
1662
1698
|
Captcha,
|
|
1663
1699
|
Sse,
|
|
1664
1700
|
Errors: errors_exports,
|
|
1665
|
-
Interception
|
|
1701
|
+
Interception,
|
|
1702
|
+
Mutation
|
|
1666
1703
|
};
|
|
1667
1704
|
};
|
|
1668
1705
|
export {
|