auto-account-machine/src/tools/account-register/sites/windsurf.js
2025-11-17 18:15:04 +08:00

1442 lines
52 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Windsurf Register - Windsurf网站注册
* https://windsurf.com/account/register
*
* 注册流程:
* Step 1: 填写基本信息First Name, Last Name, Email
* Step 2: 设置密码
* Step 3: 邮箱验证
* Step 4: 完善个人信息
* ... 根据实际情况继续添加步骤
*/
const AccountDataGenerator = require('../generator');
const HumanBehavior = require('../utils/human-behavior');
const CloudflareHandler = require('../utils/cloudflare-handler');
const logger = require('../../../shared/logger');
const EmailVerificationService = require('../email-verification');
const { DEFAULT_CONFIG } = require('../config');
const CardGenerator = require('../../card-generator/generator');
const database = require('../../database');
const CapSolverAPI = require('../utils/capsolver-api');
const BrowserManager = require('../utils/browser-manager');
class WindsurfRegister {
constructor(options = {}) {
this.siteName = 'Windsurf';
this.siteUrl = 'https://windsurf.com/account/register';
this.dataGen = new AccountDataGenerator();
this.human = new HumanBehavior();
this.emailService = new EmailVerificationService();
this.capsolver = new CapSolverAPI();
// 浏览器管理器支持多profile并发
this.browserManager = new BrowserManager({
profileId: options.adspowerUserId || process.env.ADSPOWER_USER_ID,
siteName: this.siteName
});
this.browser = null;
this.page = null;
this.currentStep = 0;
this.accountData = null;
// 记录注册时间和额外信息
this.registrationTime = null;
this.quotaInfo = null;
this.billingInfo = null;
this.cardInfo = null;
// 定义所有步骤
this.steps = [
{ id: 1, name: '填写基本信息', method: 'step1_fillBasicInfo' },
{ id: 2, name: '设置密码', method: 'step2_setPassword' },
{ id: 3, name: '邮箱验证', method: 'step3_emailVerification' },
{ id: 4, name: '跳过问卷', method: 'step4_skipSurvey' },
{ id: 5, name: '选择计划', method: 'step5_selectPlan' },
{ id: 6, name: '填写支付信息', method: 'step6_fillPayment' },
{ id: 7, name: '获取订阅信息', method: 'step7_getSubscriptionInfo' },
{ id: 8, name: '保存到数据库', method: 'step8_saveToDatabase' },
{ id: 9, name: '清理并关闭浏览器', method: 'step9_clearAndCloseBrowser' },
];
}
/**
* 获取步骤总数
*/
getTotalSteps() {
return this.steps.length;
}
/**
* 获取当前步骤信息
*/
getCurrentStepInfo() {
if (this.currentStep === 0) {
return { id: 0, name: '未开始', total: this.getTotalSteps() };
}
const step = this.steps[this.currentStep - 1];
return {
...step,
current: this.currentStep,
total: this.getTotalSteps()
};
}
/**
* 通用方法:等待按钮激活并点击,然后等待页面内容变化
* @param {Object} options - 配置选项
* @param {string} options.buttonText - 按钮文本(用于识别,如'Continue'
* @param {Function} options.checkContentChanged - 检查页面内容是否已变化的函数返回Promise<boolean>
* @param {number} options.waitButtonTimeout - 等待按钮激活的超时时间毫秒默认30000
* @param {number} options.waitContentTimeout - 等待内容变化的超时时间毫秒默认15000
* @param {string} options.actionName - 操作名称(用于日志)
* @returns {Promise<boolean>} - 是否成功完成
*/
async clickButtonAndWaitForPageChange(options) {
const {
buttonText = 'Continue',
checkContentChanged,
waitButtonTimeout = 30000,
waitContentTimeout = 15000,
actionName = '点击按钮'
} = options;
try {
// 阶段1: 等待按钮变为可点击状态enabled
logger.info(this.siteName, ` → 等待"${buttonText}"按钮激活...`);
const buttonStartTime = Date.now();
let buttonEnabled = false;
while (Date.now() - buttonStartTime < waitButtonTimeout) {
buttonEnabled = await this.page.evaluate((btnText) => {
const buttons = Array.from(document.querySelectorAll('button'));
const targetButton = buttons.find(btn =>
btn.textContent.trim() === btnText && !btn.disabled
);
return !!targetButton;
}, buttonText);
if (buttonEnabled) {
const elapsed = ((Date.now() - buttonStartTime) / 1000).toFixed(1);
logger.success(this.siteName, ` → ✓ 按钮已激活 (耗时: ${elapsed}秒)`);
break;
}
// 每5秒输出一次进度
const elapsed = Date.now() - buttonStartTime;
if (elapsed > 0 && elapsed % 5000 === 0) {
logger.info(this.siteName, ` → 等待按钮激活中... 已用时 ${(elapsed/1000).toFixed(0)}`);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
if (!buttonEnabled) {
logger.error(this.siteName, ` → ⚠️ 等待${waitButtonTimeout/1000}秒后按钮仍未激活`);
return false;
}
// 阶段2: 点击按钮
logger.info(this.siteName, `${actionName}...`);
await this.human.humanClick(this.page, `button:not([disabled])`);
await this.human.randomDelay(1000, 2000);
// 阶段3: 等待页面内容变化
logger.info(this.siteName, ` → 等待页面内容变化...`);
const contentStartTime = Date.now();
let contentChanged = false;
while (Date.now() - contentStartTime < waitContentTimeout) {
contentChanged = await checkContentChanged();
if (contentChanged) {
const elapsed = ((Date.now() - contentStartTime) / 1000).toFixed(1);
logger.success(this.siteName, ` → ✓ 页面内容已变化 (耗时: ${elapsed}秒)`);
break;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
if (!contentChanged) {
logger.warn(this.siteName, ` → ⚠️ 等待${waitContentTimeout/1000}秒后页面内容未变化`);
return false;
}
return true;
} catch (error) {
logger.error(this.siteName, `${actionName}失败: ${error.message}`);
return false;
}
}
/**
* 生成账号数据(干运行模式)
*/
generateData(options = {}) {
logger.info(this.siteName, '生成账号数据...');
const account = this.dataGen.generateAccount({
name: options.name,
email: options.email,
username: options.username,
password: {
strategy: options.passwordStrategy || 'email',
...options.password
},
includePhone: false // Windsurf第一步不需要手机号
});
return account;
}
/**
* 初始化浏览器
*/
async initBrowser() {
const result = await this.browserManager.launch();
this.browser = result.browser;
this.page = result.page;
logger.info(this.siteName, '等待浏览器完全准备...');
await this.human.randomDelay(2000, 3000);
}
/**
* 关闭浏览器
*/
async closeBrowser() {
await this.browserManager.close();
}
/**
* 步骤1: 填写基本信息
*/
async step1_fillBasicInfo() {
logger.info(this.siteName, `[步骤 1/${this.getTotalSteps()}] 填写基本信息`);
const overallStartTime = Date.now();
const overallMaxWait = 180000; // 总共最多重试3分钟
let stepCompleted = false;
let retryCount = 0;
while (!stepCompleted && (Date.now() - overallStartTime < overallMaxWait)) {
try {
if (retryCount > 0) {
logger.info(this.siteName, ` → 第 ${retryCount + 1} 次尝试...`);
}
// 打开注册页面
logger.info(this.siteName, `打开注册页面: ${this.siteUrl}`);
await this.page.goto(this.siteUrl, {
waitUntil: 'networkidle2',
timeout: 30000
});
// 模拟阅读页面1-3秒
await this.human.readPage(1, 3);
// 填写First Name使用人类行为
logger.info(this.siteName, ' → 填写First Name...');
await this.page.waitForSelector('#firstName', { timeout: 10000 });
await this.human.humanType(this.page, '#firstName', this.accountData.firstName);
// 填写Last Name
logger.info(this.siteName, ' → 填写Last Name...');
await this.page.waitForSelector('#lastName', { timeout: 10000 });
await this.human.humanType(this.page, '#lastName', this.accountData.lastName);
// 填写Email
logger.info(this.siteName, ' → 填写Email...');
await this.page.waitForSelector('#email', { timeout: 10000 });
await this.human.humanType(this.page, '#email', this.accountData.email);
// 勾选同意条款(如果有)
try {
const checkbox = await this.page.$('input[type="checkbox"]');
if (checkbox) {
logger.info(this.siteName, ' → 勾选同意条款...');
await this.human.humanCheckbox(this.page, 'input[type="checkbox"]');
}
} catch (error) {
logger.warn(this.siteName, ' → 未找到同意条款checkbox跳过');
}
// 点击Continue按钮并等待跳转到密码页面
const success = await this.clickButtonAndWaitForPageChange({
buttonText: 'Continue',
checkContentChanged: async () => {
// 检查是否有密码输入框(表示已进入下一步)
return await this.page.evaluate(() => {
return !!document.querySelector('#password');
});
},
waitButtonTimeout: 30000,
waitContentTimeout: 15000,
actionName: '点击Continue进入密码设置页面'
});
if (!success) {
logger.warn(this.siteName, ' → ⚠️ 未能进入密码页面,将重新尝试');
retryCount++;
await this.human.randomDelay(2000, 3000);
continue;
}
// 成功完成
stepCompleted = true;
} catch (error) {
logger.warn(this.siteName, ` → 执行出错: ${error.message},将重新尝试`);
retryCount++;
await this.human.randomDelay(3000, 5000);
continue;
}
}
if (!stepCompleted) {
logger.error(this.siteName, ` → ✗ 步骤1失败${retryCount + 1}次尝试后仍未成功`);
throw new Error(`步骤1${retryCount + 1}次尝试后仍未能完成基本信息填写`);
}
// 额外等待确保页面稳定
await this.human.randomDelay(1000, 2000);
this.currentStep = 1;
logger.success(this.siteName, `步骤 1 完成 (共尝试 ${retryCount + 1} 次)`);
}
/**
* 步骤2: 设置密码
*/
async step2_setPassword() {
logger.info(this.siteName, `[步骤 2/${this.getTotalSteps()}] 设置密码`);
// 等待密码页面加载
await this.human.readPage(1, 2);
// 填写密码
logger.info(this.siteName, ' → 填写密码...');
await this.page.waitForSelector('#password', { timeout: 10000 });
// 先清空密码框
await this.page.evaluate(() => {
const elem = document.querySelector('#password');
if (elem) elem.value = '';
});
await this.human.humanType(this.page, '#password', this.accountData.password);
// 填写确认密码
logger.info(this.siteName, ' → 填写确认密码...');
await this.page.waitForSelector('#passwordConfirmation', { timeout: 10000 });
// 先清空确认密码框(防止有残留)
await this.page.evaluate(() => {
const elem = document.querySelector('#passwordConfirmation');
if (elem) elem.value = '';
});
await this.human.humanType(this.page, '#passwordConfirmation', this.accountData.password);
// 等待密码验证
logger.info(this.siteName, ' → 等待密码验证...');
await this.human.randomDelay(1000, 2000);
// 查找并点击Continue按钮此时会进入 Cloudflare Turnstile 验证页面)
logger.info(this.siteName, ' → 点击Continue按钮...');
try {
// 等待按钮可点击
await this.page.waitForFunction(
() => {
const button = document.querySelector('button');
if (!button) return false;
const text = button.textContent.trim();
return text === 'Continue' && !button.disabled;
},
{ timeout: 20000 }
);
logger.info(this.siteName, ' → 按钮已激活,点击进入验证页面...');
// 点击按钮
await Promise.all([
this.page.waitForNavigation({ waitUntil: 'load', timeout: 10000 }).catch(() => {}),
this.human.humanClick(this.page, 'button:not([disabled])')
]);
logger.success(this.siteName, ' → 已进入 Cloudflare Turnstile 验证页面');
} catch (error) {
logger.warn(this.siteName, ' → 按钮等待超时尝试按Enter键');
await this.page.keyboard.press('Enter');
}
// 等待页面稳定
await this.human.randomDelay(1000, 2000);
// 等待 Cloudflare Turnstile 验证完成(手动或其他方式)
logger.info(this.siteName, ' → 等待 Cloudflare Turnstile 验证...');
logger.info(this.siteName, ' → 请手动完成验证或等待自动处理...');
const startTime = Date.now();
const maxWait = 120000; // 最多等待120秒
// 轮询检查按钮是否激活
while (Date.now() - startTime < maxWait) {
const buttonEnabled = await this.page.evaluate(() => {
const button = document.querySelector('button');
return button && !button.disabled && button.textContent.trim() === 'Continue';
});
if (buttonEnabled) {
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
logger.success(this.siteName, ` → ✓ Turnstile 验证完成!耗时: ${duration}`);
break;
}
// 每10秒输出一次进度
const elapsed = Date.now() - startTime;
if (elapsed > 0 && elapsed % 10000 === 0) {
logger.info(this.siteName, ` → 等待验证中... 已用时 ${elapsed/1000}`);
}
await new Promise(resolve => setTimeout(resolve, 500));
}
if (Date.now() - startTime >= maxWait) {
logger.error(this.siteName, ' → ⚠️ 验证超时120秒');
throw new Error('Turnstile 验证超时');
}
// 点击 Continue 进入邮箱验证
logger.info(this.siteName, ' → 点击Continue进入邮箱验证...');
await Promise.all([
this.page.waitForNavigation({ waitUntil: 'load', timeout: 10000 }).catch(() => {}),
this.human.humanClick(this.page, 'button:not([disabled])')
]);
// 额外等待确保页面稳定
await this.human.randomDelay(1000, 2000);
this.currentStep = 2;
logger.success(this.siteName, `步骤 2 完成`);
}
/**
* 步骤3: 邮箱验证
*/
async step3_emailVerification() {
logger.info(this.siteName, `[步骤 3/${this.getTotalSteps()}] 邮箱验证`);
// Cloudflare Turnstile 验证已在步骤2中完成
logger.info(this.siteName, ' → Turnstile 验证已通过,开始邮箱验证...');
try {
// 等待验证码页面加载
await this.human.readPage(1, 2);
// 延迟2秒后再获取验证码让邮件有足够时间到达
logger.info(this.siteName, ' → 延迟2秒等待邮件到达...');
await new Promise(resolve => setTimeout(resolve, 2000));
// 获取验证码(从邮箱)
logger.info(this.siteName, ' → 正在从邮箱获取验证码...');
logger.info(this.siteName, ` → 接收邮箱: ${this.accountData.email}`);
logger.info(this.siteName, ' → 注意:如果长时间无响应,请检查:');
logger.info(this.siteName, ' 1. 邮件是否已发送到邮箱');
logger.info(this.siteName, ' 2. QQ邮箱IMAP配置是否正确');
logger.info(this.siteName, ' 3. 邮件是否被标记为垃圾邮件');
const code = await this.emailService.getVerificationCode(
'windsurf',
this.accountData.email,
120 // 增加到120秒超时
);
logger.success(this.siteName, ` → ✓ 验证码: ${code}`);
// 等待验证码输入框加载
await this.human.randomDelay(2000, 3000);
// Windsurf使用6个独立的输入框需要逐个填写
logger.info(this.siteName, ' → 查找验证码输入框...');
// 等待输入框出现
await this.page.waitForSelector('input[type="text"]', { timeout: 10000 });
// 获取所有文本输入框
const inputs = await this.page.$$('input[type="text"]');
logger.info(this.siteName, ` → 找到 ${inputs.length} 个输入框`);
if (inputs.length >= 6 && code.length === 6) {
// 逐个填写每一位验证码
logger.info(this.siteName, ' → 填写6位验证码...');
for (let i = 0; i < 6; i++) {
const char = code[i].toUpperCase(); // 确保大写
// 点击输入框获取焦点
await inputs[i].click();
await this.human.randomDelay(100, 200);
// 输入字符
await inputs[i].type(char);
await this.human.randomDelay(300, 500);
logger.info(this.siteName, ` → 已输入第 ${i + 1} 位: ${char}`);
}
logger.success(this.siteName, ' → 验证码已填写完成');
// 输入完6位验证码后页面会自动提交
logger.info(this.siteName, ' → 等待邮箱验证完成并跳转到问卷页面...');
logger.info(this.siteName, ' → 将持续等待直到跳转成功(无时间限制)...');
// 无限等待页面跳转到 /account/onboarding?page=source
const startTime = Date.now();
let registrationComplete = false;
let hasClickedButton = false;
while (!registrationComplete) {
const currentUrl = this.page.url();
// 检查1: 页面是否已经跳转成功(自动跳转成功)
if (currentUrl.includes('/account/onboarding') && currentUrl.includes('page=source')) {
registrationComplete = true;
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
logger.success(this.siteName, ` → ✓ 邮箱验证成功!已跳转到问卷页面 (耗时: ${totalTime}秒)`);
logger.info(this.siteName, ` → 当前页面: ${currentUrl}`);
break;
}
// 检查2: 页面按钮是否变为激活状态(自动跳转失败,需要手动点击)
if (!hasClickedButton) {
const buttonEnabled = await this.page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button'));
const continueButton = buttons.find(btn =>
btn.textContent.trim() === 'Continue' && !btn.disabled
);
return !!continueButton;
});
if (buttonEnabled) {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
logger.warn(this.siteName, ` → ⚠️ 检测到按钮重新激活 (${elapsed}秒后)`);
logger.info(this.siteName, ' → 自动跳转可能失败,尝试手动点击按钮...');
try {
// 点击 Continue 按钮
await this.human.humanClick(this.page, 'button:not([disabled])');
hasClickedButton = true;
logger.success(this.siteName, ' → ✓ 已点击按钮,继续等待跳转...');
await this.human.randomDelay(1000, 2000);
} catch (e) {
logger.warn(this.siteName, ` → 点击按钮失败: ${e.message}`);
}
}
}
// 每5秒输出一次进度
const elapsed = Date.now() - startTime;
if (elapsed > 0 && elapsed % 5000 === 0) {
logger.info(this.siteName, ` → 等待中... 已用时 ${(elapsed/1000).toFixed(0)}`);
}
await new Promise(resolve => setTimeout(resolve, 500));
}
// 额外等待页面稳定
await this.human.randomDelay(2000, 3000);
this.currentStep = 3;
logger.success(this.siteName, `步骤 3 完成 - 账号创建成功`);
} else {
logger.error(this.siteName, ' → 未找到验证码输入框!');
logger.warn(this.siteName, ' → 请手动输入验证码: ' + code);
// 等待用户手动输入
await this.human.randomDelay(30000, 30000);
this.currentStep = 3;
}
} catch (error) {
logger.error(this.siteName, `邮箱验证失败: ${error.message}`);
throw error;
}
}
/**
* 步骤4: 跳过问卷调查
*/
async step4_skipSurvey() {
logger.info(this.siteName, `[步骤 4/${this.getTotalSteps()}] 跳过问卷`);
const overallStartTime = Date.now();
const overallMaxWait = 120000; // 总共最多重试2分钟
let stepCompleted = false;
let retryCount = 0;
while (!stepCompleted && (Date.now() - overallStartTime < overallMaxWait)) {
try {
if (retryCount > 0) {
logger.info(this.siteName, ` → 第 ${retryCount + 1} 次尝试...`);
}
// 等待页面加载
await this.human.readPage(2, 3);
logger.info(this.siteName, ' → 查找"Skip this step"按钮...');
const startTime = Date.now();
const maxWait = 30000; // 单次查找最多等待30秒
let skipButton = null;
let buttonFound = false;
// 轮询查找按钮
while (!buttonFound && (Date.now() - startTime < maxWait)) {
try {
// 查找所有按钮或链接
const buttons = await this.page.$$('button, a');
for (const button of buttons) {
const text = await this.page.evaluate(el => el.textContent?.trim(), button);
// 匹配 "Skip this step" 或 "skip"
if (text && (text.toLowerCase().includes('skip this step') || text.toLowerCase() === 'skip')) {
skipButton = button;
buttonFound = true;
logger.success(this.siteName, ` → ✓ 找到按钮: "${text}" (耗时: ${((Date.now() - startTime) / 1000).toFixed(1)}秒)`);
break;
}
}
if (buttonFound) {
break;
}
// 每5秒输出一次进度
const elapsed = Date.now() - startTime;
if (elapsed > 0 && elapsed % 5000 === 0) {
logger.info(this.siteName, ` → 等待按钮出现... 已用时 ${elapsed/1000}`);
}
// 等待2秒后继续查找
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
logger.warn(this.siteName, ` → 查找按钮时出错: ${error.message},继续尝试...`);
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
if (skipButton && buttonFound) {
logger.info(this.siteName, ' → 点击"Skip this step"按钮...');
// 直接点击找到的按钮元素
await skipButton.click();
logger.success(this.siteName, ' → ✓ 已点击按钮');
// 等待页面跳转到 /account/upgrade-prompt
logger.info(this.siteName, ' → 等待跳转到 /account/upgrade-prompt 页面...');
const jumpStartTime = Date.now();
const jumpMaxWait = 15000; // 最多等待15秒
let jumpSuccess = false;
while (Date.now() - jumpStartTime < jumpMaxWait) {
const newUrl = this.page.url();
// 必须跳转到 upgrade-prompt 页面
if (newUrl.includes('/account/upgrade-prompt')) {
jumpSuccess = true;
stepCompleted = true;
logger.success(this.siteName, ` → ✓ 成功跳转到计划选择页面`);
logger.info(this.siteName, ` → 当前页面: ${newUrl}`);
break;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
if (!jumpSuccess) {
const finalUrl = this.page.url();
logger.warn(this.siteName, ` → ⚠️ 未跳转到升级页面,将重新尝试`);
logger.warn(this.siteName, ` → 当前页面: ${finalUrl}`);
retryCount++;
await this.human.randomDelay(2000, 3000);
continue; // 重新开始循环
}
} else {
// 未找到按钮,重试
logger.warn(this.siteName, ` → ⚠️ 未找到Skip按钮将重新尝试`);
retryCount++;
await this.human.randomDelay(2000, 3000);
continue;
}
} catch (error) {
logger.warn(this.siteName, ` → 执行出错: ${error.message},将重新尝试`);
retryCount++;
await this.human.randomDelay(2000, 3000);
continue;
}
}
if (!stepCompleted) {
const currentUrl = this.page.url();
logger.error(this.siteName, ` → ✗ 步骤4失败${retryCount + 1}次尝试后仍未成功`);
logger.error(this.siteName, ` → 当前页面: ${currentUrl}`);
throw new Error(`步骤4${retryCount + 1}次尝试后仍未成功跳转到 /account/upgrade-prompt 页面`);
}
// 额外等待页面稳定
await this.human.randomDelay(2000, 3000);
this.currentStep = 4;
logger.success(this.siteName, `步骤 4 完成 (共尝试 ${retryCount + 1} 次)`);
}
/**
* 步骤5: 选择计划
*/
async step5_selectPlan() {
logger.info(this.siteName, `[步骤 5/${this.getTotalSteps()}] 选择计划`);
const overallStartTime = Date.now();
const overallMaxWait = 120000; // 总共最多重试2分钟
let stepCompleted = false;
let retryCount = 0;
while (!stepCompleted && (Date.now() - overallStartTime < overallMaxWait)) {
try {
if (retryCount > 0) {
logger.info(this.siteName, ` → 第 ${retryCount + 1} 次尝试...`);
}
// 等待页面加载
await this.human.readPage(2, 3);
logger.info(this.siteName, ' → 查找计划选择按钮...');
const startTime = Date.now();
const maxWait = 30000; // 单次查找最多等待30秒
let selectButton = null;
let buttonFound = false;
// 轮询查找按钮
while (!buttonFound && (Date.now() - startTime < maxWait)) {
try {
const buttons = await this.page.$$('button, a');
for (const button of buttons) {
const text = await this.page.evaluate(el => el.textContent?.trim(), button);
// 匹配多种可能的按钮文本
if (text && (
text.toLowerCase().includes('select plan') ||
text.toLowerCase().includes('continue') ||
text.toLowerCase().includes('get started') ||
text.toLowerCase() === 'select'
)) {
selectButton = button;
buttonFound = true;
logger.success(this.siteName, ` → ✓ 找到按钮: "${text}" (耗时: ${((Date.now() - startTime) / 1000).toFixed(1)}秒)`);
break;
}
}
if (buttonFound) {
break;
}
// 每5秒输出一次进度
const elapsed = Date.now() - startTime;
if (elapsed > 0 && elapsed % 5000 === 0) {
logger.info(this.siteName, ` → 等待按钮出现... 已用时 ${elapsed/1000}`);
}
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
logger.warn(this.siteName, ` → 查找按钮时出错: ${error.message},继续尝试...`);
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
if (selectButton && buttonFound) {
logger.info(this.siteName, ' → 点击计划选择按钮...');
// 直接点击找到的按钮元素
await selectButton.click();
logger.success(this.siteName, ' → ✓ 已点击按钮');
// 等待页面跳转到 Stripe checkout
logger.info(this.siteName, ' → 等待跳转到 Stripe 支付页面...');
const jumpStartTime = Date.now();
const jumpMaxWait = 20000; // 最多等待20秒
let jumpSuccess = false;
while (Date.now() - jumpStartTime < jumpMaxWait) {
const newUrl = this.page.url();
// 必须跳转到 Stripe checkout
if (newUrl.includes('checkout.stripe.com') || newUrl.includes('stripe.com')) {
jumpSuccess = true;
stepCompleted = true;
logger.success(this.siteName, ` → ✓ 成功跳转到 Stripe 支付页面`);
logger.info(this.siteName, ` → 当前页面: ${newUrl}`);
break;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
if (!jumpSuccess) {
const finalUrl = this.page.url();
logger.warn(this.siteName, ` → ⚠️ 未跳转到支付页面,将重新尝试`);
logger.warn(this.siteName, ` → 当前页面: ${finalUrl}`);
retryCount++;
await this.human.randomDelay(2000, 3000);
continue;
}
} else {
// 未找到按钮,重试
logger.warn(this.siteName, ` → ⚠️ 未找到计划选择按钮,将重新尝试`);
retryCount++;
await this.human.randomDelay(2000, 3000);
continue;
}
} catch (error) {
logger.warn(this.siteName, ` → 执行出错: ${error.message},将重新尝试`);
retryCount++;
await this.human.randomDelay(2000, 3000);
continue;
}
}
if (!stepCompleted) {
const currentUrl = this.page.url();
logger.error(this.siteName, ` → ✗ 步骤5失败${retryCount + 1}次尝试后仍未成功`);
logger.error(this.siteName, ` → 当前页面: ${currentUrl}`);
throw new Error(`步骤5${retryCount + 1}次尝试后仍未成功跳转到 Stripe 支付页面`);
}
// 额外等待页面稳定
await this.human.randomDelay(2000, 3000);
this.currentStep = 5;
logger.success(this.siteName, `步骤 5 完成 (共尝试 ${retryCount + 1} 次)`);
}
/**
* 填写银行卡表单
*/
async fillCardForm(card, isRetry = false) {
if (!isRetry) {
// 首次填写:选择支付方式
logger.info(this.siteName, ' → 选择银行卡支付方式...');
const cardRadio = await this.page.$('input[type="radio"][value="card"]');
if (cardRadio) {
await cardRadio.click();
logger.success(this.siteName, ' → ✓ 已点击银行卡选项');
await this.human.randomDelay(3000, 5000);
try {
await this.page.waitForFunction(
() => {
const cardNumber = document.querySelector('#cardNumber');
const cardExpiry = document.querySelector('#cardExpiry');
const cardCvc = document.querySelector('#cardCvc');
const billingName = document.querySelector('#billingName');
return cardNumber && cardExpiry && cardCvc && billingName;
},
{ timeout: 30000 }
);
logger.success(this.siteName, ' → ✓ 支付表单加载完成');
} catch (e) {
logger.warn(this.siteName, ` → 等待表单超时: ${e.message}`);
}
}
}
// 填写卡号
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, ' → 填写有效期...');
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');
await this.human.randomDelay(300, 500);
const expiry = `${card.month}${card.year}`;
await cardExpiryField.type(expiry, { delay: 250 });
// 填写CVC
logger.info(this.siteName, ' → 填写CVC...');
const cardCvcField = await this.page.$('#cardCvc');
await cardCvcField.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');
await this.human.randomDelay(300, 500);
await cardCvcField.type(card.cvv, { delay: 250 });
if (!isRetry) {
// 首次填写:填写持卡人姓名和地址
logger.info(this.siteName, ' → 填写持卡人姓名...');
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 });
logger.info(this.siteName, ' → 选择地址:中国澳门特别行政区...');
await this.page.select('#billingCountry', 'MO');
await this.human.randomDelay(1000, 2000);
const addressFields = await this.page.$$('input[placeholder*="地址"]');
if (addressFields.length > 0) {
logger.info(this.siteName, ' → 填写地址信息...');
await addressFields[0].type('Macau', { delay: 100 });
if (addressFields[1]) {
await this.human.randomDelay(300, 500);
await addressFields[1].type('Macao', { delay: 100 });
}
}
}
}
/**
* 处理 hCaptcha 验证码
*/
async handleHCaptcha() {
const hasHCaptcha = await this.page.evaluate(() => {
const hcaptchaFrame = document.querySelector('iframe[src*="hcaptcha.com"]');
const hcaptchaCheckbox = document.querySelector('.h-captcha');
return !!(hcaptchaFrame || hcaptchaCheckbox);
});
if (!hasHCaptcha) return true;
logger.warn(this.siteName, ' → 检测到 hCaptcha 验证码');
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;
});
if (siteKey) {
const currentUrl = this.page.url();
const token = await this.capsolver.solveHCaptcha(siteKey, currentUrl);
await this.page.evaluate((token) => {
const textarea = document.querySelector('[name="h-captcha-response"]');
if (textarea) textarea.value = token;
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}`);
}
}
// 手动等待
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;
});
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}秒)`);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
return false;
}
/**
* 检查银行卡是否被拒绝
*/
async checkCardRejected() {
return await this.page.evaluate(() => {
const errorContainers = [
'.FieldError-container',
'[class*="Error"]',
'[class*="error"]',
'.error-message'
];
for (const selector of errorContainers) {
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')) {
return true;
}
}
}
return false;
});
}
/**
* 等待支付成功
*/
async waitForPaymentSuccess() {
const paymentStartTime = Date.now();
logger.info(this.siteName, ' → 等待支付处理...');
while (true) {
const currentUrl = this.page.url();
if (!currentUrl.includes('stripe.com') && !currentUrl.includes('checkout.stripe.com')) {
const totalTime = ((Date.now() - paymentStartTime) / 1000).toFixed(1);
this.registrationTime = new Date();
logger.success(this.siteName, ` → ✓ 支付成功!(耗时: ${totalTime}秒)`);
logger.info(this.siteName, ` → 当前页面: ${currentUrl}`);
return true;
}
const elapsed = Date.now() - paymentStartTime;
if (elapsed > 0 && elapsed % 5000 === 0) {
logger.info(this.siteName, ` → 支付处理中... (${(elapsed/1000).toFixed(0)}秒)`);
}
await new Promise(resolve => setTimeout(resolve, 500));
}
}
/**
* 提交支付(递归重试)
*/
async submitPayment(retryCount = 0, maxRetries = 5) {
if (retryCount >= maxRetries) {
throw new Error(`银行卡被拒绝,已重试 ${maxRetries}`);
}
if (retryCount > 0) {
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;
}
await submitButton.click();
logger.success(this.siteName, ' → ✓ 已点击订阅按钮');
await this.human.randomDelay(3000, 5000);
// 处理验证码
await this.handleHCaptcha();
await this.human.randomDelay(2000, 3000);
// 检查卡是否被拒绝
const cardRejected = await this.checkCardRejected();
if (cardRejected) {
logger.warn(this.siteName, ` → ⚠️ 银行卡被拒绝!(第 ${retryCount + 1}/${maxRetries} 次)`);
// 生成新卡
logger.info(this.siteName, ' → 生成新的银行卡信息...');
const cardGen = new CardGenerator();
const newCard = cardGen.generate('unionpay');
logger.info(this.siteName, ` → 新卡号: ${newCard.number}`);
this.cardInfo = {
number: newCard.number,
month: newCard.month,
year: newCard.year,
cvv: newCard.cvv,
country: 'MO'
};
// 重新填写卡信息
await this.fillCardForm(newCard, true);
logger.success(this.siteName, ' → ✓ 已更新银行卡信息');
await this.human.randomDelay(2000, 3000);
// 递归重试
return await this.submitPayment(retryCount + 1, maxRetries);
}
// 等待支付成功
await this.waitForPaymentSuccess();
return true;
}
/**
* 步骤6: 填写支付信息
*/
async step6_fillPayment() {
logger.info(this.siteName, `[步骤 6/${this.getTotalSteps()}] 填写支付信息`);
try {
await this.human.readPage(3, 5);
// 生成银行卡
logger.info(this.siteName, ' → 生成银联卡信息...');
const cardGen = new CardGenerator();
const card = cardGen.generate('unionpay');
logger.info(this.siteName, ` → 卡号: ${card.number}`);
logger.info(this.siteName, ` → 有效期: ${card.month}/${card.year}`);
logger.info(this.siteName, ` → CVV: ${card.cvv}`);
this.cardInfo = {
number: card.number,
month: card.month,
year: card.year,
cvv: card.cvv,
country: 'MO'
};
// 填写卡表单
await this.fillCardForm(card);
await this.human.randomDelay(2000, 3000);
// 提交支付(递归重试)
await this.submitPayment();
this.currentStep = 6;
logger.success(this.siteName, `步骤 6 完成`);
} catch (error) {
logger.error(this.siteName, `填写支付信息失败: ${error.message}`);
throw error;
}
}
/**
* 步骤7: 获取订阅信息
*/
async step7_getSubscriptionInfo() {
logger.info(this.siteName, `[步骤 7/${this.getTotalSteps()}] 获取订阅信息`);
try {
// 0. 检查并关闭可能存在的弹窗("要打开 Windsurf 吗?"
logger.info(this.siteName, ' → 检查是否有弹窗需要关闭...');
try {
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待弹窗出现
// 方法1: 尝试按ESC键关闭浏览器原生对话框
logger.info(this.siteName, ' → 尝试按ESC键关闭原生对话框...');
await this.page.keyboard.press('Escape');
await this.human.randomDelay(500, 1000);
// 方法2: 尝试查找并点击网页内的按钮如果是HTML弹窗
const closeDialog = await this.page.evaluate(() => {
// 查找包含"取消"或"打开Windsurf"的按钮
const buttons = Array.from(document.querySelectorAll('button'));
const cancelBtn = buttons.find(btn =>
btn.textContent.includes('取消') ||
btn.textContent.includes('打开Windsurf') ||
btn.textContent.includes('Cancel')
);
if (cancelBtn) {
// 优先点击"取消"按钮
const actualCancelBtn = buttons.find(btn =>
btn.textContent.includes('取消') ||
btn.textContent.includes('Cancel')
);
if (actualCancelBtn) {
actualCancelBtn.click();
return '取消';
} else {
cancelBtn.click();
return '打开Windsurf';
}
}
return null;
});
if (closeDialog) {
logger.success(this.siteName, ` → ✓ 已关闭HTML弹窗点击了"${closeDialog}"`);
await this.human.randomDelay(1000, 2000);
} else {
logger.success(this.siteName, ' → ✓ 已尝试关闭原生对话框ESC键');
}
} catch (e) {
logger.info(this.siteName, ` → 关闭弹窗时出错: ${e.message}`);
}
// 1. 跳转到订阅使用页面
logger.info(this.siteName, ' → 跳转到订阅使用页面...');
await this.page.goto('https://windsurf.com/subscription/usage', {
waitUntil: 'networkidle2',
timeout: 30000
});
// 等待页面加载
await this.human.randomDelay(3000, 5000);
// 2. 获取配额信息
logger.info(this.siteName, ' → 获取配额信息...');
const quotaInfo = await this.page.evaluate(() => {
// 查找包含配额的 p 标签
const quotaElement = document.querySelector('p.caption1.font-medium.text-sk-black\\/80');
if (!quotaElement) return null;
const spans = quotaElement.querySelectorAll('span.caption3 span');
if (spans.length >= 2) {
const used = spans[0].textContent.trim();
const total = spans[1].textContent.trim().replace('/', '').trim();
return { used, total };
}
return null;
});
if (quotaInfo) {
logger.success(this.siteName, ` → ✓ 配额: ${quotaInfo.used} / ${quotaInfo.total} used`);
} else {
logger.warn(this.siteName, ' → ⚠️ 未找到配额信息');
}
// 3. 获取下次账单日期信息
logger.info(this.siteName, ' → 获取账单周期信息...');
const billingInfo = await this.page.evaluate(() => {
// 查找包含 "Next billing cycle" 的 p 标签
const billingElement = Array.from(document.querySelectorAll('p.caption1'))
.find(p => p.textContent.includes('Next billing cycle'));
if (!billingElement) return null;
// 提取天数
const daysMatch = billingElement.textContent.match(/(\d+)\s+days?/);
const dateMatch = billingElement.textContent.match(/on\s+([A-Za-z]+\s+\d+,\s+\d{4})/);
return {
days: daysMatch ? daysMatch[1] : null,
date: dateMatch ? dateMatch[1] : null,
fullText: billingElement.textContent.trim()
};
});
if (billingInfo && billingInfo.days) {
logger.success(this.siteName, ` → ✓ 下次账单: ${billingInfo.days} 天后 (${billingInfo.date})`);
} else {
logger.warn(this.siteName, ' → ⚠️ 未找到账单周期信息');
}
// 4. 简要打印关键信息
logger.success(this.siteName, `✓ 配额: ${quotaInfo ? `${quotaInfo.used}/${quotaInfo.total}` : 'N/A'} | 下次账单: ${billingInfo?.days || 'N/A'}天后`);
// 保存订阅信息供后续使用
this.quotaInfo = quotaInfo;
this.billingInfo = billingInfo;
this.currentStep = 7;
logger.success(this.siteName, `步骤 7 完成`);
} catch (error) {
logger.error(this.siteName, `获取订阅信息失败: ${error.message}`);
throw error;
}
}
/**
* 步骤8: 保存到数据库
*/
async step8_saveToDatabase() {
logger.info(this.siteName, `[步骤 8/${this.getTotalSteps()}] 保存到数据库`);
try {
// 初始化数据库连接
logger.info(this.siteName, ' → 连接数据库...');
await database.initialize();
// 准备账号数据
const accountData = {
email: this.accountData.email,
password: this.accountData.password,
firstName: this.accountData.firstName,
lastName: this.accountData.lastName,
registrationTime: this.registrationTime,
quotaUsed: this.quotaInfo ? parseFloat(this.quotaInfo.used) : 0,
quotaTotal: this.quotaInfo ? parseFloat(this.quotaInfo.total) : 0,
billingDays: this.billingInfo ? parseInt(this.billingInfo.days) : null,
billingDate: this.billingInfo ? this.billingInfo.date : null,
paymentCardNumber: this.cardInfo ? this.cardInfo.number : null,
paymentCountry: this.cardInfo ? this.cardInfo.country : 'MO',
status: 'active'
};
// 保存到数据库
logger.info(this.siteName, ' → 保存账号信息...');
const accountRepo = database.getRepository('account');
const accountId = await accountRepo.create(accountData);
logger.success(this.siteName, ` → ✓ 账号信息已保存到数据库 (ID: ${accountId})`);
logger.info(this.siteName, ` → 邮箱: ${accountData.email}`);
logger.info(this.siteName, ` → 配额: ${accountData.quotaUsed} / ${accountData.quotaTotal}`);
logger.info(this.siteName, ` → 卡号: ${accountData.paymentCardNumber}`);
this.currentStep = 8;
logger.success(this.siteName, `步骤 8 完成`);
} catch (error) {
logger.error(this.siteName, `保存到数据库失败: ${error.message}`);
throw error;
}
}
/**
* 步骤9: 清理并关闭浏览器
*/
async step9_clearAndCloseBrowser() {
logger.info(this.siteName, `[步骤 9/${this.getTotalSteps()}] 清理并关闭浏览器`);
try {
await this.browserManager.clearAndClose();
this.currentStep = 9;
logger.success(this.siteName, `步骤 9 完成`);
} catch (error) {
logger.error(this.siteName, `清理并关闭浏览器失败: ${error.message}`);
throw error;
}
}
/**
* 执行注册流程
* @param {Object} options - 选项
* @param {number} options.fromStep - 从第几步开始默认1
* @param {number} options.toStep - 执行到第几步(默认全部)
*/
async register(options = {}) {
const fromStep = options.fromStep || 1;
const toStep = options.toStep || this.getTotalSteps();
try {
// 1. 生成数据
this.accountData = this.generateData(options);
logger.success(this.siteName, '账号数据生成完成');
logger.info(this.siteName, `First Name: ${this.accountData.firstName}`);
logger.info(this.siteName, `Last Name: ${this.accountData.lastName}`);
logger.info(this.siteName, `Email: ${this.accountData.email}`);
logger.info(this.siteName, `Password: ${this.accountData.password}`);
// 2. 初始化浏览器
await this.initBrowser();
// 3. 执行指定范围的步骤
logger.info(this.siteName, `\n开始执行步骤 ${fromStep}${toStep} (共 ${this.getTotalSteps()} 步)\n`);
for (let i = fromStep; i <= toStep && i <= this.getTotalSteps(); i++) {
const step = this.steps[i - 1];
if (typeof this[step.method] === 'function') {
try {
await this[step.method]();
} catch (error) {
logger.error(this.siteName, `步骤 ${i} 执行失败: ${error.message}`);
throw error;
}
} else {
logger.warn(this.siteName, `步骤 ${i} (${step.name}) 未实现`);
break;
}
}
const stepInfo = this.getCurrentStepInfo();
logger.success(this.siteName, `\n✅ 所有步骤已完成 (${fromStep}${this.currentStep})`);
return {
success: true,
account: this.accountData,
completedSteps: this.currentStep,
totalSteps: this.getTotalSteps(),
message: `完成 ${this.currentStep}/${this.getTotalSteps()}`
};
} catch (error) {
logger.error(this.siteName, `注册失败: ${error.message}`);
// 截图保存错误状态
if (this.page) {
try {
const screenshotPath = `/tmp/windsurf-error-${Date.now()}.png`;
await this.page.screenshot({ path: screenshotPath });
logger.info(this.siteName, `错误截图已保存: ${screenshotPath}`);
} catch (e) {
// 忽略截图错误
}
}
// 清理浏览器缓存并关闭
if (this.page) {
try {
logger.info(this.siteName, ' → 清除浏览器缓存...');
const client = await this.page.target().createCDPSession();
await client.send('Network.clearBrowserCookies');
await client.send('Network.clearBrowserCache');
await client.send('Storage.clearDataForOrigin', {
origin: '*',
storageTypes: 'all'
});
await this.page.goto('https://windsurf.com', { waitUntil: 'domcontentloaded' });
await this.page.evaluate(() => {
try {
localStorage.clear();
sessionStorage.clear();
} catch (e) {}
});
await client.detach();
logger.success(this.siteName, ' → ✓ 浏览器缓存已清除');
} catch (e) {
logger.warn(this.siteName, ` → 清除缓存失败: ${e.message}`);
}
}
await this.closeBrowser();
throw error;
}
}
}
module.exports = WindsurfRegister;