133 lines
4.4 KiB
JavaScript
133 lines
4.4 KiB
JavaScript
const axios = require('axios');
|
||
const logger = require('../../../shared/logger');
|
||
|
||
/**
|
||
* YesCaptcha API 封装
|
||
* 支持 hCaptcha、reCaptcha、Turnstile 等验证码识别
|
||
*/
|
||
class YesCaptchaAPI {
|
||
constructor() {
|
||
this.apiKey = process.env.YESCAPTCHA_API_KEY;
|
||
this.apiUrl = 'https://api.yescaptcha.com';
|
||
}
|
||
|
||
/**
|
||
* 识别 hCaptcha
|
||
* @param {string} siteKey - 网站的 sitekey
|
||
* @param {string} pageUrl - 网页 URL
|
||
* @param {object} options - 可选参数 {isInvisible, rqdata, userAgent}
|
||
* @returns {Promise<string>} - hCaptcha token
|
||
*/
|
||
async solveHCaptcha(siteKey, pageUrl, options = {}) {
|
||
if (!this.apiKey) {
|
||
throw new Error('YesCaptcha API Key 未配置');
|
||
}
|
||
|
||
logger.info('YesCaptcha', '开始识别 hCaptcha...');
|
||
logger.info('YesCaptcha', `SiteKey: ${siteKey.substring(0, 20)}...`);
|
||
|
||
try {
|
||
// 1. 创建任务
|
||
const requestBody = {
|
||
clientKey: this.apiKey,
|
||
task: {
|
||
type: 'HCaptchaTaskProxyless',
|
||
websiteURL: pageUrl,
|
||
websiteKey: siteKey
|
||
}
|
||
};
|
||
|
||
// 添加可选参数
|
||
if (options.isInvisible !== undefined) {
|
||
requestBody.task.isInvisible = options.isInvisible;
|
||
}
|
||
if (options.rqdata) {
|
||
requestBody.task.rqdata = options.rqdata;
|
||
}
|
||
if (options.userAgent) {
|
||
requestBody.task.userAgent = options.userAgent;
|
||
}
|
||
|
||
logger.info('YesCaptcha', `请求 URL: ${pageUrl}`);
|
||
logger.info('YesCaptcha', `API Key: ${this.apiKey.substring(0, 10)}...`);
|
||
|
||
const createTaskResponse = await axios.post(`${this.apiUrl}/createTask`, requestBody);
|
||
|
||
if (createTaskResponse.data.errorId !== 0) {
|
||
logger.error('YesCaptcha', `API 返回错误: ${JSON.stringify(createTaskResponse.data)}`);
|
||
throw new Error(`创建任务失败: ${createTaskResponse.data.errorDescription}`);
|
||
}
|
||
|
||
const taskId = createTaskResponse.data.taskId;
|
||
logger.info('YesCaptcha', `任务已创建,TaskID: ${taskId}`);
|
||
|
||
// 2. 轮询获取结果(最多等待120秒)
|
||
const maxAttempts = 40; // 120秒 / 3秒
|
||
let attempts = 0;
|
||
|
||
while (attempts < maxAttempts) {
|
||
attempts++;
|
||
|
||
// 等待3秒再查询
|
||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
|
||
logger.info('YesCaptcha', `查询结果... (${attempts}/${maxAttempts})`);
|
||
|
||
const getResultResponse = await axios.post(`${this.apiUrl}/getTaskResult`, {
|
||
clientKey: this.apiKey,
|
||
taskId: taskId
|
||
});
|
||
|
||
if (getResultResponse.data.errorId !== 0) {
|
||
throw new Error(`查询结果失败: ${getResultResponse.data.errorDescription}`);
|
||
}
|
||
|
||
const status = getResultResponse.data.status;
|
||
|
||
if (status === 'ready') {
|
||
const token = getResultResponse.data.solution.gRecaptchaResponse;
|
||
logger.success('YesCaptcha', `✓ 识别成功!耗时: ${attempts * 3}秒`);
|
||
|
||
// 返回 userAgent 如果有的话
|
||
if (getResultResponse.data.solution.userAgent) {
|
||
logger.info('YesCaptcha', `返回的 UserAgent: ${getResultResponse.data.solution.userAgent.substring(0, 50)}...`);
|
||
}
|
||
|
||
return token;
|
||
} else if (status === 'processing') {
|
||
// 继续等待
|
||
continue;
|
||
} else {
|
||
throw new Error(`未知状态: ${status}`);
|
||
}
|
||
}
|
||
|
||
throw new Error('识别超时(120秒)');
|
||
|
||
} catch (error) {
|
||
// 详细的错误日志
|
||
if (error.response) {
|
||
// API 返回了错误响应
|
||
logger.error('YesCaptcha', `HTTP ${error.response.status}: ${error.response.statusText}`);
|
||
logger.error('YesCaptcha', `响应数据: ${JSON.stringify(error.response.data)}`);
|
||
|
||
if (error.response.status === 400) {
|
||
logger.error('YesCaptcha', '可能原因:');
|
||
logger.error('YesCaptcha', ' 1. API Key 无效或已过期');
|
||
logger.error('YesCaptcha', ' 2. 余额不足');
|
||
logger.error('YesCaptcha', ' 3. siteKey 或 URL 格式错误');
|
||
}
|
||
} else if (error.request) {
|
||
// 请求已发送但没有收到响应
|
||
logger.error('YesCaptcha', '无法连接到 YesCaptcha API');
|
||
} else {
|
||
// 其他错误
|
||
logger.error('YesCaptcha', `识别失败: ${error.message}`);
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = YesCaptchaAPI;
|