/** * Cloudflare 挑战统一处理:检测 → 会话快速通道 → 内置等待 → Captcha API 兜底 */ const { detectCloudflare } = require('./cf-detector'); const { waitForTurnstile, extractTurnstileSitekey, injectTurnstileToken } = require('./turnstile-handler'); const { solveTurnstile, isCaptchaConfigured } = require('./captcha-solver'); const { isSessionLikelyValid } = require('./session-store'); /** * @param {import('puppeteer').Page} page * @param {{ turnstile?: boolean, solverFallback?: boolean, challengeTimeoutMs?: number }} antiBot * @param {(page: import('puppeteer').Page) => Promise} [waitForUrlSettled] * @param {import('./session-store').StoredSession|null} [storedSession] */ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, storedSession = null) { const started = Date.now(); const timeoutMs = antiBot.challengeTimeoutMs || 60000; let cfState = await detectCloudflare(page); if (!cfState.blocked && cfState.hasCfClearance) { return { success: true, code: null, challengeType: null, stage: 'session_reuse', solverUsed: false, elapsedMs: Date.now() - started, cfDetected: false, }; } if (!cfState.blocked) { return { success: true, code: null, challengeType: null, stage: storedSession && isSessionLikelyValid(storedSession) ? 'session_reuse' : null, solverUsed: false, elapsedMs: Date.now() - started, cfDetected: false, }; } if (storedSession && isSessionLikelyValid(storedSession)) { console.log('[CF] 会话有效但仍被拦截,尝试 reload 一次'); try { await page.reload({ waitUntil: 'domcontentloaded', timeout: Math.min(timeoutMs, 45000) }); if (waitForUrlSettled) await waitForUrlSettled(page).catch(() => {}); cfState = await detectCloudflare(page); if (!cfState.blocked) { return { success: true, code: null, challengeType: cfState.challengeType, stage: 'session_reload', solverUsed: false, elapsedMs: Date.now() - started, cfDetected: true, }; } } catch (reloadErr) { console.warn('[CF] reload 失败:', reloadErr.message); } } console.log(`[CF] 检测到挑战 type=${cfState.challengeType} url=${cfState.url}`); if (antiBot.turnstile !== false) { const builtIn = await waitForTurnstile(page, { timeoutMs, useBuiltInClick: true }); if (builtIn.success) { if (waitForUrlSettled) await waitForUrlSettled(page).catch(() => {}); cfState = await detectCloudflare(page); if (!cfState.blocked) { return { success: true, code: null, challengeType: cfState.challengeType, stage: 'built_in', solverUsed: false, elapsedMs: Date.now() - started, cfDetected: true, }; } } } if (antiBot.solverFallback !== false && isCaptchaConfigured()) { try { const sitekey = await extractTurnstileSitekey(page); const pageUrl = page.url(); const { token, provider } = await solveTurnstile({ pageUrl, sitekey }); console.log(`[CF] Captcha API (${provider}) 返回 token,注入页面`); await injectTurnstileToken(page, token); const apiWaitMs = Math.min(timeoutMs, 30000); await waitForTurnstile(page, { timeoutMs: apiWaitMs }); if (waitForUrlSettled) await waitForUrlSettled(page).catch(() => {}); cfState = await detectCloudflare(page); if (!cfState.blocked) { return { success: true, code: null, challengeType: 'turnstile', stage: 'api', solverUsed: true, elapsedMs: Date.now() - started, cfDetected: true, }; } } catch (apiErr) { console.error('[CF] Captcha API 兜底失败:', apiErr.message); return { success: false, code: 'CF_TURNSTILE_FAILED', challengeType: cfState.challengeType || 'turnstile', stage: 'api', solverUsed: true, elapsedMs: Date.now() - started, cfDetected: true, }; } } return { success: false, code: 'CF_TURNSTILE_FAILED', challengeType: cfState.challengeType || 'unknown', stage: antiBot.solverFallback !== false && !isCaptchaConfigured() ? 'built_in' : 'timeout', solverUsed: false, elapsedMs: Date.now() - started, cfDetected: true, }; } module.exports = { handleCloudflareChallenge, isCaptchaConfigured, };