@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/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
- logger.start(`[Step] ${step}`);
162
- try {
163
- const result = await actionFn();
164
- logger.success(`[Step] ${step}`);
165
- return result;
166
- } catch (error) {
167
- logger.fail(`[Step] ${step}`, error);
168
- if (failActor) {
169
- let base64 = "\u622A\u56FE\u5931\u8D25";
170
- try {
171
- if (page) {
172
- const buffer = await page.screenshot({ fullPage: true, type: "jpeg", quality: 60 });
173
- base64 = `data:image/jpeg;base64,${buffer.toString("base64")}`;
174
- }
175
- } catch (snapErr) {
176
- logger.warn(`\u622A\u56FE\u751F\u6210\u5931\u8D25: ${snapErr.message}`);
177
- }
178
- await this.pushFailed(error, {
179
- step,
180
- page,
181
- options,
182
- base64
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
- throw error;
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 maxHeight = document.body.scrollHeight;
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 > maxHeight) {
297
- maxHeight = el.scrollHeight;
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 maxHeight;
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: maxScrollHeight + buffer
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
- await page.evaluate(() => {
323
- document.querySelectorAll(".__pk_expanded__").forEach((el) => {
324
- el.style.overflow = el.dataset.pkOrigOverflow || "";
325
- el.style.height = el.dataset.pkOrigHeight || "";
326
- el.style.maxHeight = el.dataset.pkOrigMaxHeight || "";
327
- delete el.dataset.pkOrigOverflow;
328
- delete el.dataset.pkOrigHeight;
329
- delete el.dataset.pkOrigMaxHeight;
330
- el.classList.remove("__pk_expanded__");
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
- if (originalViewport) {
334
- await page.setViewportSize(originalViewport);
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
- "--disable-blink-features=AutomationControlled",
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
- const primaryLocale = parseAcceptLanguage(acceptLanguage || BASE_CONFIG.acceptLanguage)[0] || BASE_CONFIG.locale;
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 (acceptLanguage) {
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
- // 模拟浏览器 TLS 指纹
1572
- headerGeneratorOptions: AntiCheat.getTlsFingerprintOptions(userAgent, resolvedAcceptLanguage),
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 {