This commit is contained in:
dengqichen 2025-11-17 23:25:41 +08:00
parent c7035b0784
commit e7e875a010

View File

@ -870,6 +870,7 @@ class WindsurfRegister {
/**
* 填写银行卡表单每次都彻底清空后重新填写
* @returns {boolean} 是否成功填写
*/
async fillCardForm(card, isRetry = false) {
if (!isRetry) {
@ -895,6 +896,12 @@ class WindsurfRegister {
logger.success(this.siteName, ' → ✓ 支付表单加载完成');
} catch (e) {
logger.warn(this.siteName, ` → 等待表单超时: ${e.message}`);
// 尝试截图调试
try {
const screenshot = `/tmp/stripe-form-timeout-${Date.now()}.png`;
await this.page.screenshot({ path: screenshot });
logger.info(this.siteName, ` → 调试截图: ${screenshot}`);
} catch (err) {}
}
}
}
@ -902,12 +909,25 @@ class WindsurfRegister {
// ===== 填写卡号 =====
logger.info(this.siteName, ` → 填写卡号: ${card.number}...`);
// 0. 先确认元素存在
try {
await this.page.waitForSelector('#cardNumber', { timeout: 10000 });
} catch (e) {
logger.error(this.siteName, ` → ✗ 卡号输入框未找到,可能支付表单未加载: ${e.message}`);
return false;
}
// 1. 彻底清空卡号
await this.clearInputField('#cardNumber');
await this.human.randomDelay(300, 500);
// 2. 点击并聚焦
await this.page.click('#cardNumber');
const cardNumberField = await this.page.$('#cardNumber');
if (!cardNumberField) {
logger.error(this.siteName, ' → ✗ 卡号输入框消失,页面可能已变化');
return false;
}
await cardNumberField.click();
await this.human.randomDelay(300, 500);
// 3. 再次确保清空(按多次 Backspace
@ -930,7 +950,12 @@ class WindsurfRegister {
await this.human.randomDelay(200, 300);
// 2. 点击并聚焦
await this.page.click('#cardExpiry');
const cardExpiryField = await this.page.$('#cardExpiry');
if (!cardExpiryField) {
logger.error(this.siteName, ' → ✗ 有效期输入框未找到');
return false;
}
await cardExpiryField.click();
await this.human.randomDelay(200, 300);
// 3. 再次确保清空
@ -952,7 +977,12 @@ class WindsurfRegister {
await this.human.randomDelay(200, 300);
// 2. 点击并聚焦
await this.page.click('#cardCvc');
const cardCvcField = await this.page.$('#cardCvc');
if (!cardCvcField) {
logger.error(this.siteName, ' → ✗ CVC输入框未找到');
return false;
}
await cardCvcField.click();
await this.human.randomDelay(200, 300);
// 3. 再次确保清空
@ -970,7 +1000,12 @@ class WindsurfRegister {
if (!isRetry) {
logger.info(this.siteName, ' → 填写持卡人姓名...');
await this.clearInputField('#billingName');
await this.page.click('#billingName');
const billingNameField = await this.page.$('#billingName');
if (!billingNameField) {
logger.error(this.siteName, ' → ✗ 持卡人姓名输入框未找到');
return false;
}
await billingNameField.click();
await this.human.randomDelay(300, 500);
const fullName = `${this.accountData.firstName} ${this.accountData.lastName}`;
await this.page.type('#billingName', fullName, { delay: 100 });
@ -991,6 +1026,7 @@ class WindsurfRegister {
}
logger.success(this.siteName, ' → ✓ 银行卡信息填写完成');
return true;
}
/**
@ -1153,13 +1189,67 @@ class WindsurfRegister {
} else if (injected.callbackExecuted) {
logger.info(this.siteName, ' → ✓ 已执行 hCaptcha 回调');
} else {
logger.warn(this.siteName, ' → ⚠️ 未找到回调函数,可能需要手动触发');
logger.info(this.siteName, ' → Token 已注入,准备点击 checkbox');
}
logger.info(this.siteName, ' → 等待自动验证...');
// 不要主动点击 checkboxYesCaptcha 的 token 应该自动通过
// 如果主动点击反而会触发 hCaptcha 的二次验证
await this.human.randomDelay(2000, 3000);
// 等待一下,让 token 生效
await this.human.randomDelay(1000, 2000);
// 点击对话框中的 checkbox必须
logger.info(this.siteName, ' → 查找并点击 checkbox...');
try {
const checkboxClicked = await this.page.evaluate(() => {
const dialog = document.querySelector('[role="dialog"]');
if (!dialog) return { success: false, reason: 'no-dialog' };
// 查找 checkbox 元素
const checkbox = dialog.querySelector('input[type="checkbox"]') ||
dialog.querySelector('[role="checkbox"]') ||
dialog.querySelector('#checkbox');
if (checkbox) {
const rect = checkbox.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
checkbox.click();
return {
success: true,
checked: checkbox.checked,
coords: { x: Math.round(rect.left + rect.width/2), y: Math.round(rect.top + rect.height/2) }
};
}
return { success: false, reason: 'not-visible' };
}
return { success: false, reason: 'not-found' };
});
if (checkboxClicked.success) {
logger.success(this.siteName, ` → ✓ 已点击 checkbox`);
} else {
logger.warn(this.siteName, ` → ⚠️ 未找到 checkbox (${checkboxClicked.reason}),尝试坐标点击...`);
// 备用方案:通过坐标点击对话框左侧
const coords = await this.page.evaluate(() => {
const dialog = document.querySelector('[role="dialog"]');
if (!dialog) return null;
const rect = dialog.getBoundingClientRect();
// checkbox 通常在对话框左侧,距离顶部约 1/3 处
return {
x: rect.left + 40,
y: rect.top + rect.height * 0.4
};
});
if (coords) {
await this.page.mouse.click(coords.x, coords.y);
logger.success(this.siteName, ` → ✓ 已点击坐标 (${coords.x}, ${coords.y})`);
}
}
} catch (e) {
logger.warn(this.siteName, ` → 点击失败: ${e.message}`);
}
await this.human.randomDelay(1000, 2000);
// 等待验证完成(如果回调成功,应该很快;如果降级到图片,需要手动)
logger.info(this.siteName, ' → 等待验证完成...');
@ -1325,18 +1415,125 @@ class WindsurfRegister {
logger.info(this.siteName, ` → 第 ${retryCount + 1} 次尝试提交...`);
}
// 点击订阅按钮
const submitButton = await this.page.$('button[type="submit"][data-testid="hosted-payment-submit-button"]');
if (!submitButton) {
logger.warn(this.siteName, ' → 未找到订阅按钮');
return false;
// 点击订阅按钮(增强版)
logger.info(this.siteName, ' → 查找订阅按钮...');
// 等待按钮可点击
await this.human.randomDelay(500, 1000);
const clicked = await this.page.evaluate(() => {
// 查找订阅按钮的多种方式
let button = null;
// 方式1: 精确选择器
button = document.querySelector('button[type="submit"][data-testid="hosted-payment-submit-button"]');
// 方式2: 通过文本内容查找
if (!button) {
const buttons = document.querySelectorAll('button[type="submit"]');
for (const btn of buttons) {
const text = (btn.innerText || btn.textContent || '').toLowerCase();
if (text.includes('订阅') || text.includes('subscribe') || text.includes('start')) {
button = btn;
break;
}
}
}
await submitButton.click();
logger.success(this.siteName, ' → ✓ 已点击订阅按钮');
// 方式3: 任意 submit 按钮
if (!button) {
button = document.querySelector('button[type="submit"]');
}
// 等待一下让 Stripe 开始处理
await this.human.randomDelay(2000, 3000);
if (!button) {
return { success: false, reason: 'not-found' };
}
// 检查按钮状态
const rect = button.getBoundingClientRect();
const isVisible = rect.width > 0 && rect.height > 0 &&
rect.top >= 0 && rect.top <= window.innerHeight;
const isDisabled = button.disabled || button.getAttribute('aria-disabled') === 'true';
const buttonText = button.innerText || button.textContent || '';
if (isDisabled) {
return { success: false, reason: 'disabled', text: buttonText };
}
if (!isVisible) {
// 滚动到按钮位置
button.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 等待滚动完成
return { success: false, reason: 'scrolling', text: buttonText, needScroll: true };
}
// 点击按钮
try {
button.click();
return {
success: true,
text: buttonText,
coords: { x: Math.round(rect.left + rect.width/2), y: Math.round(rect.top + rect.height/2) }
};
} catch (e) {
return { success: false, reason: 'click-failed', error: e.message };
}
});
if (!clicked.success) {
if (clicked.needScroll) {
logger.info(this.siteName, ` → 按钮不在可视区域,已滚动,重试点击...`);
await this.human.randomDelay(500, 1000);
// 重试一次
const retryClicked = await this.page.evaluate(() => {
const button = document.querySelector('button[type="submit"]');
if (button) {
button.click();
return { success: true, text: button.innerText };
}
return { success: false };
});
if (!retryClicked.success) {
logger.warn(this.siteName, ` → 订阅按钮点击失败: ${clicked.reason}`);
return false;
}
logger.success(this.siteName, ` → ✓ 已点击订阅按钮: "${retryClicked.text}"`);
} else {
logger.warn(this.siteName, ` → 订阅按钮点击失败: ${clicked.reason}`);
return false;
}
} else {
logger.success(this.siteName, ` → ✓ 已点击订阅按钮: "${clicked.text}"`);
}
// 等待一下,验证按钮状态变化
await this.human.randomDelay(1000, 1500);
// 验证点击是否生效(按钮文字应该变为"正在处理"或按钮被禁用)
const verified = await this.page.evaluate(() => {
const button = document.querySelector('button[type="submit"]');
if (!button) return { changed: false };
const text = button.innerText || button.textContent || '';
const isDisabled = button.disabled || button.getAttribute('aria-disabled') === 'true';
const isProcessing = text.includes('处理') || text.includes('Processing') ||
text.includes('Loading') || text.includes('Submitting');
return {
changed: isDisabled || isProcessing,
text: text,
disabled: isDisabled
};
});
if (verified.changed) {
logger.success(this.siteName, ` → ✓ 订阅按钮状态已变化: "${verified.text}"`);
} else {
logger.warn(this.siteName, ` → ⚠️ 按钮状态未变化,可能点击未生效,当前文字: "${verified.text}"`);
}
// 等待 Stripe 开始处理
await this.human.randomDelay(1000, 2000);
// 检查是否有验证码(只在第一次提交时检查)
if (retryCount === 0) {
@ -1555,12 +1752,50 @@ class WindsurfRegister {
country: 'MO'
};
// 填写卡表单
await this.fillCardForm(card);
// 填写卡表单(带重试)
let fillSuccess = false;
const maxFillRetries = 2;
for (let retry = 0; retry < maxFillRetries; retry++) {
if (retry > 0) {
logger.warn(this.siteName, ` → 第 ${retry + 1} 次尝试填写表单...`);
await this.human.randomDelay(2000, 3000);
// 刷新页面重试
try {
await this.page.reload({ waitUntil: 'networkidle0', timeout: 15000 });
await this.human.randomDelay(3000, 5000);
} catch (e) {
logger.warn(this.siteName, ` → 刷新页面失败: ${e.message}`);
}
}
fillSuccess = await this.fillCardForm(card);
if (fillSuccess) {
break;
}
// 截图调试
try {
const screenshot = `/tmp/windsurf-fill-failed-retry${retry}-${Date.now()}.png`;
await this.page.screenshot({ path: screenshot });
logger.info(this.siteName, ` → 调试截图: ${screenshot}`);
} catch (e) {}
}
if (!fillSuccess) {
logger.error(this.siteName, `✗ 填写支付表单失败(已重试 ${maxFillRetries} 次),跳过此账号`);
throw new Error('填写支付表单失败');
}
await this.human.randomDelay(2000, 3000);
// 提交支付(递归重试)
await this.submitPayment();
const submitSuccess = await this.submitPayment();
if (!submitSuccess) {
logger.error(this.siteName, '✗ 提交支付失败,跳过此账号');
throw new Error('提交支付失败');
}
this.currentStep = 6;
logger.success(this.siteName, `步骤 6 完成`);