@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 CHANGED
@@ -38,9 +38,6 @@ const crawler = new PlaywrightCrawler({
38
38
  },
39
39
  preNavigationHooks: [
40
40
  async ({ page }) => {
41
- // 统一反爬:时区/语言/权限/视口
42
- await AntiCheat.applyPage(page);
43
-
44
41
  // 验证码监控
45
42
  Captcha.useCaptchaMonitor(page, {
46
43
  domSelector: '#captcha_container',
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
- logger.start(`[Step] ${step}`);
191
- try {
192
- const result = await actionFn();
193
- logger.success(`[Step] ${step}`);
194
- return result;
195
- } catch (error) {
196
- logger.fail(`[Step] ${step}`, error);
197
- if (failActor) {
198
- let base64 = "\u622A\u56FE\u5931\u8D25";
199
- try {
200
- if (page) {
201
- const buffer = await page.screenshot({ fullPage: true, type: "jpeg", quality: 60 });
202
- base64 = `data:image/jpeg;base64,${buffer.toString("base64")}`;
203
- }
204
- } catch (snapErr) {
205
- logger.warn(`\u622A\u56FE\u751F\u6210\u5931\u8D25: ${snapErr.message}`);
206
- }
207
- await this.pushFailed(error, {
208
- step,
209
- page,
210
- options,
211
- base64
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
- throw error;
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 maxHeight = document.body.scrollHeight;
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 > maxHeight) {
326
- maxHeight = el.scrollHeight;
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 maxHeight;
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: maxScrollHeight + buffer
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
- await page.evaluate(() => {
352
- document.querySelectorAll(".__pk_expanded__").forEach((el) => {
353
- el.style.overflow = el.dataset.pkOrigOverflow || "";
354
- el.style.height = el.dataset.pkOrigHeight || "";
355
- el.style.maxHeight = el.dataset.pkOrigMaxHeight || "";
356
- delete el.dataset.pkOrigOverflow;
357
- delete el.dataset.pkOrigHeight;
358
- delete el.dataset.pkOrigMaxHeight;
359
- el.classList.remove("__pk_expanded__");
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
- if (originalViewport) {
363
- await page.setViewportSize(originalViewport);
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
- "--disable-blink-features=AutomationControlled",
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
- const primaryLocale = parseAcceptLanguage(acceptLanguage || BASE_CONFIG.acceptLanguage)[0] || BASE_CONFIG.locale;
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 (acceptLanguage) {
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
- // 模拟浏览器 TLS 指纹
1601
- headerGeneratorOptions: AntiCheat.getTlsFingerprintOptions(userAgent, resolvedAcceptLanguage),
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: