/** * Cloudflare 挑战统一处理:检测 → 内置等待 → Captcha API 兜底 */ const { detectCloudflare } = require('./cf-detector'); const { waitForTurnstile, extractTurnstileSitekey, injectTurnstileToken } = require('./turnstile-handler'); const { solveTurnstile, isCaptchaConfigured } = require('./captcha-solver'); /** * @typedef {Object} CfHandleResult * @property {boolean} success * @property {string|null} code * @property {string|null} challengeType * @property {string|null} stage * @property {boolean} solverUsed * @property {number} elapsedMs * @property {boolean} cfDetected */ /** * 处理 Cloudflare / Turnstile 挑战(在 authActions 之前调用) * @param {import('puppeteer').Page} page * @param {{ turnstile?: boolean, solverFallback?: boolean, challengeTimeoutMs?: number }} antiBot * @param {(page: import('puppeteer').Page) => Promise} [waitForUrlSettled] * @returns {Promise} */ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled) { const started = Date.now(); const timeoutMs = antiBot.challengeTimeoutMs || 60000; let cfState = await detectCloudflare(page); if (!cfState.blocked) { return { success: true, code: null, challengeType: null, stage: null, solverUsed: false, elapsedMs: Date.now() - started, cfDetected: false, }; } 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, };