diff --git a/src/tools/account-register/sites/windsurf.js b/src/tools/account-register/sites/windsurf.js index 5c50193..ff5dc0e 100644 --- a/src/tools/account-register/sites/windsurf.js +++ b/src/tools/account-register/sites/windsurf.js @@ -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, ' → 等待自动验证...'); - // 不要主动点击 checkbox!YesCaptcha 的 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; + } + } + } + + // 方式3: 任意 submit 按钮 + if (!button) { + button = document.querySelector('button[type="submit"]'); + } + + 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 submitButton.click(); - logger.success(this.siteName, ' → ✓ 已点击订阅按钮'); + // 等待一下,验证按钮状态变化 + await this.human.randomDelay(1000, 1500); - // 等待一下让 Stripe 开始处理 - await this.human.randomDelay(2000, 3000); + // 验证点击是否生效(按钮文字应该变为"正在处理"或按钮被禁用) + 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 完成`);