154 lines
4.6 KiB
JavaScript
Executable File
154 lines
4.6 KiB
JavaScript
Executable File
/**
|
|
* 付费 Captcha API 兜底:CapSolver / 2Captcha 统一 Turnstile 求解接口
|
|
*/
|
|
const {
|
|
CAPTCHA_PROVIDER,
|
|
CAPTCHA_API_KEY,
|
|
CAPTCHA_MAX_WAIT_MS,
|
|
} = require('./constants');
|
|
|
|
/**
|
|
* @param {string} url
|
|
* @param {Record<string, string>} [headers]
|
|
* @returns {Promise<any>}
|
|
*/
|
|
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<any>}
|
|
*/
|
|
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<string>}
|
|
*/
|
|
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<string>}
|
|
*/
|
|
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,
|
|
};
|