/** * Cloudflare Turnstile 验证处理器 * 通用的Cloudflare人机验证处理,支持全自动和半自动模式 */ const logger = require('../../../shared/logger'); class CloudflareHandler { /** * @param {Page} page - Puppeteer page对象 * @param {HumanBehavior} human - 人类行为模拟对象 * @param {string} siteName - 网站名称(用于日志) * @param {string} mode - 验证模式: 'auto' 或 'manual' (默认 'manual') * @param {Function} customCheck - 自定义检测函数,返回Promise,检测验证是否完成 */ constructor(page, human, siteName, mode = 'manual', customCheck = null) { this.page = page; this.human = human; this.siteName = siteName; this.mode = mode; // 'auto' 或 'manual' this.customCheck = customCheck; // 自定义检测函数 } /** * 处理Cloudflare Turnstile验证 * @returns {Promise} 'passed' | 'failed' | 'manual' */ async handle() { logger.info(this.siteName, '[Cloudflare] 检查人机验证...'); await this.human.randomDelay(2000, 3000); // 检查页面内容,判断是否有验证 const pageText = await this.page.evaluate(() => document.body.textContent || ''); if (!pageText.includes('verify') && !pageText.includes('human')) { logger.success(this.siteName, '[Cloudflare] 无验证,自动通过'); return 'passed'; } logger.info(this.siteName, '[Cloudflare] 发现验证页面'); // 情况1:等待自动验证(某些情况下会自动通过) logger.info(this.siteName, '[Cloudflare] 等待自动验证(5秒)...'); await this.human.randomDelay(5000, 6000); const afterWait = await this.page.evaluate(() => document.body.textContent || ''); if (!afterWait.includes('verify') && !afterWait.includes('human')) { logger.success(this.siteName, '[Cloudflare] 自动验证通过'); return 'passed'; } // 情况2:需要点击验证框 if (this.mode === 'auto') { return await this.autoHandle(); } else { return await this.manualHandle(); } } /** * 自动处理模式(实验性) */ async autoHandle() { logger.info(this.siteName, '[Cloudflare] 自动模式:尝试点击验证框...'); try { // 查找Turnstile iframe const frames = await this.page.frames(); let turnstileFrame = null; for (const frame of frames) { const url = frame.url(); if (url.includes('challenges.cloudflare.com') || url.includes('turnstile')) { turnstileFrame = frame; logger.success(this.siteName, `[Cloudflare] 找到iframe: ${url.substring(0, 80)}...`); break; } } if (!turnstileFrame) { logger.warn(this.siteName, '[Cloudflare] 未找到Turnstile iframe'); return 'failed'; } // 等待iframe加载 await turnstileFrame.waitForSelector('body', { timeout: 5000 }); await this.human.randomDelay(8000, 8000); // 查找并点击checkbox const checkboxElement = await turnstileFrame.$('label.cb-lb'); if (!checkboxElement) { logger.error(this.siteName, '[Cloudflare] 未找到验证框'); return 'failed'; } // 使用真实鼠标点击 const box = await checkboxElement.boundingBox(); if (box) { const clickX = box.x + box.width / 2; const clickY = box.y + box.height / 2; await this.page.mouse.move(clickX - 50, clickY - 50); await this.human.randomDelay(100, 300); await this.page.mouse.move(clickX, clickY, { steps: 10 }); await this.human.randomDelay(200, 400); await this.page.mouse.click(clickX, clickY); logger.success(this.siteName, '[Cloudflare] 已点击验证框'); } // 等待验证处理 await this.human.randomDelay(8000, 12000); // 检查是否通过 const finalCheck = await this.page.evaluate(() => document.body.textContent || ''); if (!finalCheck.includes('verify') && !finalCheck.includes('human')) { logger.success(this.siteName, '[Cloudflare] ✓ 验证通过'); return 'passed'; } return 'failed'; } catch (error) { logger.error(this.siteName, `[Cloudflare] 自动处理失败: ${error.message}`); return 'failed'; } } /** * 半自动处理模式(推荐) */ async manualHandle() { logger.info(this.siteName, '[Cloudflare] 半自动模式:等待用户手动点击...'); try { // 查找Turnstile iframe const frames = await this.page.frames(); let turnstileFrame = null; for (const frame of frames) { const url = frame.url(); if (url.includes('challenges.cloudflare.com') || url.includes('turnstile')) { turnstileFrame = frame; break; } } if (turnstileFrame) { await turnstileFrame.waitForSelector('body', { timeout: 5000 }); } // 显示提示 logger.warn(this.siteName, '='.repeat(60)); logger.warn(this.siteName, '[Cloudflare] 🖱️ 请手动点击验证框!'); logger.warn(this.siteName, '[Cloudflare] 程序将自动检测验证完成(超时10分钟)...'); logger.warn(this.siteName, '='.repeat(60)); // 轮询检查验证是否完成(最多10分钟) let verified = false; let attempts = 0; const maxAttempts = 600; // 10分钟 = 600秒 while (!verified && attempts < maxAttempts) { attempts++; await this.human.randomDelay(1000, 1000); // 方法1: 检查页面内容 const pageText = await this.page.evaluate(() => document.body.textContent || ''); const hasVerifyText = pageText.includes('verify') || pageText.includes('human'); // 方法2: 检查URL是否改变 const currentUrl = this.page.url(); const urlChanged = !currentUrl.includes('register'); // 方法3: 检查iframe内成功标志 let iframeSuccess = false; if (turnstileFrame) { try { iframeSuccess = await turnstileFrame.evaluate(() => { const successDiv = document.querySelector('#success'); return successDiv && successDiv.style.display !== 'none'; }); } catch (e) { // iframe可能已消失 } } // 方法4: 自定义检测(由网站自己定义) let customCheckPassed = false; if (this.customCheck) { try { customCheckPassed = await this.customCheck(); } catch (e) { // 忽略错误 } } // 每15秒输出一次状态 if (attempts % 15 === 0) { const minutes = Math.floor(attempts / 60); const seconds = attempts % 60; logger.info(this.siteName, `[Cloudflare] 等待中... (${minutes}分${seconds}秒/${Math.floor(maxAttempts/60)}分钟) - custom=${customCheckPassed}, iframe=${iframeSuccess}`); } // 判断验证完成 if (customCheckPassed || iframeSuccess || (!hasVerifyText && attempts > 3) || urlChanged) { verified = true; logger.success(this.siteName, '[Cloudflare] ✓✓✓ 检测到验证完成!'); break; } } if (!verified) { logger.error(this.siteName, '[Cloudflare] ✗ 10分钟超时'); return 'failed'; } // 验证完成后等待页面稳定 await this.human.randomDelay(2000, 3000); return 'passed'; } catch (error) { logger.error(this.siteName, `[Cloudflare] 半自动处理失败: ${error.message}`); return 'failed'; } } } module.exports = CloudflareHandler;