@skrillex1224/playwright-toolkit 2.1.217 → 2.1.218

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.cjs CHANGED
@@ -2975,13 +2975,45 @@ var LiveView = {
2975
2975
  // src/captcha-monitor.js
2976
2976
  var import_uuid = require("uuid");
2977
2977
  var logger9 = createInternalLogger("Captcha");
2978
+ var DEFAULT_BYTEDANCE_CAPTCHA_TOKEN = "eKJvBfwfN0YRav0-VD_44E2VBSfm7l0YtddUQ7cFySI";
2979
+ var DEFAULT_BYTEDANCE_CAPTCHA_OPTIONS = Object.freeze({
2980
+ token: DEFAULT_BYTEDANCE_CAPTCHA_TOKEN,
2981
+ apiUrl: "https://api.jfbym.com/api/YmServer/customApi",
2982
+ apiType: "31234",
2983
+ maxRetries: 3,
2984
+ containerSelector: "#captcha_container",
2985
+ iframeSelector: 'iframe[src*="verifycenter"]',
2986
+ iframeFallbackSelector: "iframe",
2987
+ sourceImageSelector: "div.canvas-container",
2988
+ dropTargetContainerSelector: "#captcha_verify_image div",
2989
+ dropTargetTexts: ["\u62D6\u62FD\u5230\u8FD9\u91CC"],
2990
+ refreshTexts: ["\u5237\u65B0"],
2991
+ submitTexts: ["\u63D0\u4EA4"],
2992
+ recognitionSuccessCode: 1e4,
2993
+ containerVisibleTimeoutMs: 2e3,
2994
+ iframeVisibleTimeoutMs: 12e3,
2995
+ iframeFallbackVisibleTimeoutMs: 4e3,
2996
+ contentFrameResolveRetries: 5,
2997
+ contentFrameResolveDelayMs: 500,
2998
+ actionVisibleTimeoutMs: 1500,
2999
+ sourceImageVisibleTimeoutMs: 3e3,
3000
+ challengeReadyTimeoutMs: 15e3,
3001
+ challengeReadyPollMs: 300,
3002
+ loadingIndicatorVisibleTimeoutMs: 200,
3003
+ loadingTexts: ["\u52A0\u8F7D\u4E2D", "\u52A0\u8F7D\u4E2D..."],
3004
+ recognitionDelayMs: 2e3,
3005
+ refreshWaitMs: 3e3,
3006
+ submitWaitMs: 3e3,
3007
+ retryDelayBaseMs: 2e3,
3008
+ retryDelayStepMs: 1e3
3009
+ });
2978
3010
  function useCaptchaMonitor(page, options) {
2979
3011
  const { domSelector, urlPattern, onDetected } = options;
2980
3012
  if (!domSelector && !urlPattern) {
2981
- throw new Error("[CaptchaMonitor] \u5FC5\u987B\u63D0\u4F9B domSelector \u6216 urlPattern \u81F3\u5C11\u4E00\u4E2A");
3013
+ throw new Error("[CaptchaMonitor] \u5FC5\u987B\u63D0\u4F9B domSelector \u6216 urlPattern\u3002");
2982
3014
  }
2983
3015
  if (!onDetected || typeof onDetected !== "function") {
2984
- throw new Error("[CaptchaMonitor] onDetected \u5FC5\u987B\u662F\u4E00\u4E2A\u51FD\u6570");
3016
+ throw new Error("[CaptchaMonitor] onDetected \u5FC5\u987B\u662F\u51FD\u6570\u3002");
2985
3017
  }
2986
3018
  let isStopped = false;
2987
3019
  let isHandling = false;
@@ -3002,28 +3034,22 @@ function useCaptchaMonitor(page, options) {
3002
3034
  const cleanerName = `__c_cleaner_${(0, import_uuid.v4)().replace(/-/g, "_")}`;
3003
3035
  page.exposeFunction(exposedFunctionName, triggerDetected).catch(() => {
3004
3036
  });
3005
- page.addInitScript(({ selector, callbackName, cleanerName: cleanerName2 }) => {
3037
+ page.addInitScript(({ selector, callbackName, cleanerName: cleanupName }) => {
3006
3038
  (() => {
3007
3039
  let observer = null;
3008
3040
  const checkAndReport = () => {
3009
3041
  const element = document.querySelector(selector);
3010
- if (element) {
3011
- if (window[callbackName]) {
3012
- window[callbackName]();
3013
- }
3014
- return true;
3042
+ if (!element) {
3043
+ return false;
3015
3044
  }
3016
- return false;
3045
+ if (window[callbackName]) {
3046
+ window[callbackName]();
3047
+ }
3048
+ return true;
3017
3049
  };
3018
3050
  checkAndReport();
3019
3051
  observer = new MutationObserver((mutations) => {
3020
- let shouldCheck = false;
3021
- for (const mutation of mutations) {
3022
- if (mutation.addedNodes.length > 0) {
3023
- shouldCheck = true;
3024
- break;
3025
- }
3026
- }
3052
+ const shouldCheck = mutations.some((mutation) => mutation.addedNodes.length > 0);
3027
3053
  if (shouldCheck && observer) {
3028
3054
  checkAndReport();
3029
3055
  }
@@ -3039,7 +3065,7 @@ function useCaptchaMonitor(page, options) {
3039
3065
  } else {
3040
3066
  mountObserver();
3041
3067
  }
3042
- window[cleanerName2] = () => {
3068
+ window[cleanupName] = () => {
3043
3069
  if (observer) {
3044
3070
  observer.disconnect();
3045
3071
  observer = null;
@@ -3047,7 +3073,7 @@ function useCaptchaMonitor(page, options) {
3047
3073
  };
3048
3074
  })();
3049
3075
  }, { selector: domSelector, callbackName: exposedFunctionName, cleanerName });
3050
- logger9.success("useCaptchaMonitor", `DOM \u76D1\u63A7\u5DF2\u542F\u7528: ${domSelector}`);
3076
+ logger9.success("useCaptchaMonitor", `DOM \u76D1\u63A7\u5DF2\u542F\u7528\uFF1A${domSelector}`);
3051
3077
  cleanupFns.push(async () => {
3052
3078
  try {
3053
3079
  await page.evaluate((name) => {
@@ -3056,28 +3082,29 @@ function useCaptchaMonitor(page, options) {
3056
3082
  delete window[name];
3057
3083
  }
3058
3084
  }, cleanerName);
3059
- } catch (e) {
3085
+ } catch {
3060
3086
  }
3061
3087
  });
3062
3088
  }
3063
3089
  if (urlPattern) {
3064
3090
  frameHandler = async (frame) => {
3065
- if (frame === page.mainFrame()) {
3066
- const currentUrl = page.url();
3067
- if (currentUrl.includes(urlPattern)) {
3068
- await triggerDetected();
3069
- }
3091
+ if (frame !== page.mainFrame()) {
3092
+ return;
3093
+ }
3094
+ const currentUrl = page.url();
3095
+ if (currentUrl.includes(urlPattern)) {
3096
+ await triggerDetected();
3070
3097
  }
3071
3098
  };
3072
3099
  page.on("framenavigated", frameHandler);
3073
- logger9.success("useCaptchaMonitor", `URL \u76D1\u63A7\u5DF2\u542F\u7528: ${urlPattern}`);
3100
+ logger9.success("useCaptchaMonitor", `URL \u76D1\u63A7\u5DF2\u542F\u7528\uFF1A${urlPattern}`);
3074
3101
  cleanupFns.push(async () => {
3075
3102
  page.off("framenavigated", frameHandler);
3076
3103
  });
3077
3104
  }
3078
3105
  return {
3079
3106
  stop: async () => {
3080
- logger9.info("useCaptchaMonitor", "\u6B63\u5728\u505C\u6B62\u76D1\u63A7...");
3107
+ logger9.info("\u6B63\u5728\u505C\u6B62\u9A8C\u8BC1\u7801\u76D1\u63A7...");
3081
3108
  for (const fn of cleanupFns) {
3082
3109
  await fn();
3083
3110
  }
@@ -3085,8 +3112,281 @@ function useCaptchaMonitor(page, options) {
3085
3112
  }
3086
3113
  };
3087
3114
  }
3115
+ var callCaptchaRecognitionApi = async (imageBase64, { apiUrl, apiType, token }) => {
3116
+ const response = await fetch(apiUrl, {
3117
+ method: "POST",
3118
+ headers: {
3119
+ "Content-Type": "application/json"
3120
+ },
3121
+ body: JSON.stringify({
3122
+ type: apiType,
3123
+ image: imageBase64,
3124
+ token
3125
+ })
3126
+ });
3127
+ if (!response.ok) {
3128
+ throw new Error(`Captcha API request failed with status ${response.status}`);
3129
+ }
3130
+ return await response.json();
3131
+ };
3132
+ var extractCaptchaSerialNumbers = (apiResponse) => {
3133
+ const serialNumbers = apiResponse?.data?.data?.serial_number;
3134
+ if (!Array.isArray(serialNumbers)) {
3135
+ return [];
3136
+ }
3137
+ return serialNumbers.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value >= 0);
3138
+ };
3139
+ var waitForVisible = async (locator, timeout) => {
3140
+ try {
3141
+ await locator.waitFor({
3142
+ state: "visible",
3143
+ timeout
3144
+ });
3145
+ return true;
3146
+ } catch {
3147
+ return false;
3148
+ }
3149
+ };
3150
+ var isAnyCaptchaTextVisible = async (frame, texts, timeout) => {
3151
+ for (const text of texts || []) {
3152
+ if (!text) {
3153
+ continue;
3154
+ }
3155
+ const candidates = [
3156
+ frame.getByText(text, { exact: false }).first(),
3157
+ frame.locator(`text=${text}`).first()
3158
+ ];
3159
+ for (const candidate of candidates) {
3160
+ const isVisible = await candidate.isVisible({ timeout }).catch(() => false);
3161
+ if (isVisible) {
3162
+ return true;
3163
+ }
3164
+ }
3165
+ }
3166
+ return false;
3167
+ };
3168
+ var waitForCaptchaChallengeReady = async (page, frame, options) => {
3169
+ const deadline = Date.now() + options.challengeReadyTimeoutMs;
3170
+ let hasSeenLoading = false;
3171
+ while (Date.now() < deadline) {
3172
+ const isLoadingVisible = await isAnyCaptchaTextVisible(
3173
+ frame,
3174
+ options.loadingTexts,
3175
+ options.loadingIndicatorVisibleTimeoutMs
3176
+ );
3177
+ hasSeenLoading = hasSeenLoading || isLoadingVisible;
3178
+ const sourceImages = frame.locator(options.sourceImageSelector);
3179
+ const imageCount = await sourceImages.count().catch(() => 0);
3180
+ const hasVisibleSourceImage = imageCount > 0 ? await sourceImages.first().isVisible({ timeout: options.loadingIndicatorVisibleTimeoutMs }).catch(() => false) : false;
3181
+ if (!isLoadingVisible && hasVisibleSourceImage) {
3182
+ logger9.info(hasSeenLoading ? "\u9A8C\u8BC1\u7801\u56FE\u7247\u5DF2\u52A0\u8F7D\u5B8C\u6210\u3002" : "\u9A8C\u8BC1\u7801\u56FE\u7247\u5DF2\u5C31\u7EEA\u3002");
3183
+ return;
3184
+ }
3185
+ await page.waitForTimeout(options.challengeReadyPollMs);
3186
+ }
3187
+ throw new Error("Captcha challenge is still loading and did not become ready in time.");
3188
+ };
3189
+ var resolveContentFrame = async (page, iframeLocator, options) => {
3190
+ for (let attempt = 1; attempt <= options.contentFrameResolveRetries; attempt++) {
3191
+ const iframeHandle = await iframeLocator.elementHandle();
3192
+ const frame = await iframeHandle?.contentFrame();
3193
+ if (frame) {
3194
+ return frame;
3195
+ }
3196
+ if (attempt < options.contentFrameResolveRetries) {
3197
+ await page.waitForTimeout(options.contentFrameResolveDelayMs);
3198
+ }
3199
+ }
3200
+ return null;
3201
+ };
3202
+ var getVerifycenterCaptchaContext = async (page, options) => {
3203
+ const captchaContainer = page.locator(options.containerSelector).first();
3204
+ const isContainerVisible = await waitForVisible(
3205
+ captchaContainer,
3206
+ options.containerVisibleTimeoutMs
3207
+ );
3208
+ if (!isContainerVisible) {
3209
+ return null;
3210
+ }
3211
+ logger9.info("\u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801\u5BB9\u5668\uFF0C\u5F00\u59CB\u7B49\u5F85 iframe \u52A0\u8F7D\u3002");
3212
+ let iframeLocator = page.locator(options.iframeSelector).first();
3213
+ let isIframeVisible = await waitForVisible(
3214
+ iframeLocator,
3215
+ options.iframeVisibleTimeoutMs
3216
+ );
3217
+ if (!isIframeVisible) {
3218
+ logger9.warn("\u672A\u5728\u9884\u671F\u9009\u62E9\u5668\u4E2D\u627E\u5230 verifycenter iframe\uFF0C\u5C1D\u8BD5\u5BB9\u5668\u5185\u4EFB\u610F iframe\u3002");
3219
+ iframeLocator = captchaContainer.locator(options.iframeFallbackSelector).first();
3220
+ isIframeVisible = await waitForVisible(
3221
+ iframeLocator,
3222
+ options.iframeFallbackVisibleTimeoutMs
3223
+ );
3224
+ }
3225
+ if (!isIframeVisible) {
3226
+ throw new Error("verifycenter iframe not found inside captcha container.");
3227
+ }
3228
+ logger9.info("\u9A8C\u8BC1\u7801 iframe \u5DF2\u53EF\u89C1\uFF0C\u5F00\u59CB\u89E3\u6790\u5185\u5BB9 frame\u3002");
3229
+ const frame = await resolveContentFrame(page, iframeLocator, options);
3230
+ if (!frame) {
3231
+ throw new Error("Failed to resolve verifycenter iframe content frame.");
3232
+ }
3233
+ return { iframeLocator, frame };
3234
+ };
3235
+ var clickCaptchaAction = async (frame, texts, options) => {
3236
+ for (const text of texts) {
3237
+ const candidates = [
3238
+ frame.getByText(text, { exact: false }).first(),
3239
+ frame.locator(`text=${text}`).first()
3240
+ ];
3241
+ for (const candidate of candidates) {
3242
+ const isVisible = await waitForVisible(candidate, options.actionVisibleTimeoutMs);
3243
+ if (!isVisible) {
3244
+ continue;
3245
+ }
3246
+ await candidate.click();
3247
+ return true;
3248
+ }
3249
+ }
3250
+ return false;
3251
+ };
3252
+ var findCaptchaDropTarget = async (frame, options) => {
3253
+ for (const text of options.dropTargetTexts) {
3254
+ const candidates = [
3255
+ frame.locator(options.dropTargetContainerSelector).filter({ hasText: text }).first(),
3256
+ frame.getByText(text, { exact: false }).first()
3257
+ ];
3258
+ for (const candidate of candidates) {
3259
+ const isVisible = await waitForVisible(candidate, options.actionVisibleTimeoutMs);
3260
+ if (isVisible) {
3261
+ return candidate;
3262
+ }
3263
+ }
3264
+ }
3265
+ return null;
3266
+ };
3267
+ var dragCaptchaWithMouse = async (page, sourceLocator, targetLocator) => {
3268
+ const sourceBox = await sourceLocator.boundingBox();
3269
+ const targetBox = await targetLocator.boundingBox();
3270
+ if (!sourceBox || !targetBox) {
3271
+ throw new Error("Unable to resolve captcha drag coordinates.");
3272
+ }
3273
+ const startX = sourceBox.x + sourceBox.width / 2;
3274
+ const startY = sourceBox.y + sourceBox.height / 2;
3275
+ const endX = targetBox.x + targetBox.width / 2;
3276
+ const endY = targetBox.y + targetBox.height / 2;
3277
+ const steps = 10;
3278
+ const liftOffsetX = Math.min(18, Math.max(8, sourceBox.width * 0.12));
3279
+ const liftOffsetY = Math.min(12, Math.max(4, sourceBox.height * 0.08));
3280
+ await page.mouse.move(startX, startY, { steps: 8 });
3281
+ await page.waitForTimeout(250);
3282
+ await page.mouse.down();
3283
+ await page.waitForTimeout(350);
3284
+ await page.mouse.move(startX + liftOffsetX, startY + liftOffsetY, { steps: 6 });
3285
+ await page.waitForTimeout(250);
3286
+ for (let step = 1; step <= steps; step++) {
3287
+ const progress = step / steps;
3288
+ const easedProgress = 1 - (1 - progress) * (1 - progress);
3289
+ const currentX = startX + liftOffsetX + (endX - startX - liftOffsetX) * easedProgress;
3290
+ const currentY = startY + liftOffsetY + (endY - startY - liftOffsetY) * easedProgress;
3291
+ await page.mouse.move(currentX, currentY, { steps: 2 });
3292
+ await page.waitForTimeout(90);
3293
+ }
3294
+ await page.waitForTimeout(100);
3295
+ await page.mouse.up();
3296
+ await page.waitForTimeout(100);
3297
+ };
3298
+ var refreshCaptcha = async (page, frame, options) => {
3299
+ const clicked = await clickCaptchaAction(frame, options.refreshTexts, options).catch(() => false);
3300
+ if (!clicked) {
3301
+ logger9.warn("Refresh button not found.");
3302
+ return false;
3303
+ }
3304
+ await page.waitForTimeout(options.refreshWaitMs);
3305
+ return true;
3306
+ };
3307
+ async function solveBytedanceCaptcha(page, options = {}) {
3308
+ const config = {
3309
+ ...DEFAULT_BYTEDANCE_CAPTCHA_OPTIONS,
3310
+ ...options
3311
+ };
3312
+ if (!config.token) {
3313
+ logger9.warn("\u7F3A\u5C11\u9A8C\u8BC1\u7801 token\uFF0C\u8DF3\u8FC7\u81EA\u52A8\u8BC6\u522B\u3002");
3314
+ return false;
3315
+ }
3316
+ for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
3317
+ logger9.info(`\u5F00\u59CB\u7B2C ${attempt}/${config.maxRetries} \u6B21 verifycenter \u9A8C\u8BC1\u7801\u8BC6\u522B\u3002`);
3318
+ try {
3319
+ const captchaContext = await getVerifycenterCaptchaContext(page, config);
3320
+ if (!captchaContext) {
3321
+ logger9.info("Captcha container is not visible anymore.");
3322
+ return true;
3323
+ }
3324
+ const { iframeLocator, frame } = captchaContext;
3325
+ await waitForCaptchaChallengeReady(page, frame, config);
3326
+ await page.waitForTimeout(config.recognitionDelayMs);
3327
+ const screenshotBuffer = await iframeLocator.screenshot();
3328
+ const apiResponse = await callCaptchaRecognitionApi(
3329
+ screenshotBuffer.toString("base64"),
3330
+ config
3331
+ );
3332
+ const serialNumbers = extractCaptchaSerialNumbers(apiResponse);
3333
+ if (apiResponse?.code !== config.recognitionSuccessCode || serialNumbers.length === 0) {
3334
+ logger9.warn(
3335
+ `\u9A8C\u8BC1\u7801\u8BC6\u522B\u5931\u8D25\u3002code=${apiResponse?.code}, msg=${apiResponse?.msg || "unknown"}`
3336
+ );
3337
+ await refreshCaptcha(page, frame, config);
3338
+ continue;
3339
+ }
3340
+ logger9.info(`\u9A8C\u8BC1\u7801\u8BC6\u522B\u6210\u529F\uFF0C\u5E8F\u53F7\uFF1A${serialNumbers.join(", ")}`);
3341
+ const dropTarget = await findCaptchaDropTarget(frame, config);
3342
+ if (!dropTarget) {
3343
+ logger9.warn("\u672A\u627E\u5230\u9A8C\u8BC1\u7801\u62D6\u62FD\u76EE\u6807\u533A\u57DF\u3002");
3344
+ await refreshCaptcha(page, frame, config);
3345
+ continue;
3346
+ }
3347
+ const sourceImages = frame.locator(config.sourceImageSelector);
3348
+ const imageCount = await sourceImages.count();
3349
+ for (const rawIndex of serialNumbers) {
3350
+ let imageIndex = rawIndex;
3351
+ if (imageIndex >= imageCount && imageIndex > 0 && imageIndex - 1 < imageCount) {
3352
+ imageIndex -= 1;
3353
+ }
3354
+ if (imageIndex < 0 || imageIndex >= imageCount) {
3355
+ throw new Error(`Captcha image index ${rawIndex} is out of range. count=${imageCount}`);
3356
+ }
3357
+ const sourceImage = sourceImages.nth(imageIndex);
3358
+ await sourceImage.waitFor({
3359
+ state: "visible",
3360
+ timeout: config.sourceImageVisibleTimeoutMs
3361
+ });
3362
+ await dragCaptchaWithMouse(page, sourceImage, dropTarget);
3363
+ }
3364
+ const submitted = await clickCaptchaAction(frame, config.submitTexts, config).catch(() => false);
3365
+ if (!submitted) {
3366
+ logger9.warn("\u672A\u627E\u5230\u63D0\u4EA4\u6309\u94AE\uFF0C\u53EF\u80FD\u4F1A\u81EA\u52A8\u63D0\u4EA4\u3002");
3367
+ }
3368
+ await page.waitForTimeout(config.submitWaitMs);
3369
+ const stillVisible = await iframeLocator.isVisible({ timeout: config.containerVisibleTimeoutMs }).catch(() => false);
3370
+ if (!stillVisible) {
3371
+ logger9.info("\u9A8C\u8BC1\u7801\u8BC6\u522B\u5E76\u63D0\u4EA4\u6210\u529F\u3002");
3372
+ return true;
3373
+ }
3374
+ logger9.warn("\u63D0\u4EA4\u540E\u9A8C\u8BC1\u7801 iframe \u4ECD\u7136\u53EF\u89C1\uFF0C\u51C6\u5907\u5237\u65B0\u540E\u91CD\u8BD5\u3002");
3375
+ await page.waitForTimeout(2e3);
3376
+ await refreshCaptcha(page, frame, config);
3377
+ } catch (error) {
3378
+ logger9.error(`\u7B2C ${attempt}/${config.maxRetries} \u6B21\u9A8C\u8BC1\u7801\u8BC6\u522B\u5931\u8D25\uFF1A${error?.message || error}`);
3379
+ }
3380
+ if (attempt < config.maxRetries) {
3381
+ await page.waitForTimeout(config.retryDelayBaseMs + attempt * config.retryDelayStepMs);
3382
+ }
3383
+ }
3384
+ logger9.error(`\u91CD\u8BD5 ${config.maxRetries} \u6B21\u540E\uFF0C\u9A8C\u8BC1\u7801\u4ECD\u672A\u8BC6\u522B\u6210\u529F\u3002`);
3385
+ return false;
3386
+ }
3088
3387
  var Captcha = {
3089
- useCaptchaMonitor
3388
+ useCaptchaMonitor,
3389
+ solveBytedanceCaptcha
3090
3390
  };
3091
3391
 
3092
3392
  // src/mutation.js