This commit is contained in:
dengqichen 2025-11-18 13:03:30 +08:00
parent 572b74afbd
commit a3518c7054

View File

@ -858,14 +858,35 @@ class WindsurfRegister {
* 彻底清空输入框通过DOM直接操作
*/
async clearInputField(selector) {
await this.page.evaluate((sel) => {
const field = document.querySelector(sel);
try {
// 方法1: 通过 DOM 清空
await this.page.evaluate((sel) => {
const field = document.querySelector(sel);
if (field) {
// 聚焦
field.focus();
// 全选
field.select();
// 清空
field.value = '';
// 触发所有相关事件
field.dispatchEvent(new Event('input', { bubbles: true }));
field.dispatchEvent(new Event('change', { bubbles: true }));
field.dispatchEvent(new Event('blur', { bubbles: true }));
field.blur();
}
}, selector);
// 方法2: 使用 Puppeteer API 进一步清理
const field = await this.page.$(selector);
if (field) {
field.value = '';
field.dispatchEvent(new Event('input', { bubbles: true }));
field.dispatchEvent(new Event('change', { bubbles: true }));
await field.click({ clickCount: 3 }); // 三击全选
await this.page.keyboard.press('Backspace');
await this.human.randomDelay(100, 200);
}
}, selector);
} catch (e) {
logger.warn(this.siteName, ` → 清空输入框失败 (${selector}): ${e.message}`);
}
}
/**
@ -1401,12 +1422,18 @@ class WindsurfRegister {
document.querySelector('[name="g-recaptcha-response"]');
const hasResponse = response && response.value && response.value.length > 20;
// 检查 iframe 中的图片挑战
// 检查 iframe 中的图片挑战(必须是可见的)
let hasImageChallenge = false;
const iframes = document.querySelectorAll('iframe[src*="hcaptcha"]');
for (const iframe of iframes) {
const rect = iframe.getBoundingClientRect();
if (rect.width > 300 && rect.height > 300) {
const style = window.getComputedStyle(iframe);
const isVisible = style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0' &&
rect.width > 300 && rect.height > 300;
if (isVisible) {
hasImageChallenge = true;
break;
}
@ -1423,14 +1450,142 @@ class WindsurfRegister {
if (status.hasImageChallenge && !hasImageChallenge) {
hasImageChallenge = true;
logger.warn(this.siteName, ' → ⚠️ 检测到图片挑战!');
logger.warn(this.siteName, ' → 请手动完成图片验证...');
// 尝试使用 2captcha 自动解决图片挑战
if (this.twoCaptchaSolver) {
try {
logger.info(this.siteName, ' → 尝试使用 2Captcha 自动解决图片挑战...');
// 1. 获取挑战信息
const challengeInfo = await this.page.evaluate(() => {
// 查找挑战 iframe
const iframes = document.querySelectorAll('iframe[src*="hcaptcha"]');
for (const iframe of iframes) {
const rect = iframe.getBoundingClientRect();
if (rect.width > 300 && rect.height > 300) {
// 尝试获取任务文本(可能在主页面或 iframe 外层)
let taskText = '';
const parentDiv = iframe.closest('div');
if (parentDiv) {
const textElements = parentDiv.querySelectorAll('h2, h3, div, span, label');
for (const el of textElements) {
const text = el.textContent?.trim();
if (text && (text.includes('点击') || text.includes('选择') || text.includes('Select'))) {
taskText = text;
break;
}
}
}
return {
found: true,
taskText: taskText,
iframeSrc: iframe.src,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
}
};
}
}
return { found: false };
});
if (challengeInfo.found) {
logger.info(this.siteName, ` → 挑战任务: ${challengeInfo.taskText || '未检测到任务文本'}`);
// 2. 截取挑战图片
const challengeFrame = this.page.frames().find(f => f.url().includes('hcaptcha') && f.url().includes('challenge'));
if (challengeFrame) {
// 等待图片加载
await this.human.randomDelay(2000, 3000);
// 截取挑战区域的图片
const screenshot = await this.page.screenshot({
clip: {
x: challengeInfo.rect.x,
y: challengeInfo.rect.y,
width: challengeInfo.rect.width,
height: challengeInfo.rect.height
},
encoding: 'base64'
});
logger.info(this.siteName, ' → 正在发送到 2Captcha 识别...');
// 3. 使用 2captcha coordinates API
const result = await this.twoCaptchaSolver.coordinates({
body: screenshot,
textinstructions: challengeInfo.taskText || '点击正确的图片'
});
logger.success(this.siteName, ` → ✓ 2Captcha 识别成功!耗时: ${result.cost}`);
// 4. 点击识别出的坐标
const coords = result.data.split(',').map(coord => {
const [key, value] = coord.split(':');
return { [key]: parseInt(value) };
}).reduce((acc, curr) => ({ ...acc, ...curr }), {});
if (coords.x && coords.y) {
// 转换为屏幕坐标
const clickX = challengeInfo.rect.x + coords.x;
const clickY = challengeInfo.rect.y + coords.y;
logger.info(this.siteName, ` → 点击坐标: (${clickX}, ${clickY})`);
await this.page.mouse.click(clickX, clickY);
logger.success(this.siteName, ' → ✓ 已点击识别结果');
// 等待并点击提交按钮(通常是"验证"或"检查"按钮)
await this.human.randomDelay(1000, 1500);
// 查找并点击提交按钮
const submitClicked = await challengeFrame.evaluate(() => {
const buttons = document.querySelectorAll('button, [role="button"]');
for (const btn of buttons) {
const text = btn.textContent?.trim();
if (text && (text.includes('验证') || text.includes('检查') || text.includes('Verify') || text.includes('Submit'))) {
btn.click();
return true;
}
}
return false;
});
if (submitClicked) {
logger.success(this.siteName, ' → ✓ 已提交验证');
}
// 等待验证结果
await this.human.randomDelay(2000, 3000);
}
} else {
logger.warn(this.siteName, ' → 未找到挑战 iframe');
logger.warn(this.siteName, ' → 请手动完成图片验证...');
}
} else {
logger.warn(this.siteName, ' → 未找到挑战信息');
logger.warn(this.siteName, ' → 请手动完成图片验证...');
}
} catch (error) {
logger.error(this.siteName, ` → 2Captcha 自动解决失败: ${error.message}`);
logger.warn(this.siteName, ' → 请手动完成图片验证...');
}
} else {
logger.warn(this.siteName, ' → 请手动完成图片验证...');
}
}
// 验证完成的条件token 已填充 且 没有图片挑战
if (status.verified && !status.hasImageChallenge) {
// 验证完成的条件token 已填充(不管 iframe 是否还存在)
if (status.verified) {
dialogClosed = true;
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
logger.success(this.siteName, ` → ✓ 验证完成(耗时${elapsed}秒)`);
if (status.hasImageChallenge) {
logger.info(this.siteName, ' → 图片挑战 iframe 仍存在,但 token 已填充,视为完成');
}
break;
}
@ -1448,7 +1603,9 @@ class WindsurfRegister {
}
if (!dialogClosed) {
logger.warn(this.siteName, ' → ⚠️ 验证超时,继续尝试支付...');
logger.error(this.siteName, ' → ✗ 验证超时120秒');
logger.warn(this.siteName, ' → 验证码可能还在页面上,需要手动完成或重新提交');
return false; // 返回 false不要继续
}
await this.human.randomDelay(1000, 2000);
@ -1506,11 +1663,17 @@ class WindsurfRegister {
const elements = document.querySelectorAll(selector);
for (const el of elements) {
const text = el.textContent || '';
// 检查卡被拒绝或卡号无效
if (text.includes('银行卡') && text.includes('拒绝') ||
text.includes('您的银行卡被拒绝了') ||
text.includes('card was declined') ||
text.includes('被拒绝') ||
text.includes('declined')) {
text.includes('declined') ||
text.includes('卡号无效') ||
text.includes('您的卡号无效') ||
text.includes('invalid card') ||
text.includes('card number is invalid') ||
text.includes('无效')) {
return true;
}
}
@ -1675,65 +1838,70 @@ class WindsurfRegister {
// 等待 Stripe 开始处理
await this.human.randomDelay(1000, 2000);
// 检查是否有验证码(只在第一次提交时检查)
if (retryCount === 0) {
// 等待可能的验证码弹窗/iframe 加载最多等5秒
let captchaDetected = false;
const checkStartTime = Date.now();
const maxCheckTime = 5000; // 最多检查5秒
let captchaCheckCount = 0;
// 检查是否有验证码(每次提交都检查)
// 等待可能的验证码弹窗/iframe 加载最多等5秒
let captchaDetected = false;
const checkStartTime = Date.now();
const maxCheckTime = 5000; // 最多检查5秒
let captchaCheckCount = 0;
while (Date.now() - checkStartTime < maxCheckTime && !captchaDetected) {
captchaCheckCount++;
logger.info(this.siteName, ` → 检查验证码状态(第 ${retryCount + 1} 次提交)...`);
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');
while (Date.now() - checkStartTime < maxCheckTime && !captchaDetected) {
captchaCheckCount++;
// 检查是否有"还需一步即可完成"的对话框
const modalTexts = Array.from(document.querySelectorAll('*')).map(el => el.textContent);
const hasCaptchaModal = modalTexts.some(text =>
text && (text.includes('还需一步') || text.includes('我是真实访问者') || text.includes('hCaptcha'))
);
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');
return {
hasStripeFrame: !!stripeHCaptchaFrame,
hasHCaptchaFrame: !!hcaptchaFrame,
hasDiv: !!hcaptchaDiv,
hasModal: hasCaptchaModal,
stripeFrameUrl: stripeHCaptchaFrame ? stripeHCaptchaFrame.src.substring(0, 100) : null
};
});
// 检查是否有"还需一步即可完成"的对话框
const modalTexts = Array.from(document.querySelectorAll('*')).map(el => el.textContent);
const hasCaptchaModal = modalTexts.some(text =>
text && (text.includes('还需一步') || text.includes('我是真实访问者') || text.includes('hCaptcha'))
);
// 只要检测到任一验证码元素就退出循环
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));
return {
hasStripeFrame: !!stripeHCaptchaFrame,
hasHCaptchaFrame: !!hcaptchaFrame,
hasDiv: !!hcaptchaDiv,
hasModal: hasCaptchaModal,
stripeFrameUrl: stripeHCaptchaFrame ? stripeHCaptchaFrame.src.substring(0, 100) : null
};
});
// 只要检测到任一验证码元素就退出循环
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}...`);
}
}
if (captchaDetected) {
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)}秒)`);
// 每500ms检查一次
await new Promise(resolve => setTimeout(resolve, 500));
}
}
if (captchaDetected) {
logger.info(this.siteName, ' → 检测到验证码,使用 YesCaptcha API 处理...');
// 使用 YesCaptcha API 处理
const captchaSolved = await this.handleHCaptcha();
if (!captchaSolved) {
logger.error(this.siteName, ' → ✗ 验证码自动处理失败!');
logger.warn(this.siteName, ' → 继续检测支付结果(可能用户已手动完成,或卡被拒绝需要重试)');
// ✅ 不要直接返回 false继续执行检测支付结果
} else {
logger.success(this.siteName, ' → ✓ 验证码处理成功');
}
await this.human.randomDelay(1000, 2000);
} else {
logger.info(this.siteName, ` → 跳过验证码检查(重试 ${retryCount + 1}`);
logger.info(this.siteName, `✓ 无验证码(检查${captchaCheckCount}次,耗时${((Date.now() - checkStartTime) / 1000).toFixed(1)}`);
}
// 开始轮询检测:同时等待成功或失败