dasdasd
This commit is contained in:
parent
a3518c7054
commit
d52f97027b
318
src/tools/account-register/utils/hcaptcha-solver.js
Normal file
318
src/tools/account-register/utils/hcaptcha-solver.js
Normal file
@ -0,0 +1,318 @@
|
||||
/**
|
||||
* HCaptcha 通用自动解决器
|
||||
*
|
||||
* 功能:
|
||||
* - 自动检测 hCaptcha 元素
|
||||
* - 调用多种 Captcha API(YesCaptcha, 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;
|
||||
Loading…
Reference in New Issue
Block a user