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