/** * 付费 Captcha API 兜底:CapSolver / 2Captcha 统一 Turnstile 求解接口 */ const { CAPTCHA_PROVIDER, CAPTCHA_API_KEY, CAPTCHA_MAX_WAIT_MS, } = require('./constants'); /** * @param {string} url * @param {Record} [headers] * @returns {Promise} */ async function httpJsonPost(url, body, headers = {}) { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify(body), }); const text = await res.text(); try { return JSON.parse(text); } catch (_) { throw new Error(`Captcha API 返回非 JSON: ${text.slice(0, 200)}`); } } /** * @param {string} url * @returns {Promise} */ async function httpJsonGet(url) { const res = await fetch(url); const text = await res.text(); try { return JSON.parse(text); } catch (_) { throw new Error(`Captcha API 返回非 JSON: ${text.slice(0, 200)}`); } } /** * CapSolver Turnstile 求解 * @param {{ pageUrl: string, sitekey: string, apiKey: string }} params * @returns {Promise} */ async function solveViaCapSolver({ pageUrl, sitekey, apiKey }) { const create = await httpJsonPost('https://api.capsolver.com/createTask', { clientKey: apiKey, task: { type: 'AntiTurnstileTaskProxyLess', websiteURL: pageUrl, websiteKey: sitekey, }, }); if (create.errorId !== 0 || !create.taskId) { throw new Error(`CapSolver createTask 失败: ${create.errorDescription || create.errorCode || 'unknown'}`); } const deadline = Date.now() + CAPTCHA_MAX_WAIT_MS; while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, 3000)); const result = await httpJsonPost('https://api.capsolver.com/getTaskResult', { clientKey: apiKey, taskId: create.taskId, }); if (result.status === 'ready' && result.solution && result.solution.token) { return result.solution.token; } if (result.errorId !== 0) { throw new Error(`CapSolver getTaskResult 失败: ${result.errorDescription || result.errorCode}`); } } throw new Error('CapSolver 求解超时'); } /** * 2Captcha Turnstile 求解 * @param {{ pageUrl: string, sitekey: string, apiKey: string }} params * @returns {Promise} */ async function solveVia2Captcha({ pageUrl, sitekey, apiKey }) { const params = new URLSearchParams({ key: apiKey, method: 'turnstile', sitekey, pageurl: pageUrl, json: '1', }); const create = await httpJsonGet(`https://2captcha.com/in.php?${params.toString()}`); if (create.status !== 1 || !create.request) { throw new Error(`2Captcha in.php 失败: ${create.request || create.error_text || 'unknown'}`); } const taskId = create.request; const deadline = Date.now() + CAPTCHA_MAX_WAIT_MS; while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, 5000)); const result = await httpJsonGet( `https://2captcha.com/res.php?key=${encodeURIComponent(apiKey)}&action=get&id=${encodeURIComponent(taskId)}&json=1` ); if (result.status === 1 && result.request) { return result.request; } if (result.request && result.request !== 'CAPCHA_NOT_READY') { throw new Error(`2Captcha res.php 失败: ${result.request}`); } } throw new Error('2Captcha 求解超时'); } /** * 统一 Turnstile 求解入口 * @param {{ pageUrl: string, sitekey: string, provider?: string, apiKey?: string }} params * @returns {Promise<{ token: string, provider: string }>} */ async function solveTurnstile({ pageUrl, sitekey, provider, apiKey }) { const resolvedProvider = (provider || CAPTCHA_PROVIDER).toLowerCase(); const resolvedKey = apiKey || CAPTCHA_API_KEY; if (!sitekey) { throw new Error('无法提取 Turnstile sitekey'); } if (!resolvedKey) { throw new Error('未配置 CAPTCHA_API_KEY,无法使用付费 Captcha 兜底'); } let token; if (resolvedProvider === '2captcha') { token = await solveVia2Captcha({ pageUrl, sitekey, apiKey: resolvedKey }); } else { token = await solveViaCapSolver({ pageUrl, sitekey, apiKey: resolvedKey }); } return { token, provider: resolvedProvider }; } /** * Captcha API 是否已配置 * @returns {boolean} */ function isCaptchaConfigured() { return CAPTCHA_API_KEY.length > 0; } module.exports = { solveTurnstile, isCaptchaConfigured, };