/** * Turnstile 内置求解:等待 widget 自动完成 + token 轮询 */ const { detectCloudflare, isTurnstileSolved, urlIndicatesCfChallenge } = require('./cf-detector'); /** * @typedef {Object} TurnstileResult * @property {boolean} success * @property {string} stage built_in|timeout|already_clear * @property {number} elapsedMs */ /** * 等待 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 }; } /** * 从页面提取 Turnstile sitekey * @param {import('puppeteer').Page} page * @returns {Promise} */ async function extractTurnstileSitekey(page) { return page.evaluate(() => { const widget = document.querySelector('.cf-turnstile[data-sitekey], [data-sitekey]'); if (widget) { return widget.getAttribute('data-sitekey'); } const iframe = document.querySelector('iframe[src*="challenges.cloudflare.com"]'); if (iframe && iframe.src) { const match = iframe.src.match(/[?&]sitekey=([^&]+)/); if (match) return decodeURIComponent(match[1]); } return null; }).catch(() => null); } /** * 将 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, extractTurnstileSitekey, injectTurnstileToken, };