Files
links/patches/puppeteer-api/lib/cf-api-fallback.js
T

174 lines
6.0 KiB
JavaScript
Raw Normal View History

2026-07-03 20:12:20 +08:00
/**
* CapSolver / 2Captcha 过盾兜底:Turnstile token 注入 或 AntiCloudflareTask cf_clearance
*
* A2C 等业务域为 CF Managed Challenge(非独立 Turnstile 挂件),须优先 AntiCloudflareTask。
*/
const { sanitizePageUrlForCfSolver } = require('./cf-detector');
const {
injectTurnstileToken,
recoverAfterTokenInject,
extractTurnstileSitekey,
} = require('./turnstile-handler');
const {
solveTurnstile,
solveCloudflare,
applyCfClearanceSolution,
} = require('./captcha-solver');
const { getSitekeyCapture } = require('./sitekey-capture');
/** @returns {boolean} */
function hasCaptchaProxy() {
return (process.env.CAPTCHA_PROXY || '').trim() !== '';
}
/**
* 是否应使用 CapSolver AntiCloudflareTaskManaged Challenge / 5s 盾)
* @param {object|null|undefined} antiBot
*/
function shouldPreferManagedCloudflareSolver(antiBot) {
if (!antiBot || antiBot.cfCloudflareSolver === false) {
return false;
}
if (antiBot.cfChallengeMode === 'managed') {
return hasCaptchaProxy();
}
if (antiBot.cfCloudflareSolver === true && hasCaptchaProxy()) {
return true;
}
return false;
}
/**
* 解析可用于 AntiTurnstileTask 的 sitekey(排除 A2C 配置的 challenge sitekey
* @param {import('puppeteer').Page} page
* @param {object} antiBot
* @param {number} waitMs
* @returns {Promise<string|null>}
*/
async function resolveTurnstileWidgetSitekey(page, antiBot, waitMs) {
if (antiBot.cfChallengeMode === 'managed') {
return null;
}
const capture = getSitekeyCapture(page);
if (capture) {
const fromCapture = await capture.resolveForSolver(page, antiBot, waitMs);
if (fromCapture && antiBot.cfChallengeMode !== 'managed') {
const configured = String(antiBot.turnstileSitekey || '').trim();
if (configured && fromCapture === configured && antiBot.cfCloudflareSolver) {
return null;
}
return fromCapture;
}
}
const dynamic = await extractTurnstileSitekey(page);
if (dynamic) {
return dynamic;
}
const configured = String(antiBot?.turnstileSitekey || '').trim();
if (configured && !antiBot.cfCloudflareSolver) {
return configured;
}
return null;
}
/**
* CapSolver AntiCloudflareTask 求解并注入 cf_clearance
* @param {import('puppeteer').Page} page
* @param {object} params
*/
async function runManagedCloudflareSolver(page, params) {
const { cleanSolverUrl, waitForUrlSettled, timeoutMs } = params;
if (!hasCaptchaProxy()) {
throw new Error('AntiCloudflareTask 需要配置 CAPTCHA_PROXY(静态代理 ip:port:user:pass');
}
console.log('[CF] A2C/Managed Challenge:使用 CapSolver AntiCloudflareTask');
const html = await page.content().catch(() => '');
const userAgent = await page.evaluate(() => navigator.userAgent).catch(() => '');
const { cookies, userAgent: solvedUa, provider } = await solveCloudflare({
pageUrl: cleanSolverUrl,
html,
userAgent,
});
console.log(`[CF] CapSolver AntiCloudflareTask (${provider}) 返回 cf_clearance`);
await applyCfClearanceSolution(page, cleanSolverUrl, { cookies, userAgent: solvedUa || userAgent });
if (typeof waitForUrlSettled === 'function') {
await waitForUrlSettled(page).catch(() => {});
}
return { mode: 'cloudflare', provider };
}
/**
* @param {import('puppeteer').Page} page
* @param {object} options
* @param {object} options.antiBot
* @param {string} [options.navUrl]
* @param {(page: import('puppeteer').Page) => Promise<string>} [options.waitForUrlSettled]
* @param {number} [options.timeoutMs]
* @returns {Promise<{ mode: 'turnstile'|'cloudflare', provider: string }>}
*/
async function runCaptchaApiFallback(page, options) {
const {
antiBot,
navUrl = '',
waitForUrlSettled,
timeoutMs = 60000,
} = options;
const cleanSolverUrl = sanitizePageUrlForCfSolver(navUrl || page.url());
if (shouldPreferManagedCloudflareSolver(antiBot)) {
console.log(`[CF] Captcha API 求解 pageUrl=${cleanSolverUrl} mode=managed_challenge`);
return runManagedCloudflareSolver(page, { cleanSolverUrl, waitForUrlSettled, timeoutMs });
}
const sitekey = await resolveTurnstileWidgetSitekey(
page,
antiBot,
Math.min(timeoutMs, 25000)
);
console.log(`[CF] Captcha API 求解 pageUrl=${cleanSolverUrl} sitekey=${sitekey ? 'ok' : 'missing'}`);
if (sitekey) {
try {
const { token, provider } = await solveTurnstile({ pageUrl: cleanSolverUrl, sitekey });
console.log(`[CF] Captcha API Turnstile (${provider}) 返回 token,注入页面`);
await injectTurnstileToken(page, token);
await recoverAfterTokenInject(
page,
cleanSolverUrl,
waitForUrlSettled,
Math.min(timeoutMs, 45000)
);
return { mode: 'turnstile', provider };
} catch (turnstileErr) {
const msg = (turnstileErr?.message || '').toLowerCase();
const isChallengeMismatch = msg.includes('challenge, not turnstile')
|| msg.includes('not turnstile');
if (isChallengeMismatch && antiBot.cfCloudflareSolver !== false && hasCaptchaProxy()) {
console.warn(`[CF] Turnstile 任务失败(${turnstileErr.message}),降级 AntiCloudflareTask`);
return runManagedCloudflareSolver(page, { cleanSolverUrl, waitForUrlSettled, timeoutMs });
}
throw turnstileErr;
}
}
if (antiBot?.cfCloudflareSolver !== false && hasCaptchaProxy()) {
return runManagedCloudflareSolver(page, { cleanSolverUrl, waitForUrlSettled, timeoutMs });
}
throw new Error('无法过盾:请配置 CAPTCHA_PROXYA2C Managed Challenge)或提供有效 Turnstile sitekey');
}
module.exports = {
runCaptchaApiFallback,
shouldPreferManagedCloudflareSolver,
hasCaptchaProxy,
};