/** * 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 AntiCloudflareTask(Managed 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} */ 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} [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_PROXY(A2C Managed Challenge)或提供有效 Turnstile sitekey'); } module.exports = { runCaptchaApiFallback, shouldPreferManagedCloudflareSolver, hasCaptchaProxy, };