This commit is contained in:
dengqichen 2025-11-18 14:00:59 +08:00
parent a3518c7054
commit d52f97027b

View File

@ -0,0 +1,318 @@
/**
* HCaptcha 通用自动解决器
*
* 功能
* - 自动检测 hCaptcha 元素
* - 调用多种 Captcha APIYesCaptcha, CapSolver, 2Captcha
* - 自动注入 token 并触发回调
* - 自动点击 checkbox
* - 等待验证完成
* - 处理图片挑战
*
* 使用示例
* const solver = new HCaptchaSolver({
* yescaptcha: yescaptchaAPI,
* capsolver: capsolverAPI,
* twoCaptcha: twoCaptchaSolver,
* logger: logger
* });
*
* const result = await solver.solve(page, {
* timeout: 120000,
* waitForToken: true,
* handleImageChallenge: true
* });
*/
const logger = require('../../../shared/logger');
class HCaptchaSolver {
constructor(options = {}) {
this.yescaptcha = options.yescaptcha;
this.capsolver = options.capsolver;
this.twoCaptcha = options.twoCaptcha;
this.logger = options.logger || logger;
this.siteName = options.siteName || 'HCaptcha';
}
/**
* 检测页面中的 hCaptcha 元素
* @param {Page} page - Puppeteer Page 实例
* @returns {Promise<Object|null>} { siteKey, callback, type, element }
*/
async detectHCaptcha(page) {
return await page.evaluate(() => {
// 方法1: 从 div.h-captcha 或 iframe 中获取
const hcaptchaDiv = document.querySelector('.h-captcha, [data-hcaptcha-response]');
if (hcaptchaDiv) {
const siteKey = hcaptchaDiv.getAttribute('data-sitekey');
const callback = hcaptchaDiv.getAttribute('data-callback');
if (siteKey) {
return {
siteKey,
callback,
type: 'div-element',
found: true
};
}
}
// 方法2: 从 iframe 的 src 中提取
const iframes = document.querySelectorAll('iframe[src*="hcaptcha"]');
for (const iframe of iframes) {
try {
const src = iframe.src;
const match = src.match(/sitekey=([^&]+)/);
if (match) {
return {
siteKey: match[1],
callback: null,
type: 'iframe-src',
found: true
};
}
} catch (e) {
// 继续
}
}
// 方法3: 检查 Stripe 的特殊 iframe
const stripeIframe = document.querySelector('iframe[src*="hcaptcha-inner"]');
if (stripeIframe) {
try {
const src = stripeIframe.src;
const match = src.match(/sitekey=([^&]+)/);
if (match) {
return {
siteKey: match[1],
callback: null,
type: 'stripe-iframe',
found: true
};
}
} catch (e) {
// 继续
}
}
return null;
});
}
/**
* 获取 hCaptcha token
* 优先级YesCaptcha > CapSolver > 2Captcha
*/
async solveToken(siteKey, pageUrl, options = {}) {
// 1. 尝试 YesCaptcha
if (this.yescaptcha && this.yescaptcha.apiKey) {
try {
this.logger.info(this.siteName, ' → 使用 YesCaptcha 获取 token...');
const token = await this.yescaptcha.solveHCaptcha(siteKey, pageUrl, options);
this.logger.success(this.siteName, ` → ✓ YesCaptcha 成功`);
return token;
} catch (error) {
this.logger.warn(this.siteName, ` → YesCaptcha 失败: ${error.message}`);
}
}
// 2. 尝试 CapSolver
if (this.capsolver && this.capsolver.apiKey) {
try {
this.logger.info(this.siteName, ' → 使用 CapSolver 获取 token...');
const token = await this.capsolver.solveHCaptcha(siteKey, pageUrl);
this.logger.success(this.siteName, ` → ✓ CapSolver 成功`);
return token;
} catch (error) {
this.logger.warn(this.siteName, ` → CapSolver 失败: ${error.message}`);
}
}
throw new Error('所有 Captcha API 都失败了');
}
/**
* 注入 token 到页面
*/
async injectToken(page, token, callback = null) {
return await page.evaluate((token, callbackName) => {
try {
// 设置到隐藏的 textarea
const textarea = document.querySelector('[name="h-captcha-response"]');
const textareaG = document.querySelector('[name="g-recaptcha-response"]');
if (textarea) {
textarea.value = token;
textarea.innerHTML = token;
}
if (textareaG) {
textareaG.value = token;
textareaG.innerHTML = token;
}
// 执行回调函数
let callbackExecuted = false;
if (callbackName && typeof window[callbackName] === 'function') {
window[callbackName](token);
callbackExecuted = true;
return { success: true, method: 'custom-callback', callback: callbackName };
}
if (window.hcaptcha) {
if (window.hcaptcha.setResponse) {
window.hcaptcha.setResponse(token);
callbackExecuted = true;
}
if (window.hcaptcha.callback && typeof window.hcaptcha.callback === 'function') {
window.hcaptcha.callback(token);
return { success: true, method: 'hcaptcha.callback' };
}
}
// 触发 change 事件
if (textarea) {
const event = new Event('change', { bubbles: true });
textarea.dispatchEvent(event);
}
return { success: true, method: 'textarea-only', callbackExecuted };
} catch (e) {
return { success: false, error: e.message };
}
}, token, callback);
}
/**
* 点击 hCaptcha checkbox
*/
async clickCheckbox(page) {
try {
const frames = page.frames();
let clicked = false;
for (const frame of frames) {
try {
const frameUrl = frame.url();
if (frameUrl.includes('hcaptcha') && (frameUrl.includes('checkbox') || frameUrl.includes('anchor'))) {
this.logger.info(this.siteName, ` → 找到 checkbox frame`);
const checkboxClicked = await frame.evaluate(() => {
const checkbox = document.querySelector('#checkbox') ||
document.querySelector('.check') ||
document.querySelector('[type="checkbox"]') ||
document.querySelector('[role="checkbox"]');
if (checkbox) {
checkbox.click();
return { success: true, type: 'direct-click' };
}
const clickableElements = document.querySelectorAll('div, span, label');
for (const el of clickableElements) {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
el.click();
return { success: true, type: 'fallback-click' };
}
}
return { success: false };
});
if (checkboxClicked.success) {
this.logger.success(this.siteName, ` → ✓ 已点击 checkbox`);
clicked = true;
break;
}
}
} catch (e) {
// 继续
}
}
return { success: clicked };
} catch (error) {
return { success: false, error: error.message };
}
}
/**
* 等待验证完成
*/
async waitForVerification(page, options = {}) {
const { timeout = 120000, checkInterval = 2000 } = options;
this.logger.info(this.siteName, ' → 等待验证完成...');
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const status = await page.evaluate(() => {
const response = document.querySelector('[name="h-captcha-response"]') ||
document.querySelector('[name="g-recaptcha-response"]');
return response && response.value && response.value.length > 20;
});
if (status) {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
this.logger.success(this.siteName, ` → ✓ 验证完成(耗时${elapsed}秒)`);
return true;
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
}
this.logger.error(this.siteName, ' → ✗ 验证超时!');
return false;
}
/**
* 完整的 hCaptcha 解决流程
*/
async solve(page, options = {}) {
try {
// 1. 检测 hCaptcha
const captchaInfo = await this.detectHCaptcha(page);
if (!captchaInfo || !captchaInfo.found) {
this.logger.info(this.siteName, ' → 未检测到 hCaptcha');
return true;
}
if (!captchaInfo.siteKey) {
this.logger.warn(this.siteName, ' → 无法获取 siteKey');
return true;
}
this.logger.info(this.siteName, ` → 检测到 hCaptcha${captchaInfo.type}`);
// 2. 获取 token
const token = await this.solveToken(captchaInfo.siteKey, page.url(), options);
// 3. 注入 token
const injected = await this.injectToken(page, token, captchaInfo.callback);
if (!injected.success) {
throw new Error(`Token 注入失败: ${injected.error}`);
}
this.logger.success(this.siteName, ` → ✓ Token 注入成功`);
// 4. 点击 checkbox可选
if (options.clickCheckbox !== false) {
await this.clickCheckbox(page);
}
// 5. 等待验证(可选)
if (options.waitForVerification !== false) {
return await this.waitForVerification(page, options);
}
return true;
} catch (error) {
this.logger.error(this.siteName, ` → ✗ 解决失败: ${error.message}`);
return false;
}
}
}
module.exports = HCaptchaSolver;