This commit is contained in:
dengqichen 2025-11-17 23:04:44 +08:00
parent 58b9d72ebe
commit c7035b0784
4 changed files with 292 additions and 61 deletions

6
.env
View File

@ -12,3 +12,9 @@ MYSQL_DATABASE=windsurf-auto-register
# CapSolver 验证码识别
CAPSOLVER_API_KEY=CAP-0FCDDA4906E87D9F4FF68EAECD34E320876FBA70E4F30EA1ADCD264EDB15E4BF
# 2Captcha 验证码识别 (支持 Stripe hCaptcha)
TWOCAPTCHA_API_KEY=4e6ac0ee29459018fd5e0c454163cd4e
# YesCaptcha 验证码识别 (优先使用)
YESCAPTCHA_API_KEY=a8a04f2e8ceab43cdf3793e2b72bf4d76f4f4a6b81789

View File

@ -21,6 +21,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"2captcha-ts": "^2.4.1",
"axios": "^1.13.2",
"commander": "^11.0.0",
"dotenv": "^17.2.3",

View File

@ -19,7 +19,9 @@ const { DEFAULT_CONFIG } = require('../config');
const CardGenerator = require('../../card-generator/generator');
const database = require('../../database');
const CapSolverAPI = require('../utils/capsolver-api');
const YesCaptchaAPI = require('../utils/yescaptcha-api');
const BrowserManager = require('../utils/browser-manager');
const { Solver } = require('2captcha-ts');
class WindsurfRegister {
constructor(options = {}) {
@ -29,6 +31,23 @@ class WindsurfRegister {
this.human = new HumanBehavior();
this.emailService = new EmailVerificationService();
this.capsolver = new CapSolverAPI();
this.yescaptcha = new YesCaptchaAPI();
// 初始化 2Captcha
const twoCaptchaKey = process.env.TWOCAPTCHA_API_KEY;
if (twoCaptchaKey && twoCaptchaKey !== '你的2Captcha_API_KEY') {
try {
this.twoCaptchaSolver = new Solver(twoCaptchaKey);
logger.info(this.siteName, ` → 2Captcha 已初始化 (Key: ${twoCaptchaKey.substring(0, 10)}...)`);
} catch (error) {
logger.error(this.siteName, ` → 2Captcha 初始化失败: ${error.message}`);
}
}
// 检查 YesCaptcha 配置
if (this.yescaptcha.apiKey) {
logger.info(this.siteName, ` → YesCaptcha 已初始化 (Key: ${this.yescaptcha.apiKey.substring(0, 10)}...)`);
}
// 浏览器管理器支持多profile并发
this.browserManager = new BrowserManager({
@ -1039,33 +1058,26 @@ class WindsurfRegister {
return true;
}
// 尝试使用 CapSolver API 自动识别
if (this.capsolver.apiKey) {
// 优先尝试使用 YesCaptcha API
if (this.yescaptcha.apiKey) {
try {
logger.info(this.siteName, ' → 使用 CapSolver API 自动识别...');
logger.info(this.siteName, ' → 使用 YesCaptcha API 自动识别...');
logger.info(this.siteName, ` → SiteKey: ${captchaInfo.siteKey.substring(0, 20)}...`);
if (captchaInfo.callback) {
logger.info(this.siteName, ` → Callback: ${captchaInfo.callback}`);
}
// 2. 调用 CapSolver API 获取 token
// 简化 URL移除 hash 和查询参数CapSolver 不需要完整 URL
logger.info(this.siteName, ` → SiteKey: ${captchaInfo.siteKey}`);
const currentUrl = this.page.url();
const simplifiedUrl = new URL(currentUrl);
const cleanUrl = `${simplifiedUrl.protocol}//${simplifiedUrl.host}${simplifiedUrl.pathname}`;
logger.info(this.siteName, ` → 页面 URL: ${currentUrl.substring(0, 80)}...`);
logger.info(this.siteName, ` → 原始 URL: ${currentUrl.substring(0, 80)}...`);
logger.info(this.siteName, ` → 简化 URL: ${cleanUrl}`);
// 调用 YesCaptcha API
const token = await this.yescaptcha.solveHCaptcha(captchaInfo.siteKey, currentUrl, {
isInvisible: true // Stripe 使用 invisible hCaptcha
});
const token = await this.capsolver.solveHCaptcha(captchaInfo.siteKey, cleanUrl);
logger.info(this.siteName, ` → ✓ 获取到 token: ${token.substring(0, 30)}...`);
logger.info(this.siteName, ` → 获取到 token: ${token.substring(0, 30)}...`);
// 3. 注入 token 并触发回调
// 3. 注入 token 并触发回调(按正确顺序)
const injected = await this.page.evaluate((token, callbackName) => {
try {
// 方法1: 设置到隐藏的 textarea
// 步骤1: 设置到隐藏的 textarea必须
const textarea = document.querySelector('[name="h-captcha-response"]');
const textareaG = document.querySelector('[name="g-recaptcha-response"]');
if (textarea) {
@ -1077,24 +1089,55 @@ class WindsurfRegister {
textareaG.innerHTML = token;
}
// 方法2: 使用 hCaptcha API
if (window.hcaptcha && window.hcaptcha.setResponse) {
window.hcaptcha.setResponse(token);
}
// 步骤2: 执行回调函数(关键!)
let callbackExecuted = false;
// 方法3: 触发自定义回调(如果有
// 方法A: 触发自定义回调(优先级最高)
if (callbackName && typeof window[callbackName] === 'function') {
window[callbackName](token);
return { success: true, method: 'callback', callback: callbackName };
callbackExecuted = true;
return { success: true, method: 'custom-callback', callback: callbackName };
}
// 方法4: 触发 hCaptcha 回调事件
if (window.hcaptcha && window.hcaptcha.callback) {
// 方法B: 使用 hcaptcha API 的 setResponse + callback
if (window.hcaptcha) {
if (window.hcaptcha.setResponse) {
window.hcaptcha.setResponse(token);
callbackExecuted = true;
}
// 触发 hcaptcha 的回调
if (window.hcaptcha.callback && typeof window.hcaptcha.callback === 'function') {
window.hcaptcha.callback(token);
return { success: true, method: 'hcaptcha.callback' };
}
}
return { success: true, method: 'textarea' };
// 方法C: 在所有 iframe 中查找回调
const frames = document.querySelectorAll('iframe');
for (const frame of frames) {
try {
if (frame.contentWindow && frame.contentWindow.hcaptcha) {
if (frame.contentWindow.hcaptcha.setResponse) {
frame.contentWindow.hcaptcha.setResponse(token);
}
if (frame.contentWindow.hcaptcha.callback) {
frame.contentWindow.hcaptcha.callback(token);
return { success: true, method: 'iframe-hcaptcha.callback' };
}
}
} catch (e) {
// 跨域iframe无法访问跳过
}
}
// 触发 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 };
}
@ -1104,16 +1147,89 @@ class WindsurfRegister {
throw new Error(`Token 注入失败: ${injected.error}`);
}
logger.success(this.siteName, ` → ✓ hCaptcha 自动识别成功 (方式: ${injected.method})`);
logger.success(this.siteName, ` → ✓ Token 注入成功 (方式: ${injected.method})`);
if (injected.callback) {
logger.info(this.siteName, ` → ✓ 已执行回调函数: ${injected.callback}`);
} else if (injected.callbackExecuted) {
logger.info(this.siteName, ' → ✓ 已执行 hCaptcha 回调');
} else {
logger.warn(this.siteName, ' → ⚠️ 未找到回调函数,可能需要手动触发');
}
logger.info(this.siteName, ' → 等待自动验证...');
// 不要主动点击 checkboxYesCaptcha 的 token 应该自动通过
// 如果主动点击反而会触发 hCaptcha 的二次验证
await this.human.randomDelay(2000, 3000);
// 等待验证完成(如果回调成功,应该很快;如果降级到图片,需要手动)
logger.info(this.siteName, ' → 等待验证完成...');
const startTime = Date.now();
let dialogClosed = false;
let hasImageChallenge = false;
const maxWaitTime = 60000; // 最多60秒包含图片挑战时间
while (Date.now() - startTime < maxWaitTime) {
const status = await this.page.evaluate(() => {
const dialog = document.querySelector('[role="dialog"]');
if (!dialog) return { dialogGone: true, verified: true, hasImageChallenge: false };
const style = window.getComputedStyle(dialog);
const isHidden = style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0';
// 检查 token 是否已填充
const response = document.querySelector('[name="h-captcha-response"]') ||
document.querySelector('[name="g-recaptcha-response"]');
const hasResponse = response && response.value && response.value.length > 20;
// 检查是否有图片挑战
const dialogText = dialog.innerText || '';
const hasImageTask = dialogText.includes('选择') ||
dialogText.includes('Select') ||
dialog.querySelector('img[src*="hcaptcha"]') ||
dialog.querySelector('[class*="task"]');
return {
dialogGone: !dialog || isHidden,
verified: hasResponse,
hasImageChallenge: hasImageTask,
dialogText: dialogText.substring(0, 80)
};
});
// 第一次检测到图片挑战
if (status.hasImageChallenge && !hasImageChallenge) {
hasImageChallenge = true;
logger.warn(this.siteName, ' → ⚠️ Token 被降级,出现图片挑战!');
logger.info(this.siteName, ` → 任务: ${status.dialogText}`);
logger.warn(this.siteName, ' → 请手动完成图片验证...');
}
// 验证完成
if (status.dialogGone || (status.verified && !status.hasImageChallenge)) {
dialogClosed = true;
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
logger.success(this.siteName, ` → ✓ 验证完成(耗时${elapsed}秒)`);
break;
}
// 动态调整检查间隔
const interval = hasImageChallenge ? 2000 : 1000;
await new Promise(resolve => setTimeout(resolve, interval));
}
if (!dialogClosed) {
logger.warn(this.siteName, ' → ⚠️ 验证超时,继续尝试支付...');
}
await this.human.randomDelay(1000, 2000);
return true;
} catch (error) {
logger.error(this.siteName, ` → ✗ CapSolver 自动识别失败: ${error.message}`);
logger.error(this.siteName, ` → ✗ YesCaptcha 自动识别失败: ${error.message}`);
logger.warn(this.siteName, ' → 将回退到手动模式');
}
} else {
logger.warn(this.siteName, ' → 未配置 CapSolver API Key');
logger.warn(this.siteName, ' → 未配置 YesCaptcha API Key');
}
// 手动等待模式
@ -1272,34 +1388,10 @@ class WindsurfRegister {
}
if (captchaDetected) {
logger.warn(this.siteName, ' → ⚠️ 检测到验证码请手动完成等待60秒...');
// 等待用户手动完成验证码
const waitStart = Date.now();
const maxWait = 60000; // 等待60秒
while (Date.now() - waitStart < maxWait) {
// 检查验证码是否已完成
const captchaCompleted = await this.page.evaluate(() => {
// 检查验证码对话框是否消失
const modal = document.querySelector('[role="dialog"]');
const modalVisible = modal && window.getComputedStyle(modal).display !== 'none';
return !modalVisible;
});
if (captchaCompleted) {
const elapsed = ((Date.now() - waitStart) / 1000).toFixed(1);
logger.success(this.siteName, ` → ✓ 验证码已完成(耗时${elapsed}秒)`);
break;
}
// 每秒检查一次
await new Promise(resolve => setTimeout(resolve, 1000));
}
if (Date.now() - waitStart >= maxWait) {
logger.warn(this.siteName, ' → ⚠️ 验证码等待超时,继续尝试...');
}
logger.info(this.siteName, ' → 检测到验证码,使用 YesCaptcha API 处理...');
// 使用 YesCaptcha API 处理
await this.handleHCaptcha();
await this.human.randomDelay(1000, 2000);
} else {
logger.info(this.siteName, ` → ✓ 无验证码(检查${captchaCheckCount}次,耗时${((Date.now() - checkStartTime) / 1000).toFixed(1)}秒)`);
}

View File

@ -0,0 +1,132 @@
const axios = require('axios');
const logger = require('../../../shared/logger');
/**
* YesCaptcha API 封装
* 支持 hCaptchareCaptchaTurnstile 等验证码识别
*/
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;