2026-06-20 04:47:34 +08:00
|
|
|
/**
|
|
|
|
|
* Turnstile 内置求解:等待 widget 自动完成 + token 轮询
|
|
|
|
|
*/
|
2026-06-29 04:54:41 +08:00
|
|
|
const { detectCloudflare, isTurnstileSolved, urlIndicatesCfChallenge } = require('./cf-detector');
|
2026-06-20 04:47:34 +08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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<TurnstileResult>}
|
|
|
|
|
*/
|
|
|
|
|
async function waitForTurnstile(page, options = {}) {
|
|
|
|
|
const timeoutMs = options.timeoutMs ?? 60000;
|
|
|
|
|
const pollMs = options.pollMs ?? 500;
|
|
|
|
|
const started = Date.now();
|
|
|
|
|
|
|
|
|
|
const initial = await detectCloudflare(page);
|
2026-06-29 04:54:41 +08:00
|
|
|
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 };
|
|
|
|
|
}
|
2026-06-20 04:47:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (Date.now() - started < timeoutMs) {
|
2026-06-29 04:54:41 +08:00
|
|
|
if (await isTurnstileSolved(page) && !urlIndicatesCfChallenge(page.url())) {
|
2026-06-20 04:47:34 +08:00
|
|
|
return { success: true, stage: 'built_in', elapsedMs: Date.now() - started };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const state = await detectCloudflare(page);
|
2026-06-29 04:54:41 +08:00
|
|
|
if (!state.blocked && !urlIndicatesCfChallenge(page.url()) && state.hasCfClearance) {
|
2026-06-20 04:47:34 +08:00
|
|
|
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<string|null>}
|
|
|
|
|
*/
|
|
|
|
|
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,
|
|
|
|
|
};
|