@skrillex1224/playwright-toolkit 2.1.247 → 2.1.248

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
@@ -106,10 +106,10 @@ var createActorInfo = (info) => {
106
106
  xurl
107
107
  };
108
108
  };
109
- const buildLandingUrl = ({ protocol: protocol2, domain: domain2, path: path3 }) => {
109
+ const buildLandingUrl = ({ protocol: protocol2, domain: domain2, path: path4 }) => {
110
110
  const safeProtocol = String(protocol2).trim();
111
111
  const safeDomain = normalizeDomain(domain2);
112
- const safePath = normalizePath(path3);
112
+ const safePath = normalizePath(path4);
113
113
  return `${safeProtocol}://${safeDomain}${safePath}`;
114
114
  };
115
115
  const buildIcon = ({ key }) => {
@@ -117,14 +117,14 @@ var createActorInfo = (info) => {
117
117
  };
118
118
  const protocol = info.protocol || "https";
119
119
  const domain = normalizeDomain(info.domain);
120
- const path2 = normalizePath(info.path);
120
+ const path3 = normalizePath(info.path);
121
121
  const share = normalizeShare2(info.share);
122
122
  const device = normalizeDevice(info.device);
123
123
  return {
124
124
  ...info,
125
125
  protocol,
126
126
  domain,
127
- path: path2,
127
+ path: path3,
128
128
  share,
129
129
  device,
130
130
  get icon() {
@@ -1484,7 +1484,7 @@ var normalizeCookies = (value) => {
1484
1484
  if (!name || !cookieValue || cookieValue === "<nil>") return null;
1485
1485
  const domain = String(raw.domain || "").trim();
1486
1486
  const url = normalizeHttpUrl(raw.url);
1487
- const path2 = String(raw.path || "").trim() || "/";
1487
+ const path3 = String(raw.path || "").trim() || "/";
1488
1488
  const sameSite = normalizeCookieSameSite(raw.sameSite);
1489
1489
  const expires = normalizeCookieExpires(raw);
1490
1490
  const secure = Boolean(raw.secure);
@@ -1493,7 +1493,7 @@ var normalizeCookies = (value) => {
1493
1493
  const normalized = {
1494
1494
  name,
1495
1495
  value: cookieValue,
1496
- path: path2,
1496
+ path: path3,
1497
1497
  ...domain ? { domain } : {},
1498
1498
  ...!domain && url ? { url } : {},
1499
1499
  ...sameSite ? { sameSite } : {},
@@ -2564,6 +2564,10 @@ var assertPoint = (point) => {
2564
2564
  throw new Error(`Invalid input point: ${JSON.stringify(point)}`);
2565
2565
  }
2566
2566
  };
2567
+ var toFiniteNumber = (value, fallback = 0) => {
2568
+ const number = Number(value);
2569
+ return Number.isFinite(number) ? number : fallback;
2570
+ };
2567
2571
  var dispatchMouseMove = (page, point, options = {}) => page.mouse.move(point.x, point.y, options);
2568
2572
  var dispatchMouseStart = (page, options = {}) => page.mouse.down(options);
2569
2573
  var dispatchMouseEnd = (page, options = {}) => page.mouse.up(options);
@@ -2583,6 +2587,11 @@ var dragWithMouse = async (page, points, options = {}) => {
2583
2587
  }, { steps: 2 });
2584
2588
  await waitFor(page, options.stepDelayMs ?? 90);
2585
2589
  }
2590
+ const finalMoveRepeats = Math.max(0, Math.floor(toFiniteNumber(options.finalMoveRepeats)));
2591
+ for (let repeat = 0; repeat < finalMoveRepeats; repeat += 1) {
2592
+ await dispatchMouseMove(page, points.end, { steps: 1 });
2593
+ await waitFor(page, options.finalMoveDelayMs ?? 35);
2594
+ }
2586
2595
  await waitFor(page, options.beforeReleaseDelayMs ?? 100);
2587
2596
  await dispatchMouseEnd(page);
2588
2597
  await waitFor(page, options.afterReleaseDelayMs ?? 100);
@@ -2592,6 +2601,7 @@ var dragWithTouch = async (page, points, options = {}) => {
2592
2601
  let client = null;
2593
2602
  try {
2594
2603
  client = await page.context().newCDPSession(page);
2604
+ await waitFor(page, options.initialDelayMs ?? 250);
2595
2605
  await client.send("Input.dispatchTouchEvent", {
2596
2606
  type: "touchStart",
2597
2607
  touchPoints: [{ x: points.start.x, y: points.start.y, id: 1 }]
@@ -2613,6 +2623,14 @@ var dragWithTouch = async (page, points, options = {}) => {
2613
2623
  });
2614
2624
  await waitFor(page, options.stepDelayMs ?? 90);
2615
2625
  }
2626
+ const finalMoveRepeats = Math.max(0, Math.floor(toFiniteNumber(options.finalMoveRepeats)));
2627
+ for (let repeat = 0; repeat < finalMoveRepeats; repeat += 1) {
2628
+ await client.send("Input.dispatchTouchEvent", {
2629
+ type: "touchMove",
2630
+ touchPoints: [{ x: points.end.x, y: points.end.y, id: 1 }]
2631
+ });
2632
+ await waitFor(page, options.finalMoveDelayMs ?? 35);
2633
+ }
2616
2634
  await waitFor(page, options.beforeReleaseDelayMs ?? 100);
2617
2635
  await client.send("Input.dispatchTouchEvent", {
2618
2636
  type: "touchEnd",
@@ -2630,7 +2648,7 @@ var dragWithTouch = async (page, points, options = {}) => {
2630
2648
  var clickTargetWithDevice = async (page, target, options = {}) => {
2631
2649
  const normalizedOptions = normalizeSelectorOptions(options);
2632
2650
  const resolvedDevice = resolveDeviceFromPage(page);
2633
- if (target && resolvedDevice === Device.Mobile && !normalizedOptions.forceClick) {
2651
+ if (target && resolvedDevice === Device.Mobile && !normalizedOptions.forceClick && !normalizedOptions.forceMouse) {
2634
2652
  if (typeof target.tap === "function") {
2635
2653
  await target.tap(normalizedOptions.tapOptions);
2636
2654
  return true;
@@ -2830,20 +2848,26 @@ var DeviceInput = {
2830
2848
  throw new Error("Unable to resolve drag coordinates.");
2831
2849
  }
2832
2850
  const steps = options.steps || 10;
2851
+ const sourceOffsetX = toFiniteNumber(options.sourceOffsetX);
2852
+ const sourceOffsetY = toFiniteNumber(options.sourceOffsetY);
2853
+ const targetOffsetX = toFiniteNumber(options.targetOffsetX);
2854
+ const targetOffsetY = toFiniteNumber(options.targetOffsetY);
2855
+ const sourceCenterX = sourceBox.x + sourceBox.width / 2 + sourceOffsetX;
2856
+ const sourceCenterY = sourceBox.y + sourceBox.height / 2 + sourceOffsetY;
2833
2857
  const liftOffsetX = Math.min(18, Math.max(8, sourceBox.width * 0.12));
2834
2858
  const liftOffsetY = Math.min(12, Math.max(4, sourceBox.height * 0.08));
2835
2859
  const points = {
2836
2860
  start: {
2837
- x: sourceBox.x + sourceBox.width / 2,
2838
- y: sourceBox.y + sourceBox.height / 2
2861
+ x: sourceCenterX,
2862
+ y: sourceCenterY
2839
2863
  },
2840
2864
  lift: {
2841
- x: sourceBox.x + sourceBox.width / 2 + liftOffsetX,
2842
- y: sourceBox.y + sourceBox.height / 2 + liftOffsetY
2865
+ x: sourceCenterX + liftOffsetX,
2866
+ y: sourceCenterY + liftOffsetY
2843
2867
  },
2844
2868
  end: {
2845
- x: targetBox.x + targetBox.width / 2,
2846
- y: targetBox.y + targetBox.height / 2
2869
+ x: targetBox.x + targetBox.width / 2 + targetOffsetX,
2870
+ y: targetBox.y + targetBox.height / 2 + targetOffsetY
2847
2871
  },
2848
2872
  steps
2849
2873
  };
@@ -4960,6 +4984,10 @@ var LiveView = {
4960
4984
  // src/chaptcha.js
4961
4985
  import { v4 as uuidv4 } from "uuid";
4962
4986
 
4987
+ // src/internals/captcha/bytedance.js
4988
+ import { mkdir, writeFile } from "fs/promises";
4989
+ import path2 from "path";
4990
+
4963
4991
  // src/internals/captcha/shared.js
4964
4992
  var waitForVisible = async (locator, timeout) => {
4965
4993
  try {
@@ -4977,38 +5005,71 @@ var isAnyCaptchaTextVisible = async (frame, texts, timeout) => {
4977
5005
  if (!text) {
4978
5006
  continue;
4979
5007
  }
4980
- const candidates = [
4981
- frame.getByText(text, { exact: false }).first(),
4982
- frame.locator(`text=${text}`).first()
4983
- ];
4984
- for (const candidate of candidates) {
4985
- const isVisible = await candidate.isVisible({ timeout }).catch(() => false);
4986
- if (isVisible) {
4987
- return true;
5008
+ const textLocator = frame.getByText(text, { exact: false });
5009
+ const locatorText = frame.locator(`text=${text}`);
5010
+ const candidateGroups = [textLocator, locatorText];
5011
+ for (const candidateGroup of candidateGroups) {
5012
+ const candidateCount = await candidateGroup.count().catch(() => 0);
5013
+ for (let index = 0; index < candidateCount; index += 1) {
5014
+ const candidate = candidateGroup.nth(index);
5015
+ const isVisible = await candidate.isVisible({ timeout }).catch(() => false);
5016
+ if (isVisible) {
5017
+ return true;
5018
+ }
4988
5019
  }
4989
5020
  }
4990
5021
  }
4991
5022
  return false;
4992
5023
  };
5024
+ var collectVisibleCandidateIndexes = async (candidateGroup, count, timeout) => {
5025
+ const visibleIndexes = [];
5026
+ for (let index = 0; index < count; index += 1) {
5027
+ const candidate = candidateGroup.nth(index);
5028
+ const isVisible = await candidate.isVisible({ timeout }).catch(() => false);
5029
+ if (isVisible) {
5030
+ visibleIndexes.push(index);
5031
+ }
5032
+ }
5033
+ return visibleIndexes;
5034
+ };
4993
5035
  var clickCaptchaAction = async (frame, texts, options) => {
4994
5036
  for (const text of texts || []) {
4995
- const candidates = [
4996
- frame.getByText(text, { exact: false }).first(),
4997
- frame.locator(`text=${text}`).first()
5037
+ const textLocator = frame.getByText(text, { exact: false });
5038
+ const locatorText = frame.locator(`text=${text}`);
5039
+ const [getByTextCount, locatorTextCount] = await Promise.all([
5040
+ textLocator.count().catch(() => 0),
5041
+ locatorText.count().catch(() => 0)
5042
+ ]);
5043
+ const [getByTextVisibleIndexes, locatorTextVisibleIndexes] = await Promise.all([
5044
+ collectVisibleCandidateIndexes(textLocator, getByTextCount, options.actionVisibleTimeoutMs),
5045
+ collectVisibleCandidateIndexes(locatorText, locatorTextCount, options.actionVisibleTimeoutMs)
5046
+ ]);
5047
+ options.logger?.info(
5048
+ `[CaptchaAction] \u6587\u672C "${text}" \u547D\u4E2D\u6570\u91CF\uFF1AgetByText=${getByTextCount} (visible=${getByTextVisibleIndexes.length}), locator=${locatorTextCount} (visible=${locatorTextVisibleIndexes.length})`
5049
+ );
5050
+ const candidateGroups = [
5051
+ { label: "getByText", locator: textLocator, count: getByTextCount },
5052
+ { label: "locator", locator: locatorText, count: locatorTextCount }
4998
5053
  ];
4999
- for (const candidate of candidates) {
5000
- const isVisible = await waitForVisible(candidate, options.actionVisibleTimeoutMs);
5001
- if (!isVisible) {
5002
- continue;
5054
+ for (const candidateGroup of candidateGroups) {
5055
+ for (let index = 0; index < candidateGroup.count; index += 1) {
5056
+ const candidate = candidateGroup.locator.nth(index);
5057
+ const isVisible = await waitForVisible(candidate, options.actionVisibleTimeoutMs);
5058
+ if (!isVisible) {
5059
+ continue;
5060
+ }
5061
+ options.logger?.info(
5062
+ `[CaptchaAction] \u6587\u672C "${text}" \u9009\u62E9 ${candidateGroup.label}[${index}] \u4F5C\u4E3A\u7B2C\u4E00\u4E2A\u53EF\u89C1\u8282\u70B9\u6267\u884C\u70B9\u51FB\u3002`
5063
+ );
5064
+ await DeviceInput.click(options.page, candidate, options);
5065
+ return true;
5003
5066
  }
5004
- await DeviceInput.click(options.page, candidate);
5005
- return true;
5006
5067
  }
5007
5068
  }
5008
5069
  return false;
5009
5070
  };
5010
- var dragCaptchaAction = async (page, sourceLocator, targetLocator) => {
5011
- await DeviceInput.drag(page, sourceLocator, targetLocator);
5071
+ var dragCaptchaAction = async (page, sourceLocator, targetLocator, options = {}) => {
5072
+ await DeviceInput.drag(page, sourceLocator, targetLocator, options);
5012
5073
  };
5013
5074
 
5014
5075
  // src/internals/captcha/bytedance.js
@@ -5019,11 +5080,12 @@ var DEFAULT_BYTEDANCE_CAPTCHA_OPTIONS = Object.freeze({
5019
5080
  containerSelector: "#captcha_container",
5020
5081
  iframeSelector: 'iframe[src*="verifycenter"]',
5021
5082
  iframeFallbackSelector: "iframe",
5022
- sourceImageSelector: "div.canvas-container",
5023
- dropTargetContainerSelector: "#captcha_verify_image div",
5083
+ sourceImageSelector: ".img-container .canvas-container",
5084
+ dropTargetContainerSelector: ".drag-area",
5024
5085
  dropTargetTexts: ["\u62D6\u62FD\u5230\u8FD9\u91CC"],
5025
5086
  refreshTexts: ["\u5237\u65B0"],
5026
5087
  submitTexts: ["\u63D0\u4EA4"],
5088
+ guideMaskSelector: ".play-guide-mask",
5027
5089
  recognitionSuccessCode: 1e4,
5028
5090
  containerVisibleTimeoutMs: 2e3,
5029
5091
  iframeVisibleTimeoutMs: 12e3,
@@ -5046,10 +5108,111 @@ var DEFAULT_BYTEDANCE_CAPTCHA_OPTIONS = Object.freeze({
5046
5108
  ],
5047
5109
  recognitionDelayMs: 2e3,
5048
5110
  refreshWaitMs: 3e3,
5049
- submitWaitMs: 3e3,
5111
+ submitWaitMs: 5e3,
5112
+ submitReadyTimeoutMs: 2500,
5050
5113
  retryDelayBaseMs: 2e3,
5051
- retryDelayStepMs: 1e3
5114
+ retryDelayStepMs: 1e3,
5115
+ sourceImageRowTolerancePx: 24,
5116
+ dragBetweenWaitMs: 250,
5117
+ promptBadgeCountSelector: ".drag-area .photo-badge .badge span",
5118
+ promptSubmitButtonSelector: ".vc-captcha-verify-mobile-button",
5119
+ promptSelectedSourceSelector: ".img-container .canvas-container.selected",
5120
+ promptActiveSourceSelector: ".img-container .canvas-container.active",
5121
+ promptDragMoveSteps: 16,
5122
+ promptDragStepDelayMs: 55,
5123
+ promptDragHoldDelayMs: 240,
5124
+ promptDragBeforeReleaseDelayMs: 180,
5125
+ promptDragAfterReleaseDelayMs: 240,
5126
+ promptDragFinalMoveRepeats: 3,
5127
+ promptDragRetryDelayMs: 250,
5128
+ debugArtifacts: false
5052
5129
  });
5130
+ var PROMPT_CAPTCHA_DRAG_PLANS = Object.freeze([
5131
+ { name: "lower-middle", endXRatio: 0.5, endYRatio: 0.72 },
5132
+ { name: "center-middle", endXRatio: 0.5, endYRatio: 0.56 },
5133
+ { name: "upper-middle", endXRatio: 0.5, endYRatio: 0.4 }
5134
+ ]);
5135
+ var resolveCaptchaDebugDir = () => path2.resolve(process.cwd(), "storage", "captcha-debug");
5136
+ var rectOf = (rect) => {
5137
+ if (!rect) {
5138
+ return null;
5139
+ }
5140
+ return {
5141
+ x: Number(rect.x.toFixed(2)),
5142
+ y: Number(rect.y.toFixed(2)),
5143
+ width: Number(rect.width.toFixed(2)),
5144
+ height: Number(rect.height.toFixed(2))
5145
+ };
5146
+ };
5147
+ var collectCaptchaDebugInfo = async (page, frame, iframeLocator, attempt, phase, extra = null) => {
5148
+ const timestamp = Date.now();
5149
+ const debugDir = resolveCaptchaDebugDir();
5150
+ await mkdir(debugDir, { recursive: true });
5151
+ const baseName = `bytedance-${timestamp}-attempt${attempt}-${phase}`;
5152
+ const iframeScreenshotPath = path2.join(debugDir, `${baseName}-iframe.png`);
5153
+ const pageScreenshotPath = path2.join(debugDir, `${baseName}-page.png`);
5154
+ const htmlPath = path2.join(debugDir, `${baseName}-iframe.html`);
5155
+ const infoPath = path2.join(debugDir, `${baseName}-info.json`);
5156
+ await iframeLocator.screenshot({ path: iframeScreenshotPath }).catch(() => {
5157
+ });
5158
+ await page.screenshot({ path: pageScreenshotPath, fullPage: true }).catch(() => {
5159
+ });
5160
+ const html = await frame.evaluate(() => document.documentElement.outerHTML).catch(() => "");
5161
+ if (html) {
5162
+ await writeFile(htmlPath, html, "utf8");
5163
+ }
5164
+ const info = await frame.evaluate(() => {
5165
+ const toRect = (element) => {
5166
+ const rect = element.getBoundingClientRect();
5167
+ return {
5168
+ x: Number(rect.x.toFixed(2)),
5169
+ y: Number(rect.y.toFixed(2)),
5170
+ width: Number(rect.width.toFixed(2)),
5171
+ height: Number(rect.height.toFixed(2))
5172
+ };
5173
+ };
5174
+ const toItem = (element, index) => ({
5175
+ index,
5176
+ tag: element.tagName,
5177
+ id: element.id || "",
5178
+ className: typeof element.className === "string" ? element.className : "",
5179
+ text: String(element.textContent || "").trim(),
5180
+ rect: toRect(element)
5181
+ });
5182
+ const visibleNodes = Array.from(document.querySelectorAll("body *")).map(toItem).filter((item) => item.rect.width > 0 && item.rect.height > 0);
5183
+ return {
5184
+ title: document.title,
5185
+ bodyText: String(document.body?.innerText || "").trim(),
5186
+ canvasContainers: visibleNodes.filter((item) => item.className.includes("canvas-container")),
5187
+ canvasNodes: visibleNodes.filter((item) => item.tag === "CANVAS"),
5188
+ captchaNodes: visibleNodes.filter((item) => item.id.startsWith("captcha_") || item.className.includes("captcha") || item.className.includes("verify") || /拖拽到这里|刷新|提交/.test(item.text)),
5189
+ visibleTextNodes: visibleNodes.filter((item) => item.text).slice(0, 300)
5190
+ };
5191
+ }).catch(() => null);
5192
+ if (info) {
5193
+ const payload = {
5194
+ capturedAt: new Date(timestamp).toISOString(),
5195
+ pageUrl: page.url(),
5196
+ attempt,
5197
+ phase,
5198
+ iframeScreenshotPath,
5199
+ pageScreenshotPath,
5200
+ htmlPath,
5201
+ info
5202
+ };
5203
+ if (extra != null) {
5204
+ payload.extra = extra;
5205
+ }
5206
+ await writeFile(infoPath, JSON.stringify(payload, null, 2), "utf8");
5207
+ }
5208
+ logger10.info(`\u5DF2\u5199\u51FA\u9A8C\u8BC1\u7801\u8C03\u8BD5\u4EA7\u7269\uFF1A${debugDir}`);
5209
+ };
5210
+ var maybeCollectCaptchaDebugInfo = async (page, frame, iframeLocator, attempt, phase, options, extra = null) => {
5211
+ if (!options.debugArtifacts) {
5212
+ return;
5213
+ }
5214
+ await collectCaptchaDebugInfo(page, frame, iframeLocator, attempt, phase, extra);
5215
+ };
5053
5216
  var extractCaptchaSerialNumbers = (apiResponse) => {
5054
5217
  const serialNumbers = apiResponse?.data?.data?.serial_number;
5055
5218
  if (!Array.isArray(serialNumbers)) {
@@ -5058,7 +5221,7 @@ var extractCaptchaSerialNumbers = (apiResponse) => {
5058
5221
  return serialNumbers.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value >= 0);
5059
5222
  };
5060
5223
  var resolveContentFrame = async (page, iframeLocator, options) => {
5061
- for (let attempt = 1; attempt <= options.contentFrameResolveRetries; attempt++) {
5224
+ for (let attempt = 1; attempt <= options.contentFrameResolveRetries; attempt += 1) {
5062
5225
  const iframeHandle = await iframeLocator.elementHandle();
5063
5226
  const frame = await iframeHandle?.contentFrame();
5064
5227
  if (frame) {
@@ -5103,20 +5266,15 @@ var getVerifycenterCaptchaContext = async (page, options) => {
5103
5266
  }
5104
5267
  return { iframeLocator, frame };
5105
5268
  };
5106
- var refreshCaptcha = async (page, frame, options) => {
5107
- const clicked = await clickCaptchaAction(frame, options.refreshTexts, { ...options, page }).catch(() => false);
5108
- if (!clicked) {
5109
- logger10.warn("Refresh button not found.");
5110
- return false;
5111
- }
5112
- await page.waitForTimeout(options.refreshWaitMs);
5113
- return true;
5114
- };
5115
5269
  var findCaptchaDropTarget = async (frame, options) => {
5270
+ const directTarget = frame.locator(options.dropTargetContainerSelector).first();
5271
+ if (await waitForVisible(directTarget, options.actionVisibleTimeoutMs)) {
5272
+ return directTarget;
5273
+ }
5116
5274
  for (const text of options.dropTargetTexts) {
5117
5275
  const candidates = [
5118
- frame.locator(options.dropTargetContainerSelector).filter({ hasText: text }).first(),
5119
- frame.getByText(text, { exact: false }).first()
5276
+ frame.getByText(text, { exact: false }).first(),
5277
+ frame.locator(`text=${text}`).first()
5120
5278
  ];
5121
5279
  for (const candidate of candidates) {
5122
5280
  const isVisible = await waitForVisible(candidate, options.actionVisibleTimeoutMs);
@@ -5127,10 +5285,112 @@ var findCaptchaDropTarget = async (frame, options) => {
5127
5285
  }
5128
5286
  return null;
5129
5287
  };
5288
+ var readPromptCaptchaState = async (frame, options) => frame.evaluate((selectors) => {
5289
+ const toRect = (element) => {
5290
+ if (!element) {
5291
+ return null;
5292
+ }
5293
+ const rect = element.getBoundingClientRect();
5294
+ return {
5295
+ x: Number(rect.x.toFixed(2)),
5296
+ y: Number(rect.y.toFixed(2)),
5297
+ width: Number(rect.width.toFixed(2)),
5298
+ height: Number(rect.height.toFixed(2))
5299
+ };
5300
+ };
5301
+ const badgeNode = document.querySelector(selectors.badgeCountSelector);
5302
+ const submitButton = document.querySelector(selectors.submitButtonSelector);
5303
+ const dragArea = document.querySelector(selectors.dragAreaSelector);
5304
+ const badgeCount = badgeNode ? Number.parseInt(String(badgeNode.textContent || "").trim(), 10) : 0;
5305
+ return {
5306
+ badgeCount: Number.isFinite(badgeCount) ? badgeCount : 0,
5307
+ selectedCount: document.querySelectorAll(selectors.selectedSourceSelector).length,
5308
+ activeCount: document.querySelectorAll(selectors.activeSourceSelector).length,
5309
+ submitDisabled: submitButton ? submitButton.classList.contains("disable") : null,
5310
+ dragAreaActive: dragArea ? dragArea.classList.contains("active") : false,
5311
+ dragAreaRect: toRect(dragArea)
5312
+ };
5313
+ }, {
5314
+ badgeCountSelector: options.promptBadgeCountSelector,
5315
+ submitButtonSelector: options.promptSubmitButtonSelector,
5316
+ selectedSourceSelector: options.promptSelectedSourceSelector,
5317
+ activeSourceSelector: options.promptActiveSourceSelector,
5318
+ dragAreaSelector: options.dropTargetContainerSelector
5319
+ }).catch(() => ({
5320
+ badgeCount: 0,
5321
+ selectedCount: 0,
5322
+ activeCount: 0,
5323
+ submitDisabled: null,
5324
+ dragAreaActive: false,
5325
+ dragAreaRect: null
5326
+ }));
5327
+ var normalizeCaptchaImageIndexes = (serialNumbers, imageCount) => {
5328
+ if (!Array.isArray(serialNumbers) || imageCount <= 0) {
5329
+ return [];
5330
+ }
5331
+ const areAllOneBased = serialNumbers.every((value) => value >= 1 && value <= imageCount);
5332
+ if (areAllOneBased) {
5333
+ return serialNumbers.map((value) => value - 1);
5334
+ }
5335
+ const areAllZeroBased = serialNumbers.every((value) => value >= 0 && value < imageCount);
5336
+ if (areAllZeroBased) {
5337
+ return [...serialNumbers];
5338
+ }
5339
+ return serialNumbers.map((value) => {
5340
+ if (value >= 1 && value <= imageCount) {
5341
+ return value - 1;
5342
+ }
5343
+ if (value >= 0 && value < imageCount) {
5344
+ return value;
5345
+ }
5346
+ return null;
5347
+ }).filter((value) => Number.isInteger(value));
5348
+ };
5349
+ var resolveCaptchaSourceImagesInVisualOrder = async (frame, options) => {
5350
+ const sourceImages = frame.locator(options.sourceImageSelector);
5351
+ const imageCount = await sourceImages.count().catch(() => 0);
5352
+ const sources = [];
5353
+ for (let domIndex = 0; domIndex < imageCount; domIndex += 1) {
5354
+ const locator = sourceImages.nth(domIndex);
5355
+ const box = await locator.boundingBox().catch(() => null);
5356
+ if (!box || box.width <= 0 || box.height <= 0) {
5357
+ continue;
5358
+ }
5359
+ sources.push({
5360
+ domIndex,
5361
+ locator,
5362
+ box
5363
+ });
5364
+ }
5365
+ sources.sort((left, right) => {
5366
+ const deltaY = left.box.y - right.box.y;
5367
+ if (Math.abs(deltaY) > options.sourceImageRowTolerancePx) {
5368
+ return deltaY;
5369
+ }
5370
+ return left.box.x - right.box.x;
5371
+ });
5372
+ return sources;
5373
+ };
5374
+ var refreshCaptcha = async (page, frame, options) => {
5375
+ const clicked = await clickCaptchaAction(frame, options.refreshTexts, {
5376
+ ...options,
5377
+ page,
5378
+ logger: logger10,
5379
+ forceMouse: true
5380
+ }).catch(() => false);
5381
+ if (!clicked) {
5382
+ logger10.warn("Refresh button not found.");
5383
+ return false;
5384
+ }
5385
+ await page.waitForTimeout(options.refreshWaitMs);
5386
+ return true;
5387
+ };
5130
5388
  var waitForCaptchaChallengeReady = async (page, frame, options) => {
5131
5389
  const deadline = Date.now() + options.challengeReadyTimeoutMs;
5132
5390
  let refreshDeadline = Date.now() + options.challengeReadyRefreshTimeoutMs;
5133
5391
  let hasSeenLoading = false;
5392
+ let hasSeenGuideMask = false;
5393
+ let hasLoggedGuideMask = false;
5134
5394
  while (Date.now() < deadline) {
5135
5395
  const isLoadingVisible = await isAnyCaptchaTextVisible(
5136
5396
  frame,
@@ -5146,8 +5406,17 @@ var waitForCaptchaChallengeReady = async (page, frame, options) => {
5146
5406
  const sourceImages = frame.locator(options.sourceImageSelector);
5147
5407
  const imageCount = await sourceImages.count().catch(() => 0);
5148
5408
  const hasVisibleSourceImage = imageCount > 0 ? await sourceImages.first().isVisible({ timeout: options.loadingIndicatorVisibleTimeoutMs }).catch(() => false) : false;
5149
- if (!isLoadingVisible && hasVisibleSourceImage) {
5150
- logger10.info(hasSeenLoading ? "\u9A8C\u8BC1\u7801\u56FE\u7247\u5DF2\u52A0\u8F7D\u5B8C\u6210\u3002" : "\u9A8C\u8BC1\u7801\u56FE\u7247\u5DF2\u5C31\u7EEA\u3002");
5409
+ const hasVisibleDropTarget = await frame.locator(options.dropTargetContainerSelector).first().isVisible({ timeout: options.loadingIndicatorVisibleTimeoutMs }).catch(() => false);
5410
+ const hasGuideMaskVisible = options.guideMaskSelector ? await frame.locator(options.guideMaskSelector).first().isVisible({ timeout: options.loadingIndicatorVisibleTimeoutMs }).catch(() => false) : false;
5411
+ hasSeenGuideMask = hasSeenGuideMask || hasGuideMaskVisible;
5412
+ if (hasGuideMaskVisible && !hasLoggedGuideMask) {
5413
+ logger10.info("\u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801\u64CD\u4F5C\u5F15\u5BFC\u5C42\uFF0C\u7B49\u5F85\u5176\u6D88\u5931\u540E\u518D\u5F00\u59CB\u8BC6\u522B\u3002");
5414
+ hasLoggedGuideMask = true;
5415
+ }
5416
+ if (!isLoadingVisible && hasVisibleSourceImage && hasVisibleDropTarget && !hasGuideMaskVisible) {
5417
+ logger10.info(
5418
+ hasSeenGuideMask ? "\u9A8C\u8BC1\u7801\u56FE\u7247\u548C\u62D6\u62FD\u533A\u57DF\u5DF2\u5C31\u7EEA\uFF0C\u5F15\u5BFC\u5C42\u5DF2\u6D88\u5931\u3002" : hasSeenLoading ? "\u9A8C\u8BC1\u7801\u56FE\u7247\u5DF2\u52A0\u8F7D\u5B8C\u6210\u3002" : "\u9A8C\u8BC1\u7801\u56FE\u7247\u5DF2\u5C31\u7EEA\u3002"
5419
+ );
5151
5420
  return;
5152
5421
  }
5153
5422
  if (hasErrorTextVisible) {
@@ -5157,8 +5426,8 @@ var waitForCaptchaChallengeReady = async (page, frame, options) => {
5157
5426
  hasSeenLoading = false;
5158
5427
  continue;
5159
5428
  }
5160
- if (!hasVisibleSourceImage && Date.now() >= refreshDeadline) {
5161
- logger10.warn(`\u9A8C\u8BC1\u7801\u56FE\u7247\u8D85\u8FC7 ${options.challengeReadyRefreshTimeoutMs}ms \u4ECD\u672A\u51FA\u73B0\uFF0C\u5C1D\u8BD5\u5237\u65B0\u9898\u76EE\u3002`);
5429
+ if ((!hasVisibleSourceImage || !hasVisibleDropTarget) && Date.now() >= refreshDeadline) {
5430
+ logger10.warn(`\u9A8C\u8BC1\u7801\u9898\u76EE\u8D85\u8FC7 ${options.challengeReadyRefreshTimeoutMs}ms \u4ECD\u672A\u51C6\u5907\u597D\uFF0C\u5C1D\u8BD5\u5237\u65B0\u9898\u76EE\u3002`);
5162
5431
  await refreshCaptcha(page, frame, options);
5163
5432
  refreshDeadline = Date.now() + options.challengeReadyRefreshTimeoutMs;
5164
5433
  hasSeenLoading = false;
@@ -5168,6 +5437,69 @@ var waitForCaptchaChallengeReady = async (page, frame, options) => {
5168
5437
  }
5169
5438
  throw new Error("Captcha challenge is still loading and did not become ready in time.");
5170
5439
  };
5440
+ var dragPromptCaptchaImage = async (page, frame, iframeLocator, sourceLocator, dropTarget, options, {
5441
+ attempt,
5442
+ visualIndex
5443
+ }) => {
5444
+ const baselineState = await readPromptCaptchaState(frame, options);
5445
+ const dragAttempts = [];
5446
+ for (const plan of PROMPT_CAPTCHA_DRAG_PLANS) {
5447
+ const sourceBox = await sourceLocator.boundingBox().catch(() => null);
5448
+ const targetBox = await dropTarget.boundingBox().catch(() => null);
5449
+ if (!sourceBox || !targetBox) {
5450
+ throw new Error("Unable to resolve prompt captcha drag coordinates.");
5451
+ }
5452
+ const targetOffsetX = (plan.endXRatio - 0.5) * targetBox.width;
5453
+ const targetOffsetY = (plan.endYRatio - 0.5) * targetBox.height;
5454
+ await dragCaptchaAction(page, sourceLocator, dropTarget, {
5455
+ forceMouse: true,
5456
+ targetOffsetX,
5457
+ targetOffsetY,
5458
+ steps: options.promptDragMoveSteps,
5459
+ holdDelayMs: options.promptDragHoldDelayMs,
5460
+ stepDelayMs: options.promptDragStepDelayMs,
5461
+ beforeReleaseDelayMs: options.promptDragBeforeReleaseDelayMs,
5462
+ afterReleaseDelayMs: options.promptDragAfterReleaseDelayMs,
5463
+ finalMoveRepeats: options.promptDragFinalMoveRepeats
5464
+ });
5465
+ const afterState = await readPromptCaptchaState(frame, options);
5466
+ const accepted = afterState.badgeCount > baselineState.badgeCount || afterState.selectedCount > baselineState.selectedCount;
5467
+ const attemptInfo = {
5468
+ planName: plan.name,
5469
+ sourceRect: rectOf(sourceBox),
5470
+ targetRect: rectOf(targetBox),
5471
+ targetOffsetX: Number(targetOffsetX.toFixed(2)),
5472
+ targetOffsetY: Number(targetOffsetY.toFixed(2)),
5473
+ beforeState: baselineState,
5474
+ afterState,
5475
+ accepted
5476
+ };
5477
+ dragAttempts.push(attemptInfo);
5478
+ logger10.info(
5479
+ `\u9A8C\u8BC1\u7801\u62D6\u62FD\u7B2C ${visualIndex + 1} \u5F20\uFF0C\u65B9\u6848 ${plan.name}\uFF0Cbadge ${baselineState.badgeCount} -> ${afterState.badgeCount}\uFF0Cselected ${baselineState.selectedCount} -> ${afterState.selectedCount}`
5480
+ );
5481
+ if (accepted) {
5482
+ return {
5483
+ accepted: true,
5484
+ dragAttempts
5485
+ };
5486
+ }
5487
+ if (options.promptDragRetryDelayMs > 0) {
5488
+ await page.waitForTimeout(options.promptDragRetryDelayMs);
5489
+ }
5490
+ }
5491
+ await maybeCollectCaptchaDebugInfo(page, frame, iframeLocator, attempt, `drag-${visualIndex + 1}-failed`, options, {
5492
+ visualIndex,
5493
+ dragAttempts,
5494
+ finalState: await readPromptCaptchaState(frame, options)
5495
+ }).catch((error) => {
5496
+ logger10.warn(`\u9A8C\u8BC1\u7801\u62D6\u62FD\u5931\u8D25\u8C03\u8BD5\u6293\u53D6\u5931\u8D25\uFF1A${error?.message || error}`);
5497
+ });
5498
+ return {
5499
+ accepted: false,
5500
+ dragAttempts
5501
+ };
5502
+ };
5171
5503
  async function solveCaptcha(page, options = {}, dependencies = {}) {
5172
5504
  const { callCaptchaRecognitionApi: callCaptchaRecognitionApi2 } = dependencies;
5173
5505
  if (typeof callCaptchaRecognitionApi2 !== "function") {
@@ -5182,7 +5514,7 @@ async function solveCaptcha(page, options = {}, dependencies = {}) {
5182
5514
  return false;
5183
5515
  }
5184
5516
  logger10.info("\u5F53\u524D\u4F7F\u7528\u672Ctool\u2014\u2014\u6D4B\u8BD5\u7248\u672C");
5185
- for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
5517
+ for (let attempt = 1; attempt <= config.maxRetries; attempt += 1) {
5186
5518
  logger10.info(`\u5F00\u59CB\u7B2C ${attempt}/${config.maxRetries} \u6B21 verifycenter \u9A8C\u8BC1\u7801\u8BC6\u522B\u3002`);
5187
5519
  try {
5188
5520
  const captchaContext = await getVerifycenterCaptchaContext(page, config);
@@ -5192,6 +5524,16 @@ async function solveCaptcha(page, options = {}, dependencies = {}) {
5192
5524
  }
5193
5525
  const { iframeLocator, frame } = captchaContext;
5194
5526
  await waitForCaptchaChallengeReady(page, frame, config);
5527
+ await maybeCollectCaptchaDebugInfo(
5528
+ page,
5529
+ frame,
5530
+ iframeLocator,
5531
+ attempt,
5532
+ "ready",
5533
+ config
5534
+ ).catch((error) => {
5535
+ logger10.warn(`\u9A8C\u8BC1\u7801\u8C03\u8BD5\u6293\u53D6\u5931\u8D25\uFF1A${error?.message || error}`);
5536
+ });
5195
5537
  await page.waitForTimeout(config.recognitionDelayMs);
5196
5538
  const screenshotBuffer = await iframeLocator.screenshot();
5197
5539
  const apiResponse = await callCaptchaRecognitionApi2({
@@ -5215,33 +5557,74 @@ async function solveCaptcha(page, options = {}, dependencies = {}) {
5215
5557
  await refreshCaptcha(page, frame, config);
5216
5558
  continue;
5217
5559
  }
5218
- const sourceImages = frame.locator(config.sourceImageSelector);
5219
- const imageCount = await sourceImages.count();
5220
- for (const rawIndex of serialNumbers) {
5221
- let imageIndex = rawIndex;
5222
- if (imageIndex >= imageCount && imageIndex > 0 && imageIndex - 1 < imageCount) {
5223
- imageIndex -= 1;
5224
- }
5225
- if (imageIndex < 0 || imageIndex >= imageCount) {
5226
- throw new Error(`Captcha image index ${rawIndex} is out of range. count=${imageCount}`);
5560
+ const orderedSourceImages = await resolveCaptchaSourceImagesInVisualOrder(frame, config);
5561
+ const normalizedIndexes = normalizeCaptchaImageIndexes(serialNumbers, orderedSourceImages.length);
5562
+ if (normalizedIndexes.length !== serialNumbers.length) {
5563
+ throw new Error(
5564
+ `Captcha image indexes could not be normalized. raw=${serialNumbers.join(", ")}, count=${orderedSourceImages.length}`
5565
+ );
5566
+ }
5567
+ logger10.info(`\u9A8C\u8BC1\u7801\u89C6\u89C9\u4F4D\u5E8F\u6620\u5C04\uFF1A${normalizedIndexes.map((index) => index + 1).join(", ")}`);
5568
+ for (const imageIndex of normalizedIndexes) {
5569
+ if (imageIndex < 0 || imageIndex >= orderedSourceImages.length) {
5570
+ throw new Error(
5571
+ `Captcha image index ${imageIndex} is out of range. count=${orderedSourceImages.length}`
5572
+ );
5227
5573
  }
5228
- const sourceImage = sourceImages.nth(imageIndex);
5574
+ const sourceImage = orderedSourceImages[imageIndex].locator;
5229
5575
  await sourceImage.waitFor({
5230
5576
  state: "visible",
5231
5577
  timeout: config.sourceImageVisibleTimeoutMs
5232
5578
  });
5233
- await dragCaptchaAction(page, sourceImage, dropTarget);
5579
+ const dragResult = await dragPromptCaptchaImage(
5580
+ page,
5581
+ frame,
5582
+ iframeLocator,
5583
+ sourceImage,
5584
+ dropTarget,
5585
+ config,
5586
+ {
5587
+ attempt,
5588
+ visualIndex: imageIndex
5589
+ }
5590
+ );
5591
+ if (!dragResult.accepted) {
5592
+ throw new Error(`Captcha prompt drag was not accepted for visual index ${imageIndex + 1}.`);
5593
+ }
5594
+ if (config.dragBetweenWaitMs > 0) {
5595
+ await page.waitForTimeout(config.dragBetweenWaitMs);
5596
+ }
5234
5597
  }
5235
- const submitted = await clickCaptchaAction(frame, config.submitTexts, { ...config, page }).catch(() => false);
5598
+ const beforeSubmitState = await readPromptCaptchaState(frame, config);
5599
+ logger10.info(
5600
+ `\u63D0\u4EA4\u524D\u9A8C\u8BC1\u7801\u72B6\u6001\uFF1Abadge=${beforeSubmitState.badgeCount}, selected=${beforeSubmitState.selectedCount}, submitDisabled=${beforeSubmitState.submitDisabled}`
5601
+ );
5602
+ const submitted = await clickCaptchaAction(frame, config.submitTexts, {
5603
+ ...config,
5604
+ page,
5605
+ logger: logger10,
5606
+ forceMouse: true,
5607
+ actionVisibleTimeoutMs: config.submitReadyTimeoutMs
5608
+ }).catch(() => false);
5236
5609
  if (!submitted) {
5237
5610
  logger10.warn("\u672A\u627E\u5230\u63D0\u4EA4\u6309\u94AE\uFF0C\u53EF\u80FD\u4F1A\u81EA\u52A8\u63D0\u4EA4\u3002");
5238
5611
  }
5239
5612
  await page.waitForTimeout(config.submitWaitMs);
5613
+ const afterSubmitState = await readPromptCaptchaState(frame, config);
5614
+ logger10.info(
5615
+ `\u63D0\u4EA4\u540E\u9A8C\u8BC1\u7801\u72B6\u6001\uFF1Abadge=${afterSubmitState.badgeCount}, selected=${afterSubmitState.selectedCount}, submitDisabled=${afterSubmitState.submitDisabled}`
5616
+ );
5240
5617
  const stillVisible = await iframeLocator.isVisible({ timeout: config.containerVisibleTimeoutMs }).catch(() => false);
5241
5618
  if (!stillVisible) {
5242
5619
  logger10.info("\u9A8C\u8BC1\u7801\u8BC6\u522B\u5E76\u63D0\u4EA4\u6210\u529F\u3002");
5243
5620
  return true;
5244
5621
  }
5622
+ await maybeCollectCaptchaDebugInfo(page, frame, iframeLocator, attempt, "submit-still-visible", config, {
5623
+ beforeSubmitState,
5624
+ afterSubmitState
5625
+ }).catch((error) => {
5626
+ logger10.warn(`\u63D0\u4EA4\u540E\u9A8C\u8BC1\u7801\u8C03\u8BD5\u6293\u53D6\u5931\u8D25\uFF1A${error?.message || error}`);
5627
+ });
5245
5628
  logger10.warn("\u63D0\u4EA4\u540E\u9A8C\u8BC1\u7801 iframe \u4ECD\u7136\u53EF\u89C1\uFF0C\u51C6\u5907\u5237\u65B0\u540E\u91CD\u8BD5\u3002");
5246
5629
  await page.waitForTimeout(2e3);
5247
5630
  await refreshCaptcha(page, frame, config);
@@ -5684,14 +6067,14 @@ var Mutation = {
5684
6067
  const isFrameElement = tagName === "IFRAME" || tagName === "FRAME";
5685
6068
  const nodeName = descriptor?.id || descriptor?.name || "no-id";
5686
6069
  let source = "main";
5687
- let path2 = `${selector}[${index}]`;
6070
+ let path3 = `${selector}[${index}]`;
5688
6071
  let text = "";
5689
6072
  let html = "";
5690
6073
  let frameUrl = "";
5691
6074
  let readyState = "";
5692
6075
  if (isFrameElement) {
5693
6076
  source = "iframe";
5694
- path2 = `${selector}[${index}]::iframe(${nodeName})`;
6077
+ path3 = `${selector}[${index}]::iframe(${nodeName})`;
5695
6078
  const frame = await handle.contentFrame();
5696
6079
  if (frame) {
5697
6080
  try {
@@ -5721,7 +6104,7 @@ var Mutation = {
5721
6104
  items.push({
5722
6105
  selector,
5723
6106
  source,
5724
- path: path2,
6107
+ path: path3,
5725
6108
  text,
5726
6109
  html,
5727
6110
  frameUrl,