@skrillex1224/playwright-toolkit 2.1.21 → 2.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -17
- package/dist/index.cjs +259 -177
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +259 -177
- package/dist/index.js.map +3 -3
- package/index.d.ts +28 -10
- package/package.json +2 -4
package/dist/index.cjs
CHANGED
|
@@ -352,133 +352,243 @@ var Utils = {
|
|
|
352
352
|
};
|
|
353
353
|
|
|
354
354
|
// src/stealth.js
|
|
355
|
-
var logger3 = createLogger("
|
|
356
|
-
var
|
|
355
|
+
var logger3 = createLogger("AntiDetect");
|
|
356
|
+
var BASE_CONFIG = Object.freeze({
|
|
357
|
+
locale: "zh-CN",
|
|
358
|
+
acceptLanguage: "zh-CN,zh;q=0.9",
|
|
359
|
+
timezoneId: "Asia/Shanghai",
|
|
360
|
+
timezoneOffset: -480,
|
|
361
|
+
geolocation: null
|
|
362
|
+
});
|
|
363
|
+
var DEFAULT_LAUNCH_ARGS = [
|
|
364
|
+
"--disable-blink-features=AutomationControlled",
|
|
365
|
+
"--no-sandbox",
|
|
366
|
+
"--disable-setuid-sandbox",
|
|
367
|
+
"--window-position=0,0",
|
|
368
|
+
`--lang=${BASE_CONFIG.locale}`
|
|
369
|
+
];
|
|
370
|
+
var ADVANCED_LAUNCH_ARGS = [
|
|
371
|
+
...DEFAULT_LAUNCH_ARGS,
|
|
372
|
+
"--disable-dev-shm-usage",
|
|
373
|
+
"--disable-background-networking",
|
|
374
|
+
"--disable-default-apps",
|
|
375
|
+
"--disable-extensions",
|
|
376
|
+
"--disable-sync",
|
|
377
|
+
"--disable-translate",
|
|
378
|
+
"--metrics-recording-only",
|
|
379
|
+
"--mute-audio",
|
|
380
|
+
"--no-first-run"
|
|
381
|
+
];
|
|
382
|
+
var CONTEXT_CONFIG_CACHE = /* @__PURE__ */ new WeakMap();
|
|
383
|
+
function buildFingerprintOptions(locale) {
|
|
384
|
+
return {
|
|
385
|
+
browsers: [{ name: "chrome", minVersion: 110 }],
|
|
386
|
+
devices: ["desktop"],
|
|
387
|
+
operatingSystems: ["windows", "macos", "linux"],
|
|
388
|
+
locales: [locale]
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function parseAcceptLanguage(acceptLanguage) {
|
|
392
|
+
if (!acceptLanguage) return [];
|
|
393
|
+
return acceptLanguage.split(",").map((part) => part.trim().split(";")[0]).filter(Boolean);
|
|
394
|
+
}
|
|
395
|
+
function normalizeLanguages(acceptLanguage, fallbackLocale) {
|
|
396
|
+
const languages = parseAcceptLanguage(acceptLanguage);
|
|
397
|
+
if (languages.length === 0) return [fallbackLocale];
|
|
398
|
+
if (!languages.includes(fallbackLocale)) {
|
|
399
|
+
return [fallbackLocale, ...languages];
|
|
400
|
+
}
|
|
401
|
+
return languages;
|
|
402
|
+
}
|
|
403
|
+
function getOperatingSystemsFromUserAgent(userAgent) {
|
|
404
|
+
const lowerUA = userAgent.toLowerCase();
|
|
405
|
+
if (lowerUA.includes("windows")) return ["windows"];
|
|
406
|
+
if (lowerUA.includes("mac os") || lowerUA.includes("macintosh")) return ["macos"];
|
|
407
|
+
if (lowerUA.includes("linux")) return ["linux"];
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
function buildContextConfigKey(config) {
|
|
411
|
+
return JSON.stringify({
|
|
412
|
+
locale: config.locale,
|
|
413
|
+
acceptLanguage: config.acceptLanguage,
|
|
414
|
+
timezoneId: config.timezoneId,
|
|
415
|
+
timezoneOffset: config.timezoneOffset
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async function applyContextSettings(context, config, languages, permissions, injectLocaleTimezone) {
|
|
419
|
+
const contextKey = buildContextConfigKey(config);
|
|
420
|
+
const cached = CONTEXT_CONFIG_CACHE.get(context);
|
|
421
|
+
const isFirstInit = !cached;
|
|
422
|
+
const effectiveConfig = cached?.config || config;
|
|
423
|
+
const effectiveLanguages = cached?.languages || languages;
|
|
424
|
+
if (isFirstInit) {
|
|
425
|
+
CONTEXT_CONFIG_CACHE.set(context, {
|
|
426
|
+
key: contextKey,
|
|
427
|
+
config,
|
|
428
|
+
languages
|
|
429
|
+
});
|
|
430
|
+
} else if (cached.key !== contextKey) {
|
|
431
|
+
logger3.warn("applyContext", "Context already initialized; ignore conflicting locale/timezone.");
|
|
432
|
+
}
|
|
433
|
+
await context.setExtraHTTPHeaders({
|
|
434
|
+
"accept-language": effectiveConfig.acceptLanguage
|
|
435
|
+
});
|
|
436
|
+
if (isFirstInit) {
|
|
437
|
+
await context.addInitScript(({ locale, timezoneId, timezoneOffset, languages: languages2, applyLocaleTimezone }) => {
|
|
438
|
+
const originalDateTimeFormat = Intl.DateTimeFormat;
|
|
439
|
+
if (applyLocaleTimezone) {
|
|
440
|
+
Intl.DateTimeFormat = function(locales, initOptions) {
|
|
441
|
+
const nextLocales = locales || locale;
|
|
442
|
+
const nextOptions = initOptions ? { ...initOptions } : {};
|
|
443
|
+
nextOptions.timeZone = nextOptions.timeZone || timezoneId;
|
|
444
|
+
return new originalDateTimeFormat(nextLocales, nextOptions);
|
|
445
|
+
};
|
|
446
|
+
Intl.DateTimeFormat.prototype = originalDateTimeFormat.prototype;
|
|
447
|
+
Date.prototype.getTimezoneOffset = function() {
|
|
448
|
+
return timezoneOffset;
|
|
449
|
+
};
|
|
450
|
+
Object.defineProperty(navigator, "language", { get: () => languages2[0] });
|
|
451
|
+
Object.defineProperty(navigator, "languages", { get: () => languages2 });
|
|
452
|
+
}
|
|
453
|
+
Object.defineProperty(navigator, "webdriver", { get: () => void 0 });
|
|
454
|
+
}, {
|
|
455
|
+
locale: effectiveConfig.locale,
|
|
456
|
+
timezoneId: effectiveConfig.timezoneId,
|
|
457
|
+
timezoneOffset: effectiveConfig.timezoneOffset,
|
|
458
|
+
languages: effectiveLanguages,
|
|
459
|
+
applyLocaleTimezone: injectLocaleTimezone
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
if (effectiveConfig.geolocation) {
|
|
463
|
+
await context.setGeolocation(effectiveConfig.geolocation);
|
|
464
|
+
await context.grantPermissions(["geolocation"]);
|
|
465
|
+
}
|
|
466
|
+
if (permissions?.length) {
|
|
467
|
+
await context.grantPermissions(permissions);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function resolveConfig(overrides = {}) {
|
|
471
|
+
return {
|
|
472
|
+
...BASE_CONFIG,
|
|
473
|
+
...overrides,
|
|
474
|
+
geolocation: overrides.geolocation === null ? null : overrides.geolocation || BASE_CONFIG.geolocation
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
var AntiDetect = {
|
|
478
|
+
/**
|
|
479
|
+
* 获取统一的基础配置(中国、桌面端、中文语言)。
|
|
480
|
+
*/
|
|
481
|
+
getBaseConfig() {
|
|
482
|
+
return { ...BASE_CONFIG };
|
|
483
|
+
},
|
|
484
|
+
/**
|
|
485
|
+
* 用于 Crawlee fingerprint generator 的统一配置(桌面端)。
|
|
486
|
+
*/
|
|
487
|
+
getFingerprintGeneratorOptions() {
|
|
488
|
+
return buildFingerprintOptions(BASE_CONFIG.locale);
|
|
489
|
+
},
|
|
490
|
+
/**
|
|
491
|
+
* 获取基础启动参数。
|
|
492
|
+
*/
|
|
493
|
+
getLaunchArgs() {
|
|
494
|
+
return [...DEFAULT_LAUNCH_ARGS];
|
|
495
|
+
},
|
|
496
|
+
/**
|
|
497
|
+
* 获取增强启动参数(高风险场景)。
|
|
498
|
+
*/
|
|
499
|
+
getAdvancedLaunchArgs() {
|
|
500
|
+
return [...ADVANCED_LAUNCH_ARGS];
|
|
501
|
+
},
|
|
502
|
+
/**
|
|
503
|
+
* 统一应用到 BrowserContext(时区/语言/权限/地理位置)。
|
|
504
|
+
*
|
|
505
|
+
* @param {import('playwright').BrowserContext} context
|
|
506
|
+
* @param {Object} [options]
|
|
507
|
+
* @param {string} [options.locale]
|
|
508
|
+
* @param {string} [options.acceptLanguage]
|
|
509
|
+
* @param {string} [options.timezoneId]
|
|
510
|
+
* @param {number} [options.timezoneOffset]
|
|
511
|
+
* @param {import('playwright').Geolocation|null} [options.geolocation]
|
|
512
|
+
* @param {string[]} [options.permissions]
|
|
513
|
+
*/
|
|
514
|
+
async applyContext(context, options = {}) {
|
|
515
|
+
const config = resolveConfig(options);
|
|
516
|
+
const languages = normalizeLanguages(config.acceptLanguage, config.locale);
|
|
517
|
+
const permissions = Array.isArray(options.permissions) ? options.permissions : [];
|
|
518
|
+
await applyContextSettings(context, config, languages, permissions, true);
|
|
519
|
+
logger3.success("applyContext", `${config.locale} | ${config.timezoneId}`);
|
|
520
|
+
},
|
|
357
521
|
/**
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
* @param {import('playwright').Page} page
|
|
522
|
+
* 统一应用到 Page(Context + 视口同步)。
|
|
523
|
+
*
|
|
524
|
+
* @param {import('playwright').Page} page
|
|
525
|
+
* @param {Object} [options] - 传递给 applyContext 的选项
|
|
526
|
+
*/
|
|
527
|
+
async applyPage(page, options = {}) {
|
|
528
|
+
const config = resolveConfig(options);
|
|
529
|
+
const languages = normalizeLanguages(config.acceptLanguage, config.locale);
|
|
530
|
+
const permissions = Array.isArray(options.permissions) ? options.permissions : [];
|
|
531
|
+
let injectLocaleTimezone = true;
|
|
532
|
+
try {
|
|
533
|
+
const env = await page.evaluate(() => ({
|
|
534
|
+
language: navigator.language,
|
|
535
|
+
languages: Array.isArray(navigator.languages) ? navigator.languages : [],
|
|
536
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
537
|
+
tzOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset()
|
|
538
|
+
}));
|
|
539
|
+
const languageMatch = env.language === languages[0];
|
|
540
|
+
const timeZoneMatch = env.timeZone === config.timezoneId && env.tzOffset === config.timezoneOffset;
|
|
541
|
+
injectLocaleTimezone = !(languageMatch && timeZoneMatch);
|
|
542
|
+
} catch (e) {
|
|
543
|
+
injectLocaleTimezone = true;
|
|
544
|
+
}
|
|
545
|
+
await applyContextSettings(page.context(), config, languages, permissions, injectLocaleTimezone);
|
|
546
|
+
await this.syncViewportWithScreen(page);
|
|
547
|
+
},
|
|
548
|
+
/**
|
|
549
|
+
* 同步 Page 视口到 window.screen,避免视口/屏幕不一致检测。
|
|
361
550
|
*/
|
|
362
551
|
async syncViewportWithScreen(page) {
|
|
363
552
|
try {
|
|
364
553
|
const screen = await page.evaluate(() => ({
|
|
365
554
|
width: window.screen.width,
|
|
366
|
-
height: window.screen.height
|
|
367
|
-
availWidth: window.screen.availWidth,
|
|
368
|
-
availHeight: window.screen.availHeight
|
|
555
|
+
height: window.screen.height
|
|
369
556
|
}));
|
|
370
557
|
await page.setViewportSize({
|
|
371
558
|
width: screen.width,
|
|
372
559
|
height: screen.height
|
|
373
560
|
});
|
|
374
|
-
logger3.success("
|
|
561
|
+
logger3.success("syncViewport", `size=${screen.width}x${screen.height}`);
|
|
375
562
|
} catch (e) {
|
|
376
|
-
logger3.warn(`
|
|
563
|
+
logger3.warn(`syncViewport \u5931\u8D25: ${e.message}\uFF0C\u56DE\u9000\u5230 1920x1080`);
|
|
377
564
|
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
378
565
|
}
|
|
379
566
|
},
|
|
380
567
|
/**
|
|
381
|
-
*
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
await page.addInitScript(() => {
|
|
385
|
-
Object.defineProperty(navigator, "webdriver", {
|
|
386
|
-
get: () => false
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
logger3.success("hideWebdriver");
|
|
390
|
-
},
|
|
391
|
-
/**
|
|
392
|
-
* 获取推荐的 Stealth 启动参数
|
|
393
|
-
*/
|
|
394
|
-
getStealthLaunchArgs() {
|
|
395
|
-
return [
|
|
396
|
-
"--disable-blink-features=AutomationControlled",
|
|
397
|
-
"--no-sandbox",
|
|
398
|
-
"--disable-setuid-sandbox",
|
|
399
|
-
"--disable-infobars",
|
|
400
|
-
"--window-position=0,0",
|
|
401
|
-
"--ignore-certificate-errors",
|
|
402
|
-
"--disable-web-security"
|
|
403
|
-
// 注意:不建议这里强制指定 window-size,让 syncViewportWithScreen 去动态调整
|
|
404
|
-
// '--window-size=1920,1080'
|
|
405
|
-
];
|
|
406
|
-
},
|
|
407
|
-
/**
|
|
408
|
-
* 获取增强版 Stealth 启动参数 (推荐用于高风险反爬场景)
|
|
409
|
-
* 包含更多针对自动化检测的防护
|
|
410
|
-
*/
|
|
411
|
-
getAdvancedStealthArgs() {
|
|
412
|
-
return [
|
|
413
|
-
...this.getStealthLaunchArgs(),
|
|
414
|
-
// 禁用各种可能暴露自动化的特征
|
|
415
|
-
"--disable-dev-shm-usage",
|
|
416
|
-
"--disable-accelerated-2d-canvas",
|
|
417
|
-
"--disable-gpu-sandbox",
|
|
418
|
-
"--disable-background-networking",
|
|
419
|
-
"--disable-default-apps",
|
|
420
|
-
"--disable-extensions",
|
|
421
|
-
"--disable-sync",
|
|
422
|
-
"--disable-translate",
|
|
423
|
-
"--metrics-recording-only",
|
|
424
|
-
"--mute-audio",
|
|
425
|
-
"--no-first-run",
|
|
426
|
-
// 模拟真实用户配置
|
|
427
|
-
"--lang=zh-CN,zh"
|
|
428
|
-
];
|
|
429
|
-
},
|
|
430
|
-
/**
|
|
431
|
-
* 设置中国时区 (Asia/Shanghai, UTC+8)
|
|
432
|
-
*
|
|
433
|
-
* 防止时区不一致的检测。对于中国境内爬取强烈推荐使用。
|
|
434
|
-
* 应在 preNavigationHooks 或 BrowserContext 创建后调用。
|
|
435
|
-
*
|
|
436
|
-
* @param {import('playwright').BrowserContext} context
|
|
568
|
+
* 为 got-scraping 生成与浏览器一致的 TLS 指纹配置(桌面端)。
|
|
569
|
+
*
|
|
570
|
+
* @param {string} [userAgent]
|
|
437
571
|
*/
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return new originalDateTimeFormat(locales, options);
|
|
445
|
-
};
|
|
446
|
-
Intl.DateTimeFormat.prototype = originalDateTimeFormat.prototype;
|
|
447
|
-
Date.prototype.getTimezoneOffset = function() {
|
|
448
|
-
return -480;
|
|
449
|
-
};
|
|
450
|
-
});
|
|
451
|
-
logger3.success("setChinaTimezone", "Asia/Shanghai (UTC+8)");
|
|
572
|
+
getTlsFingerprintOptions(userAgent = "", acceptLanguage = "") {
|
|
573
|
+
const primaryLocale = parseAcceptLanguage(acceptLanguage || BASE_CONFIG.acceptLanguage)[0] || BASE_CONFIG.locale;
|
|
574
|
+
const fingerprint = buildFingerprintOptions(primaryLocale);
|
|
575
|
+
const os = getOperatingSystemsFromUserAgent(userAgent);
|
|
576
|
+
if (os.length > 0) fingerprint.operatingSystems = os;
|
|
577
|
+
return fingerprint;
|
|
452
578
|
},
|
|
453
579
|
/**
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
* @
|
|
580
|
+
* 规范化请求头,确保语言与浏览器一致。
|
|
581
|
+
*
|
|
582
|
+
* @param {Record<string, string>} headers
|
|
583
|
+
* @returns {Record<string, string>}
|
|
458
584
|
*/
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (
|
|
463
|
-
|
|
464
|
-
else if (lowerUA.includes("linux")) os.push("linux");
|
|
465
|
-
else if (lowerUA.includes("android")) os.push("android");
|
|
466
|
-
else if (lowerUA.includes("iphone") || lowerUA.includes("ipad")) os.push("ios");
|
|
467
|
-
if (os.length === 0) os.push("windows", "macos");
|
|
468
|
-
const devices = [];
|
|
469
|
-
if (lowerUA.includes("mobile") || lowerUA.includes("android") || lowerUA.includes("iphone")) {
|
|
470
|
-
devices.push("mobile");
|
|
471
|
-
} else {
|
|
472
|
-
devices.push("desktop");
|
|
585
|
+
applyLocaleHeaders(headers, acceptLanguage = "") {
|
|
586
|
+
if (acceptLanguage) {
|
|
587
|
+
headers["accept-language"] = acceptLanguage;
|
|
588
|
+
} else if (!headers["accept-language"]) {
|
|
589
|
+
headers["accept-language"] = BASE_CONFIG.acceptLanguage;
|
|
473
590
|
}
|
|
474
|
-
return
|
|
475
|
-
browsers: [{ name: "chrome", minVersion: 110 }],
|
|
476
|
-
// 保持较新的 Chrome 版本
|
|
477
|
-
devices,
|
|
478
|
-
locales: ["zh-CN", "en-US"],
|
|
479
|
-
// 保持与 setChinaTimezone 一致的中文优先
|
|
480
|
-
operatingSystems: os
|
|
481
|
-
};
|
|
591
|
+
return headers;
|
|
482
592
|
}
|
|
483
593
|
};
|
|
484
594
|
|
|
@@ -693,7 +803,7 @@ var Humanize = {
|
|
|
693
803
|
const restore = async () => {
|
|
694
804
|
if (!scrollStateHandle) return;
|
|
695
805
|
try {
|
|
696
|
-
const
|
|
806
|
+
const restoreOnce2 = async () => page.evaluate((state) => {
|
|
697
807
|
if (!Array.isArray(state) || state.length === 0) return true;
|
|
698
808
|
let done = true;
|
|
699
809
|
for (const item of state) {
|
|
@@ -710,7 +820,7 @@ var Humanize = {
|
|
|
710
820
|
return done;
|
|
711
821
|
}, scrollStateHandle);
|
|
712
822
|
for (let i = 0; i < 10; i++) {
|
|
713
|
-
const done = await
|
|
823
|
+
const done = await restoreOnce2();
|
|
714
824
|
if (done) break;
|
|
715
825
|
await (0, import_delay2.default)(this.jitterMs(80, 0.4));
|
|
716
826
|
}
|
|
@@ -755,25 +865,24 @@ var Humanize = {
|
|
|
755
865
|
} else {
|
|
756
866
|
element = target;
|
|
757
867
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if (restored) return;
|
|
763
|
-
restored = true;
|
|
868
|
+
const restoreOnce2 = async () => {
|
|
869
|
+
if (restoreOnce2.restored) return;
|
|
870
|
+
restoreOnce2.restored = true;
|
|
871
|
+
if (typeof restoreOnce2.do !== "function") return;
|
|
764
872
|
try {
|
|
765
|
-
await
|
|
873
|
+
await (0, import_delay2.default)(this.jitterMs(1e3));
|
|
874
|
+
await restoreOnce2.do();
|
|
766
875
|
} catch (restoreError) {
|
|
767
876
|
logger4.warn(`humanClick: \u6062\u590D\u6EDA\u52A8\u4F4D\u7F6E\u5931\u8D25: ${restoreError.message}`);
|
|
768
877
|
}
|
|
769
878
|
};
|
|
770
879
|
if (scrollIfNeeded) {
|
|
771
|
-
const
|
|
772
|
-
|
|
880
|
+
const { restore, didScroll } = await this.humanScroll(page, element);
|
|
881
|
+
restoreOnce2.do = didScroll ? restore : null;
|
|
773
882
|
}
|
|
774
883
|
const box = await element.boundingBox();
|
|
775
884
|
if (!box) {
|
|
776
|
-
await
|
|
885
|
+
await restoreOnce2();
|
|
777
886
|
if (throwOnMissing) {
|
|
778
887
|
throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
779
888
|
}
|
|
@@ -785,12 +894,11 @@ var Humanize = {
|
|
|
785
894
|
await cursor.actions.move({ x, y });
|
|
786
895
|
await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
|
|
787
896
|
await cursor.actions.click();
|
|
788
|
-
await (
|
|
789
|
-
await restoreOnce();
|
|
897
|
+
await restoreOnce2();
|
|
790
898
|
logger4.success("humanClick");
|
|
791
899
|
return true;
|
|
792
900
|
} catch (error) {
|
|
793
|
-
|
|
901
|
+
await restoreOnce();
|
|
794
902
|
logger4.fail("humanClick", error);
|
|
795
903
|
throw error;
|
|
796
904
|
}
|
|
@@ -959,12 +1067,11 @@ var Humanize = {
|
|
|
959
1067
|
};
|
|
960
1068
|
|
|
961
1069
|
// src/launch.js
|
|
962
|
-
var logger5 = createLogger("Launch");
|
|
963
1070
|
var Launch = {
|
|
964
1071
|
getLaunchOptions(customArgs = []) {
|
|
965
1072
|
return {
|
|
966
1073
|
args: [
|
|
967
|
-
...
|
|
1074
|
+
...AntiDetect.getLaunchArgs(),
|
|
968
1075
|
...customArgs
|
|
969
1076
|
],
|
|
970
1077
|
ignoreDefaultArgs: ["--enable-automation"]
|
|
@@ -976,7 +1083,7 @@ var Launch = {
|
|
|
976
1083
|
getAdvancedLaunchOptions(customArgs = []) {
|
|
977
1084
|
return {
|
|
978
1085
|
args: [
|
|
979
|
-
...
|
|
1086
|
+
...AntiDetect.getAdvancedLaunchArgs(),
|
|
980
1087
|
...customArgs
|
|
981
1088
|
],
|
|
982
1089
|
ignoreDefaultArgs: ["--enable-automation"]
|
|
@@ -987,42 +1094,14 @@ var Launch = {
|
|
|
987
1094
|
* 确保生成的是桌面端、较新的 Chrome,以匹配我们的脚本逻辑
|
|
988
1095
|
*/
|
|
989
1096
|
getFingerprintGeneratorOptions() {
|
|
990
|
-
return
|
|
991
|
-
browsers: [{ name: "chrome", minVersion: 110 }],
|
|
992
|
-
devices: ["desktop"],
|
|
993
|
-
operatingSystems: ["windows", "linux"]
|
|
994
|
-
// 包含 Linux 兼容容器
|
|
995
|
-
};
|
|
996
|
-
},
|
|
997
|
-
/**
|
|
998
|
-
* 创建已注册 Stealth 插件的 Chromium 实例
|
|
999
|
-
*
|
|
1000
|
-
* 封装了 `chromium.use(stealthPlugin())` 的常用模式
|
|
1001
|
-
*
|
|
1002
|
-
* @example
|
|
1003
|
-
* ```javascript
|
|
1004
|
-
* import { chromium } from 'playwright-extra';
|
|
1005
|
-
* import stealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|
1006
|
-
*
|
|
1007
|
-
* const stealthChromium = Launch.createStealthChromium(chromium, stealthPlugin);
|
|
1008
|
-
* // 现在 stealthChromium 已注册 stealth 插件,可用于 launchContext.launcher
|
|
1009
|
-
* ```
|
|
1010
|
-
*
|
|
1011
|
-
* @param {import('playwright-extra').ChromiumExtra} chromium - playwright-extra 的 chromium
|
|
1012
|
-
* @param {Function} stealthPlugin - puppeteer-extra-plugin-stealth 的默认导出
|
|
1013
|
-
* @returns {import('playwright-extra').ChromiumExtra} 已注册 stealth 的 chromium
|
|
1014
|
-
*/
|
|
1015
|
-
createStealthChromium(chromium, stealthPlugin) {
|
|
1016
|
-
chromium.use(stealthPlugin());
|
|
1017
|
-
logger5.success("createStealthChromium", "Stealth plugin registered");
|
|
1018
|
-
return chromium;
|
|
1097
|
+
return AntiDetect.getFingerprintGeneratorOptions();
|
|
1019
1098
|
}
|
|
1020
1099
|
};
|
|
1021
1100
|
|
|
1022
1101
|
// src/live-view.js
|
|
1023
1102
|
var import_express = __toESM(require("express"), 1);
|
|
1024
1103
|
var import_apify = require("apify");
|
|
1025
|
-
var
|
|
1104
|
+
var logger5 = createLogger("LiveView");
|
|
1026
1105
|
async function startLiveViewServer(liveViewKey) {
|
|
1027
1106
|
const app = (0, import_express.default)();
|
|
1028
1107
|
app.get("/", async (req, res) => {
|
|
@@ -1047,13 +1126,13 @@ async function startLiveViewServer(liveViewKey) {
|
|
|
1047
1126
|
</html>
|
|
1048
1127
|
`);
|
|
1049
1128
|
} catch (error) {
|
|
1050
|
-
|
|
1129
|
+
logger5.fail("Live View Server", error);
|
|
1051
1130
|
res.status(500).send(`\u65E0\u6CD5\u52A0\u8F7D\u5C4F\u5E55\u622A\u56FE: ${error.message}`);
|
|
1052
1131
|
}
|
|
1053
1132
|
});
|
|
1054
1133
|
const port = process.env.APIFY_CONTAINER_PORT || 4321;
|
|
1055
1134
|
app.listen(port, () => {
|
|
1056
|
-
|
|
1135
|
+
logger5.success("startLiveViewServer", `\u76D1\u542C\u7AEF\u53E3 ${port}`);
|
|
1057
1136
|
});
|
|
1058
1137
|
}
|
|
1059
1138
|
async function takeLiveScreenshot(liveViewKey, page, logMessage) {
|
|
@@ -1061,10 +1140,10 @@ async function takeLiveScreenshot(liveViewKey, page, logMessage) {
|
|
|
1061
1140
|
const buffer = await page.screenshot({ type: "png" });
|
|
1062
1141
|
await import_apify.Actor.setValue(liveViewKey, buffer, { contentType: "image/png" });
|
|
1063
1142
|
if (logMessage) {
|
|
1064
|
-
|
|
1143
|
+
logger5.info(`(\u622A\u56FE): ${logMessage}`);
|
|
1065
1144
|
}
|
|
1066
1145
|
} catch (e) {
|
|
1067
|
-
|
|
1146
|
+
logger5.warn(`\u65E0\u6CD5\u6355\u83B7 Live View \u5C4F\u5E55\u622A\u56FE: ${e.message}`);
|
|
1068
1147
|
}
|
|
1069
1148
|
}
|
|
1070
1149
|
var useLiveView = (liveViewKey = PresetOfLiveViewKey) => {
|
|
@@ -1083,7 +1162,7 @@ var LiveView = {
|
|
|
1083
1162
|
|
|
1084
1163
|
// src/captcha-monitor.js
|
|
1085
1164
|
var import_uuid = require("uuid");
|
|
1086
|
-
var
|
|
1165
|
+
var logger6 = createLogger("Captcha");
|
|
1087
1166
|
function useCaptchaMonitor(page, options) {
|
|
1088
1167
|
const { domSelector, urlPattern, onDetected } = options;
|
|
1089
1168
|
if (!domSelector && !urlPattern) {
|
|
@@ -1155,7 +1234,7 @@ function useCaptchaMonitor(page, options) {
|
|
|
1155
1234
|
};
|
|
1156
1235
|
})();
|
|
1157
1236
|
}, { selector: domSelector, callbackName: exposedFunctionName, cleanerName });
|
|
1158
|
-
|
|
1237
|
+
logger6.success("useCaptchaMonitor", `DOM \u76D1\u63A7\u5DF2\u542F\u7528: ${domSelector}`);
|
|
1159
1238
|
cleanupFns.push(async () => {
|
|
1160
1239
|
try {
|
|
1161
1240
|
await page.evaluate((name) => {
|
|
@@ -1178,14 +1257,14 @@ function useCaptchaMonitor(page, options) {
|
|
|
1178
1257
|
}
|
|
1179
1258
|
};
|
|
1180
1259
|
page.on("framenavigated", frameHandler);
|
|
1181
|
-
|
|
1260
|
+
logger6.success("useCaptchaMonitor", `URL \u76D1\u63A7\u5DF2\u542F\u7528: ${urlPattern}`);
|
|
1182
1261
|
cleanupFns.push(async () => {
|
|
1183
1262
|
page.off("framenavigated", frameHandler);
|
|
1184
1263
|
});
|
|
1185
1264
|
}
|
|
1186
1265
|
return {
|
|
1187
1266
|
stop: async () => {
|
|
1188
|
-
|
|
1267
|
+
logger6.info("useCaptchaMonitor", "\u6B63\u5728\u505C\u6B62\u76D1\u63A7...");
|
|
1189
1268
|
for (const fn of cleanupFns) {
|
|
1190
1269
|
await fn();
|
|
1191
1270
|
}
|
|
@@ -1200,7 +1279,7 @@ var Captcha = {
|
|
|
1200
1279
|
// src/sse.js
|
|
1201
1280
|
var import_https = __toESM(require("https"), 1);
|
|
1202
1281
|
var import_url = require("url");
|
|
1203
|
-
var
|
|
1282
|
+
var logger7 = createLogger("Sse");
|
|
1204
1283
|
var Sse = {
|
|
1205
1284
|
/**
|
|
1206
1285
|
* 解析 SSE 流文本
|
|
@@ -1219,11 +1298,11 @@ var Sse = {
|
|
|
1219
1298
|
events.push(JSON.parse(jsonContent));
|
|
1220
1299
|
}
|
|
1221
1300
|
} catch (e) {
|
|
1222
|
-
|
|
1301
|
+
logger7.debug("parseSseStream", `JSON \u89E3\u6790\u5931\u8D25: ${e.message}, line: ${line.substring(0, 100)}...`);
|
|
1223
1302
|
}
|
|
1224
1303
|
}
|
|
1225
1304
|
}
|
|
1226
|
-
|
|
1305
|
+
logger7.success("parseSseStream", `\u89E3\u6790\u5B8C\u6210, events \u6570\u91CF: ${events.length}`);
|
|
1227
1306
|
return events;
|
|
1228
1307
|
},
|
|
1229
1308
|
/**
|
|
@@ -1258,7 +1337,7 @@ var Sse = {
|
|
|
1258
1337
|
const workPromise = new Promise((resolve, reject) => {
|
|
1259
1338
|
page.route(urlPattern, async (route) => {
|
|
1260
1339
|
const request = route.request();
|
|
1261
|
-
|
|
1340
|
+
logger7.info(`[MITM] \u5DF2\u62E6\u622A\u8BF7\u6C42: ${request.url()}`);
|
|
1262
1341
|
try {
|
|
1263
1342
|
const headers = await request.allHeaders();
|
|
1264
1343
|
const postData = request.postData();
|
|
@@ -1283,7 +1362,7 @@ var Sse = {
|
|
|
1283
1362
|
clearTimeout(initialTimer);
|
|
1284
1363
|
initialTimer = null;
|
|
1285
1364
|
}
|
|
1286
|
-
|
|
1365
|
+
logger7.debug("[Intercept] \u5DF2\u63A5\u6536\u521D\u59CB\u6570\u636E");
|
|
1287
1366
|
}
|
|
1288
1367
|
chunks.push(chunk);
|
|
1289
1368
|
const textChunk = chunk.toString("utf-8");
|
|
@@ -1292,18 +1371,18 @@ var Sse = {
|
|
|
1292
1371
|
try {
|
|
1293
1372
|
onData(textChunk, resolve, accumulatedText);
|
|
1294
1373
|
} catch (e) {
|
|
1295
|
-
|
|
1374
|
+
logger7.fail(`onData \u9519\u8BEF`, e);
|
|
1296
1375
|
}
|
|
1297
1376
|
}
|
|
1298
1377
|
});
|
|
1299
1378
|
res.on("end", () => {
|
|
1300
|
-
|
|
1379
|
+
logger7.info("[MITM] \u4E0A\u6E38\u54CD\u5E94\u7ED3\u675F");
|
|
1301
1380
|
clearAllTimers();
|
|
1302
1381
|
if (onEnd) {
|
|
1303
1382
|
try {
|
|
1304
1383
|
onEnd(accumulatedText, resolve);
|
|
1305
1384
|
} catch (e) {
|
|
1306
|
-
|
|
1385
|
+
logger7.fail(`onEnd \u9519\u8BEF`, e);
|
|
1307
1386
|
}
|
|
1308
1387
|
} else if (!onData) {
|
|
1309
1388
|
resolve(accumulatedText);
|
|
@@ -1385,7 +1464,7 @@ var Sse = {
|
|
|
1385
1464
|
var import_got_scraping = require("got-scraping");
|
|
1386
1465
|
var import_http = require("http");
|
|
1387
1466
|
var import_https2 = require("https");
|
|
1388
|
-
var
|
|
1467
|
+
var logger8 = createLogger("Interception");
|
|
1389
1468
|
var SHARED_HTTP_AGENT = new import_http.Agent({ keepAlive: false });
|
|
1390
1469
|
var SHARED_HTTPS_AGENT = new import_https2.Agent({ keepAlive: false, rejectUnauthorized: false });
|
|
1391
1470
|
var DirectConfig = {
|
|
@@ -1516,7 +1595,7 @@ var Interception = {
|
|
|
1516
1595
|
if (mergedBlockingConfig.blockFont) enabledCategories.push("\u5B57\u4F53");
|
|
1517
1596
|
if (mergedBlockingConfig.blockCss) enabledCategories.push("CSS");
|
|
1518
1597
|
if (mergedBlockingConfig.blockOther) enabledCategories.push("\u5176\u4ED6");
|
|
1519
|
-
|
|
1598
|
+
logger8.start("setup", hasDirectDomains ? `\u76F4\u8FDE\u57DF\u540D: [${directDomains.length} \u4E2A] | \u5C4F\u853D: [${enabledCategories.join(", ")}]` : `\u4EC5\u8D44\u6E90\u5C4F\u853D\u6A21\u5F0F | \u5C4F\u853D: [${enabledCategories.join(", ")}]`);
|
|
1520
1599
|
await page.route("**/*", async (route) => {
|
|
1521
1600
|
let handled = false;
|
|
1522
1601
|
try {
|
|
@@ -1543,6 +1622,9 @@ var Interception = {
|
|
|
1543
1622
|
try {
|
|
1544
1623
|
const reqHeaders = await request.allHeaders();
|
|
1545
1624
|
delete reqHeaders["host"];
|
|
1625
|
+
const currentAcceptLanguage = reqHeaders["accept-language"] || "";
|
|
1626
|
+
AntiDetect.applyLocaleHeaders(reqHeaders, currentAcceptLanguage);
|
|
1627
|
+
const resolvedAcceptLanguage = reqHeaders["accept-language"] || "";
|
|
1546
1628
|
const userAgent = reqHeaders["user-agent"] || "";
|
|
1547
1629
|
const method = request.method();
|
|
1548
1630
|
const postData = method !== "GET" && method !== "HEAD" ? request.postDataBuffer() : void 0;
|
|
@@ -1556,7 +1638,7 @@ var Interception = {
|
|
|
1556
1638
|
responseType: "buffer",
|
|
1557
1639
|
// 强制获取 Buffer
|
|
1558
1640
|
// 模拟浏览器 TLS 指纹
|
|
1559
|
-
headerGeneratorOptions:
|
|
1641
|
+
headerGeneratorOptions: AntiDetect.getTlsFingerprintOptions(userAgent, resolvedAcceptLanguage),
|
|
1560
1642
|
// 使用共享的 Agent 单例(keepAlive: false,不会池化连接)
|
|
1561
1643
|
agent: {
|
|
1562
1644
|
http: SHARED_HTTP_AGENT,
|
|
@@ -1578,7 +1660,7 @@ var Interception = {
|
|
|
1578
1660
|
delete resHeaders["transfer-encoding"];
|
|
1579
1661
|
delete resHeaders["connection"];
|
|
1580
1662
|
delete resHeaders["keep-alive"];
|
|
1581
|
-
isSilent ?
|
|
1663
|
+
isSilent ? logger8.debug(`\u76F4\u8FDE\u6210\u529F: ${urlPath}`) : logger8.info(`\u76F4\u8FDE\u6210\u529F: ${urlPath}`);
|
|
1582
1664
|
await safeFulfill(route, {
|
|
1583
1665
|
status: response.statusCode,
|
|
1584
1666
|
headers: resHeaders,
|
|
@@ -1590,7 +1672,7 @@ var Interception = {
|
|
|
1590
1672
|
const isTimeout = e.code === "ETIMEDOUT" || e.message.toLowerCase().includes("timeout");
|
|
1591
1673
|
const action = fallbackToProxy ? "\u56DE\u9000\u4EE3\u7406" : "\u5DF2\u653E\u5F03";
|
|
1592
1674
|
const reason = isTimeout ? `\u8D85\u65F6(${DirectConfig.directTimeout}s)` : `\u5F02\u5E38: ${e.message}`;
|
|
1593
|
-
|
|
1675
|
+
logger8.warn(`\u76F4\u8FDE${reason}\uFF0C${action}: ${urlPath}`);
|
|
1594
1676
|
if (fallbackToProxy) {
|
|
1595
1677
|
await safeContinue(route);
|
|
1596
1678
|
} else {
|
|
@@ -1603,7 +1685,7 @@ var Interception = {
|
|
|
1603
1685
|
await safeContinue(route);
|
|
1604
1686
|
handled = true;
|
|
1605
1687
|
} catch (err) {
|
|
1606
|
-
|
|
1688
|
+
logger8.warn(`\u8DEF\u7531\u5904\u7406\u5F02\u5E38: ${err.message}`);
|
|
1607
1689
|
if (!handled) {
|
|
1608
1690
|
try {
|
|
1609
1691
|
await route.continue();
|
|
@@ -1640,7 +1722,7 @@ function isIgnorableError(error) {
|
|
|
1640
1722
|
var usePlaywrightToolKit = () => {
|
|
1641
1723
|
return {
|
|
1642
1724
|
ApifyKit,
|
|
1643
|
-
|
|
1725
|
+
AntiDetect,
|
|
1644
1726
|
Humanize,
|
|
1645
1727
|
Launch,
|
|
1646
1728
|
LiveView,
|