/** * Turnstile 内置求解:等待 widget 自动完成 + token 轮询 + 多 frame sitekey 提取 */ const { detectCloudflare, isTurnstileSolved, urlIndicatesCfChallenge } = require('./cf-detector'); /** * @typedef {Object} TurnstileResult * @property {boolean} success * @property {string} stage built_in|timeout|already_clear * @property {number} elapsedMs */ /** * 在单个 frame 内提取 sitekey * @param {import('puppeteer').Frame} frame * @returns {Promise} */ async function extractSitekeyFromFrame(frame) { return frame.evaluate(() => { const widget = document.querySelector('.cf-turnstile[data-sitekey], [data-sitekey]'); if (widget) { const key = widget.getAttribute('data-sitekey'); if (key) { return key; } } const iframes = document.querySelectorAll('iframe[src*="challenges.cloudflare.com"], iframe[src*="challenge-platform"]'); for (const iframe of iframes) { const src = iframe.getAttribute('src') || ''; const match = src.match(/[?&]sitekey=([^&]+)/i); if (match) { return decodeURIComponent(match[1]); } } const scripts = document.querySelectorAll('script'); for (const script of scripts) { const text = script.textContent || ''; const match = text.match(/sitekey['"\s:=]+['"]([0-9x_a-zA-Z-]{10,})['"]/i) || text.match(/data-sitekey=['"]([0-9x_a-zA-Z-]{10,})['"]/i); if (match) { return match[1]; } } return null; }).catch(() => null); } /** * 从主文档及所有子 frame 提取 Turnstile sitekey * @param {import('puppeteer').Page} page * @returns {Promise} */ async function extractTurnstileSitekey(page) { const mainKey = await extractSitekeyFromFrame(page.mainFrame()); if (mainKey) { return mainKey; } for (const frame of page.frames()) { if (frame === page.mainFrame()) { continue; } const frameKey = await extractSitekeyFromFrame(frame); if (frameKey) { return frameKey; } } return null; } /** * Captcha API 调用前:等待 Turnstile 挑战面出现或 URL 进入 CF 挑战态 * * @param {import('puppeteer').Page} page * @param {number} [timeoutMs] * @returns {Promise} 若已提取到 sitekey 则返回,否则 null */ async function waitForTurnstileSurface(page, timeoutMs = 20000) { const started = Date.now(); const pollMs = 400; while (Date.now() - started < timeoutMs) { const sitekey = await extractTurnstileSitekey(page); if (sitekey) { console.log(`[CF] 已检测到 Turnstile sitekey (${sitekey.slice(0, 8)}...)`); return sitekey; } const url = page.url(); if (urlIndicatesCfChallenge(url)) { await new Promise((r) => setTimeout(r, pollMs)); continue; } const state = await detectCloudflare(page); if (!state.blocked && state.hasCfClearance) { return null; } const hasSurface = await page.evaluate(() => { return !!document.querySelector( '.cf-turnstile, [data-sitekey], [name="cf-turnstile-response"], ' + 'iframe[src*="challenges.cloudflare.com"], iframe[src*="challenge-platform"]' ); }).catch(() => false); if (hasSurface) { await new Promise((r) => setTimeout(r, pollMs)); continue; } await new Promise((r) => setTimeout(r, pollMs)); } return null; } /** * 等待 Turnstile 通过(内置点击 / 自动跳转) * @param {import('puppeteer').Page} page * @param {{ timeoutMs?: number, pollMs?: number, useBuiltInClick?: boolean }} [options] * @returns {Promise} */ async function waitForTurnstile(page, options = {}) { const timeoutMs = options.timeoutMs ?? 60000; const pollMs = options.pollMs ?? 500; const started = Date.now(); const initial = await detectCloudflare(page); const initialUrlChallenge = urlIndicatesCfChallenge(page.url()); if ((!initial.blocked && !initialUrlChallenge) || initial.hasCfClearance) { if (initial.hasCfClearance && !initialUrlChallenge) { return { success: true, stage: 'already_clear', elapsedMs: Date.now() - started }; } if (!initial.blocked && !initialUrlChallenge) { return { success: true, stage: 'already_clear', elapsedMs: Date.now() - started }; } } while (Date.now() - started < timeoutMs) { if (await isTurnstileSolved(page) && !urlIndicatesCfChallenge(page.url())) { return { success: true, stage: 'built_in', elapsedMs: Date.now() - started }; } const state = await detectCloudflare(page); if (!state.blocked && !urlIndicatesCfChallenge(page.url()) && state.hasCfClearance) { return { success: true, stage: 'built_in', elapsedMs: Date.now() - started }; } await new Promise((r) => setTimeout(r, pollMs)); } return { success: false, stage: 'timeout', elapsedMs: Date.now() - started }; } /** * 将 Captcha API 返回的 token 注入页面 * @param {import('puppeteer').Page} page * @param {string} token */ async function injectTurnstileToken(page, token) { await page.evaluate((turnstileToken) => { const input = document.querySelector('[name="cf-turnstile-response"]'); if (input) { input.value = turnstileToken; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } const form = document.querySelector('form'); if (form) { form.submit(); } }, token); } module.exports = { waitForTurnstile, waitForTurnstileSurface, extractTurnstileSitekey, extractSitekeyFromFrame, injectTurnstileToken, };