This commit is contained in:
dengqichen 2025-11-17 21:14:17 +08:00
parent 468d4d6d73
commit 75986287d1
2 changed files with 343 additions and 82 deletions

View File

@ -836,7 +836,21 @@ class WindsurfRegister {
}
/**
* 填写银行卡表单
* 彻底清空输入框通过DOM直接操作
*/
async clearInputField(selector) {
await this.page.evaluate((sel) => {
const field = document.querySelector(sel);
if (field) {
field.value = '';
field.dispatchEvent(new Event('input', { bubbles: true }));
field.dispatchEvent(new Event('change', { bubbles: true }));
}
}, selector);
}
/**
* 填写银行卡表单每次都彻底清空后重新填写
*/
async fillCardForm(card, isRetry = false) {
if (!isRetry) {
@ -866,56 +880,81 @@ class WindsurfRegister {
}
}
// 填写卡号
logger.info(this.siteName, ' → 填写卡号...');
const cardNumberField = await this.page.$('#cardNumber');
await cardNumberField.click();
await this.human.randomDelay(300, 500);
if (isRetry) {
await this.page.keyboard.down('Control');
await this.page.keyboard.press('A');
await this.page.keyboard.up('Control');
}
await this.page.keyboard.press('Backspace');
await this.human.randomDelay(500, 1000);
await cardNumberField.type(card.number, { delay: 250 });
// ===== 填写卡号 =====
logger.info(this.siteName, ` → 填写卡号: ${card.number}...`);
// 填写有效期
logger.info(this.siteName, ' → 填写有效期...');
const cardExpiryField = await this.page.$('#cardExpiry');
await cardExpiryField.click();
await this.human.randomDelay(200, 300);
if (isRetry) {
await this.page.keyboard.down('Control');
await this.page.keyboard.press('A');
await this.page.keyboard.up('Control');
}
await this.page.keyboard.press('Backspace');
// 1. 彻底清空卡号
await this.clearInputField('#cardNumber');
await this.human.randomDelay(300, 500);
// 2. 点击并聚焦
await this.page.click('#cardNumber');
await this.human.randomDelay(300, 500);
// 3. 再次确保清空(按多次 Backspace
for (let i = 0; i < 25; i++) {
await this.page.keyboard.press('Backspace');
}
await this.human.randomDelay(300, 500);
// 4. 填写新卡号
await this.page.type('#cardNumber', card.number, { delay: 100 });
await this.human.randomDelay(500, 800);
logger.success(this.siteName, ' → ✓ 卡号已填写');
// ===== 填写有效期 =====
const expiry = `${card.month}${card.year}`;
await cardExpiryField.type(expiry, { delay: 250 });
logger.info(this.siteName, ` → 填写有效期: ${card.month}/${card.year}...`);
// 填写CVC
logger.info(this.siteName, ' → 填写CVC...');
const cardCvcField = await this.page.$('#cardCvc');
await cardCvcField.click();
// 1. 彻底清空
await this.clearInputField('#cardExpiry');
await this.human.randomDelay(200, 300);
if (isRetry) {
await this.page.keyboard.down('Control');
await this.page.keyboard.press('A');
await this.page.keyboard.up('Control');
}
await this.page.keyboard.press('Backspace');
await this.human.randomDelay(300, 500);
await cardCvcField.type(card.cvv, { delay: 250 });
// 2. 点击并聚焦
await this.page.click('#cardExpiry');
await this.human.randomDelay(200, 300);
// 3. 再次确保清空
for (let i = 0; i < 10; i++) {
await this.page.keyboard.press('Backspace');
}
await this.human.randomDelay(300, 500);
// 4. 填写新有效期
await this.page.type('#cardExpiry', expiry, { delay: 100 });
await this.human.randomDelay(500, 800);
logger.success(this.siteName, ' → ✓ 有效期已填写');
// ===== 填写CVC =====
logger.info(this.siteName, ` → 填写CVC: ${card.cvv}...`);
// 1. 彻底清空
await this.clearInputField('#cardCvc');
await this.human.randomDelay(200, 300);
// 2. 点击并聚焦
await this.page.click('#cardCvc');
await this.human.randomDelay(200, 300);
// 3. 再次确保清空
for (let i = 0; i < 10; i++) {
await this.page.keyboard.press('Backspace');
}
await this.human.randomDelay(300, 500);
// 4. 填写新CVC
await this.page.type('#cardCvc', card.cvv, { delay: 100 });
await this.human.randomDelay(500, 800);
logger.success(this.siteName, ' → ✓ CVC已填写');
// ===== 首次填写:持卡人姓名和地址 =====
if (!isRetry) {
// 首次填写:填写持卡人姓名和地址
logger.info(this.siteName, ' → 填写持卡人姓名...');
await this.clearInputField('#billingName');
await this.page.click('#billingName');
await this.human.randomDelay(300, 500);
const fullName = `${this.accountData.firstName} ${this.accountData.lastName}`;
await this.page.type('#billingName', fullName, { delay: 200 });
await this.page.type('#billingName', fullName, { delay: 100 });
logger.info(this.siteName, ' → 选择地址:中国澳门特别行政区...');
await this.page.select('#billingCountry', 'MO');
@ -931,67 +970,171 @@ class WindsurfRegister {
}
}
}
logger.success(this.siteName, ' → ✓ 银行卡信息填写完成');
}
/**
* 处理 hCaptcha 验证码
* 处理 hCaptcha 验证码使用 CapSolver API 自动识别
*/
async handleHCaptcha() {
const hasHCaptcha = await this.page.evaluate(() => {
// 获取 captcha 信息(支持 Stripe iframe 和标准 hCaptcha
const captchaInfo = await this.page.evaluate(() => {
// 方式1: Stripe 的 hCaptcha iframe
const stripeFrame = document.querySelector('iframe[src*="hcaptcha-inner"]');
if (stripeFrame) {
try {
const url = new URL(stripeFrame.src);
const hash = url.hash.substring(1);
const params = new URLSearchParams(hash);
const sitekey = params.get('sitekey');
if (sitekey) {
return {
type: 'stripe-iframe',
siteKey: sitekey,
callback: null,
containerId: null
};
}
} catch (e) {
// 继续尝试其他方式
}
}
// 方式2: 标准的 hCaptcha
const hcaptchaDiv = document.querySelector('.h-captcha');
if (hcaptchaDiv) {
return {
type: 'standard',
siteKey: hcaptchaDiv.getAttribute('data-sitekey'),
callback: hcaptchaDiv.getAttribute('data-callback'),
containerId: hcaptchaDiv.id || hcaptchaDiv.getAttribute('data-hcaptcha-widget-id')
};
}
// 方式3: hcaptcha.com iframe
const hcaptchaFrame = document.querySelector('iframe[src*="hcaptcha.com"]');
const hcaptchaCheckbox = document.querySelector('.h-captcha');
return !!(hcaptchaFrame || hcaptchaCheckbox);
if (hcaptchaFrame) {
return {
type: 'iframe',
siteKey: null,
callback: null,
containerId: null
};
}
return null;
});
if (!hasHCaptcha) return true;
if (!captchaInfo) {
logger.info(this.siteName, ' → 未检测到 hCaptcha跳过');
return true;
}
logger.warn(this.siteName, ' → 检测到 hCaptcha 验证码');
logger.info(this.siteName, ` → 检测到 hCaptcha类型: ${captchaInfo.type}`);
if (!captchaInfo.siteKey) {
logger.warn(this.siteName, ' → 无法获取 siteKey跳过自动识别');
return true;
}
// 尝试使用 CapSolver API 自动识别
if (this.capsolver.apiKey) {
try {
logger.info(this.siteName, ' → 尝试使用 CapSolver 自动识别...');
const siteKey = await this.page.evaluate(() => {
const hcaptchaDiv = document.querySelector('.h-captcha');
return hcaptchaDiv ? hcaptchaDiv.getAttribute('data-sitekey') : null;
});
logger.info(this.siteName, ' → 使用 CapSolver API 自动识别...');
if (siteKey) {
logger.info(this.siteName, ` → SiteKey: ${captchaInfo.siteKey.substring(0, 20)}...`);
if (captchaInfo.callback) {
logger.info(this.siteName, ` → Callback: ${captchaInfo.callback}`);
}
// 2. 调用 CapSolver API 获取 token
const currentUrl = this.page.url();
const token = await this.capsolver.solveHCaptcha(siteKey, currentUrl);
await this.page.evaluate((token) => {
const token = await this.capsolver.solveHCaptcha(captchaInfo.siteKey, currentUrl);
logger.info(this.siteName, ` → 获取到 token: ${token.substring(0, 30)}...`);
// 3. 注入 token 并触发回调
const injected = await this.page.evaluate((token, callbackName) => {
try {
// 方法1: 设置到隐藏的 textarea
const textarea = document.querySelector('[name="h-captcha-response"]');
if (textarea) textarea.value = token;
const textareaG = document.querySelector('[name="g-recaptcha-response"]');
if (textarea) {
textarea.value = token;
textarea.innerHTML = token;
}
if (textareaG) {
textareaG.value = token;
textareaG.innerHTML = token;
}
// 方法2: 使用 hCaptcha API
if (window.hcaptcha && window.hcaptcha.setResponse) {
window.hcaptcha.setResponse(token);
}
}, token);
logger.success(this.siteName, ' → ✓ hCaptcha 自动识别成功');
await this.human.randomDelay(2000, 3000);
return true;
}
} catch (error) {
logger.error(this.siteName, ` → ✗ 自动识别失败: ${error.message}`);
}
// 方法3: 触发自定义回调(如果有)
if (callbackName && typeof window[callbackName] === 'function') {
window[callbackName](token);
return { success: true, method: 'callback', callback: callbackName };
}
// 手动等待
// 方法4: 触发 hCaptcha 回调事件
if (window.hcaptcha && window.hcaptcha.callback) {
window.hcaptcha.callback(token);
return { success: true, method: 'hcaptcha.callback' };
}
return { success: true, method: 'textarea' };
} catch (e) {
return { success: false, error: e.message };
}
}, token, captchaInfo.callback);
if (!injected.success) {
throw new Error(`Token 注入失败: ${injected.error}`);
}
logger.success(this.siteName, ` → ✓ hCaptcha 自动识别成功 (方式: ${injected.method})`);
await this.human.randomDelay(1000, 2000);
return true;
} catch (error) {
logger.error(this.siteName, ` → ✗ CapSolver 自动识别失败: ${error.message}`);
logger.warn(this.siteName, ' → 将回退到手动模式');
}
} else {
logger.warn(this.siteName, ' → 未配置 CapSolver API Key');
}
// 手动等待模式
logger.warn(this.siteName, ' → 请手动完成验证码等待120秒...');
const startWait = Date.now();
while (Date.now() - startWait < 120000) {
const captchaSolved = await this.page.evaluate(() => {
const response = document.querySelector('[name="h-captcha-response"]');
return response && response.value.length > 0;
const response = document.querySelector('[name="h-captcha-response"]') ||
document.querySelector('[name="g-recaptcha-response"]');
return response && response.value && response.value.length > 0;
});
if (captchaSolved) {
logger.success(this.siteName, ' → ✓ 验证码已完成');
return true;
}
if ((Date.now() - startWait) % 10000 === 0) {
const elapsed = Math.floor((Date.now() - startWait) / 1000);
logger.info(this.siteName, ` → 等待验证码... (${elapsed}秒)`);
// 每10秒输出一次进度
const elapsed = Date.now() - startWait;
if (elapsed > 0 && elapsed % 10000 === 0) {
logger.info(this.siteName, ` → 等待验证码... (${(elapsed/1000).toFixed(0)}秒)`);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
logger.error(this.siteName, ' → ✗ 验证码超时120秒');
return false;
}
@ -1072,18 +1215,64 @@ class WindsurfRegister {
// 等待一下让 Stripe 开始处理
await this.human.randomDelay(2000, 3000);
// 检查是否有验证码(快速检查)
const hasHCaptcha = await this.page.evaluate(() => {
return !!(
document.querySelector('iframe[src*="hcaptcha.com"]') ||
document.querySelector('.h-captcha')
// 检查是否有验证码(只在第一次提交时检查)
if (retryCount === 0) {
// 等待可能的验证码弹窗/iframe 加载最多等5秒
let captchaDetected = false;
const checkStartTime = Date.now();
const maxCheckTime = 5000; // 最多检查5秒
let captchaCheckCount = 0;
while (Date.now() - checkStartTime < maxCheckTime && !captchaDetected) {
captchaCheckCount++;
const captchaCheck = await this.page.evaluate(() => {
// 检查多种可能的验证码 iframe 和对话框
const stripeHCaptchaFrame = document.querySelector('iframe[src*="hcaptcha-inner"]');
const hcaptchaFrame = document.querySelector('iframe[src*="hcaptcha.com"]');
const hcaptchaDiv = document.querySelector('.h-captcha');
// 检查是否有"还需一步即可完成"的对话框
const modalTexts = Array.from(document.querySelectorAll('*')).map(el => el.textContent);
const hasCaptchaModal = modalTexts.some(text =>
text && (text.includes('还需一步') || text.includes('我是真实访问者') || text.includes('hCaptcha'))
);
return {
hasStripeFrame: !!stripeHCaptchaFrame,
hasHCaptchaFrame: !!hcaptchaFrame,
hasDiv: !!hcaptchaDiv,
hasModal: hasCaptchaModal,
stripeFrameUrl: stripeHCaptchaFrame ? stripeHCaptchaFrame.src.substring(0, 100) : null
};
});
if (hasHCaptcha) {
logger.info(this.siteName, ' → 检测到验证码,处理中...');
// 只要检测到任一验证码元素就退出循环
if (captchaCheck.hasStripeFrame || captchaCheck.hasHCaptchaFrame ||
captchaCheck.hasDiv || captchaCheck.hasModal) {
captchaDetected = true;
logger.info(this.siteName, ` → 检测到验证码(第${captchaCheckCount}次检查,${((Date.now() - checkStartTime) / 1000).toFixed(1)}秒)`);
if (captchaCheck.hasModal) {
logger.info(this.siteName, ` → 检测到验证码对话框`);
}
if (captchaCheck.stripeFrameUrl) {
logger.info(this.siteName, ` → Stripe hCaptcha URL: ${captchaCheck.stripeFrameUrl}...`);
}
} else {
// 每500ms检查一次
await new Promise(resolve => setTimeout(resolve, 500));
}
}
if (captchaDetected) {
logger.info(this.siteName, ' → 正在处理验证码...');
await this.handleHCaptcha();
await this.human.randomDelay(1000, 2000);
} else {
logger.info(this.siteName, ` → ✓ 无验证码(检查${captchaCheckCount}次,耗时${((Date.now() - checkStartTime) / 1000).toFixed(1)}秒)`);
}
} else {
logger.info(this.siteName, ` → 跳过验证码检查(重试 ${retryCount + 1}`);
}
// 开始轮询检测:同时等待成功或失败
@ -1100,6 +1289,9 @@ class WindsurfRegister {
if (cardRejected) {
logger.warn(this.siteName, ` → ⚠️ 银行卡被拒绝!(第 ${retryCount + 1}/${maxRetries} 次,检测了 ${checkCount} 次)`);
// 等待错误信息完全显示1-2秒
await this.human.randomDelay(1000, 2000);
// 生成新卡
logger.info(this.siteName, ' → 生成新的银行卡信息...');
const cardGen = new CardGenerator();
@ -1114,9 +1306,30 @@ class WindsurfRegister {
country: 'MO'
};
// 检查页面状态,确保表单仍然可用
try {
await this.page.waitForFunction(
() => {
const cardNumber = document.querySelector('#cardNumber');
const cardExpiry = document.querySelector('#cardExpiry');
const cardCvc = document.querySelector('#cardCvc');
return cardNumber && cardExpiry && cardCvc;
},
{ timeout: 5000 }
);
} catch (e) {
logger.warn(this.siteName, ` → 表单可能已刷新,等待恢复...`);
await this.human.randomDelay(2000, 3000);
}
// 模拟人工思考1-2秒
await this.human.randomDelay(1000, 2000);
// 重新填写卡信息
await this.fillCardForm(newCard, true);
logger.success(this.siteName, ' → ✓ 已更新银行卡信息');
// 填写后等待页面响应2-3秒
await this.human.randomDelay(2000, 3000);
// 递归重试
@ -1147,6 +1360,9 @@ class WindsurfRegister {
logger.warn(this.siteName, ` → ⚠️ 支付超时(${maxWait/1000}秒),共检测 ${checkCount}`);
logger.info(this.siteName, ` → 将生成新卡重试 (第 ${retryCount + 1}/${maxRetries} 次)`);
// 等待页面稳定1-2秒
await this.human.randomDelay(1000, 2000);
// 生成新卡
const cardGen = new CardGenerator();
const newCard = cardGen.generate('unionpay');
@ -1160,9 +1376,30 @@ class WindsurfRegister {
country: 'MO'
};
// 检查页面状态
try {
await this.page.waitForFunction(
() => {
const cardNumber = document.querySelector('#cardNumber');
const cardExpiry = document.querySelector('#cardExpiry');
const cardCvc = document.querySelector('#cardCvc');
return cardNumber && cardExpiry && cardCvc;
},
{ timeout: 5000 }
);
} catch (e) {
logger.warn(this.siteName, ` → 表单可能已刷新,等待恢复...`);
await this.human.randomDelay(2000, 3000);
}
// 模拟人工思考1-2秒
await this.human.randomDelay(1000, 2000);
// 重新填写卡信息
await this.fillCardForm(newCard, true);
logger.success(this.siteName, ' → ✓ 已更新银行卡信息');
// 填写后等待2-3秒
await this.human.randomDelay(2000, 3000);
// 递归重试

View File

@ -32,16 +32,22 @@ class CapSolverAPI {
try {
// 1. 创建任务
const createTaskResponse = await axios.post(`${this.apiUrl}/createTask`, {
const requestBody = {
clientKey: this.apiKey,
task: {
type: 'HCaptchaTaskProxyless',
websiteURL: pageUrl,
websiteKey: siteKey
}
});
};
logger.info('CapSolver', `请求 URL: ${pageUrl}`);
logger.info('CapSolver', `API Key: ${this.apiKey.substring(0, 10)}...`);
const createTaskResponse = await axios.post(`${this.apiUrl}/createTask`, requestBody);
if (createTaskResponse.data.errorId !== 0) {
logger.error('CapSolver', `API 返回错误: ${JSON.stringify(createTaskResponse.data)}`);
throw new Error(`创建任务失败: ${createTaskResponse.data.errorDescription}`);
}
@ -86,7 +92,25 @@ class CapSolverAPI {
throw new Error('CapSolver 识别超时2分钟');
} catch (error) {
// 详细的错误日志
if (error.response) {
// API 返回了错误响应
logger.error('CapSolver', `HTTP ${error.response.status}: ${error.response.statusText}`);
logger.error('CapSolver', `响应数据: ${JSON.stringify(error.response.data)}`);
if (error.response.status === 400) {
logger.error('CapSolver', '可能原因:');
logger.error('CapSolver', ' 1. API Key 无效或已过期');
logger.error('CapSolver', ' 2. 余额不足');
logger.error('CapSolver', ' 3. siteKey 或 URL 格式错误');
}
} else if (error.request) {
// 请求已发送但没有收到响应
logger.error('CapSolver', '无法连接到 CapSolver API');
} else {
// 其他错误
logger.error('CapSolver', `识别失败: ${error.message}`);
}
throw error;
}
}