1014 lines
37 KiB
JavaScript
1014 lines
37 KiB
JavaScript
/**
|
||
* 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');
|
||
|
||
class WindsurfRegister {
|
||
constructor() {
|
||
this.siteName = 'Windsurf';
|
||
this.siteUrl = 'https://windsurf.com/account/register';
|
||
this.dataGen = new AccountDataGenerator();
|
||
this.human = new HumanBehavior();
|
||
this.emailService = new EmailVerificationService();
|
||
this.browser = null;
|
||
this.page = null;
|
||
this.currentStep = 0;
|
||
this.accountData = null;
|
||
|
||
// 定义所有步骤
|
||
this.steps = [
|
||
{ id: 1, name: '填写基本信息', method: 'step1_fillBasicInfo' },
|
||
{ id: 2, name: '设置密码', method: 'step2_setPassword' },
|
||
{ id: 3, name: 'Cloudflare验证', method: 'step3_cloudflareVerification' },
|
||
{ id: 4, name: '邮箱验证码', method: 'step4_emailVerificationCode' },
|
||
{ id: 5, name: '跳过问卷', method: 'step5_skipSurvey' },
|
||
{ id: 6, name: '选择计划', method: 'step6_selectPlan' },
|
||
{ id: 7, name: '填写支付信息', method: 'step7_fillPayment' },
|
||
// 根据实际注册流程继续添加
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取步骤总数
|
||
*/
|
||
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()
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 通用方法:点击按钮并等待页面跳转
|
||
* 使用页面特征(URL、DOM元素)来判断是否真的跳转了
|
||
* @param {Function} checkPageChanged - 检查页面是否已跳转的函数,返回Promise<boolean>
|
||
* @param {number} maxAttempts - 最多尝试次数
|
||
* @param {string} actionName - 操作名称(用于日志)
|
||
* @param {string} buttonText - 按钮文本过滤(可选,如"Continue")
|
||
* @returns {Promise<boolean>} - 是否成功跳转
|
||
*/
|
||
async clickButtonAndWaitForPageChange(checkPageChanged, maxAttempts = 5, actionName = '点击按钮', buttonText = null) {
|
||
let pageChanged = false;
|
||
let attempts = 0;
|
||
let lastClickedAt = 0; // 记录上次点击的时间,避免重复点击
|
||
|
||
while (!pageChanged && attempts < maxAttempts) {
|
||
attempts++;
|
||
|
||
// 记录点击前的页面特征
|
||
const beforeUrl = this.page.url();
|
||
const beforeHtml = await this.page.content();
|
||
|
||
// 查找未禁用的按钮
|
||
const buttons = await this.page.$$('button:not([disabled])');
|
||
let targetButton = null;
|
||
|
||
// 如果指定了按钮文本,找到匹配的按钮
|
||
if (buttonText) {
|
||
for (const btn of buttons) {
|
||
const text = await this.page.evaluate(el => el.textContent.trim(), btn);
|
||
// 不区分大小写匹配
|
||
if (text.toLowerCase().includes(buttonText.toLowerCase())) {
|
||
targetButton = btn;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果没找到匹配的按钮,使用第一个(回退策略)
|
||
if (!targetButton && buttons.length > 0) {
|
||
logger.warn(this.siteName, ` → 未找到包含"${buttonText}"的按钮,使用第一个按钮`);
|
||
targetButton = buttons[0];
|
||
}
|
||
} else {
|
||
// 没有指定文本,使用第一个按钮
|
||
targetButton = buttons[0];
|
||
}
|
||
|
||
if (targetButton) {
|
||
const text = await this.page.evaluate(el => el.textContent.trim(), targetButton);
|
||
logger.info(this.siteName, ` → 第${attempts}次${actionName}"${text}"...`);
|
||
|
||
// 点击按钮
|
||
const now = Date.now();
|
||
if (now - lastClickedAt < 3000) {
|
||
logger.warn(this.siteName, ` → 距离上次点击时间太短,等待中...`);
|
||
await this.human.randomDelay(2000, 3000);
|
||
}
|
||
|
||
await targetButton.click();
|
||
lastClickedAt = Date.now();
|
||
|
||
// 等待页面响应
|
||
await this.human.randomDelay(3000, 4000);
|
||
|
||
// 检查URL是否改变
|
||
const afterUrl = this.page.url();
|
||
const urlChanged = afterUrl !== beforeUrl;
|
||
|
||
// 检查页面内容是否改变(至少改变10%)
|
||
const afterHtml = await this.page.content();
|
||
const contentChanged = Math.abs(afterHtml.length - beforeHtml.length) / beforeHtml.length > 0.1;
|
||
|
||
// 使用自定义检查函数判断页面是否跳转
|
||
const customChanged = await checkPageChanged();
|
||
|
||
logger.info(this.siteName, ` → URL变化: ${urlChanged}, 内容变化: ${contentChanged}, 自定义检查: ${customChanged}`);
|
||
|
||
if (urlChanged || customChanged) {
|
||
pageChanged = true;
|
||
logger.success(this.siteName, ` → ✓ 页面已跳转 (${urlChanged ? 'URL变化' : ''}${customChanged ? ' 元素变化' : ''})`);
|
||
break;
|
||
}
|
||
|
||
logger.warn(this.siteName, ` → 页面未跳转,${attempts}/${maxAttempts}`);
|
||
await this.human.randomDelay(1000, 2000);
|
||
} else {
|
||
logger.warn(this.siteName, ` → 未找到可点击的按钮`);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!pageChanged) {
|
||
logger.error(this.siteName, ` → ${maxAttempts}次尝试后页面仍未跳转`);
|
||
}
|
||
|
||
return pageChanged;
|
||
}
|
||
|
||
/**
|
||
* 生成账号数据(干运行模式)
|
||
*/
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 初始化浏览器(使用puppeteer-real-browser,自动处理Cloudflare)
|
||
*/
|
||
async initBrowser() {
|
||
const { connect } = require('puppeteer-real-browser');
|
||
|
||
logger.info(this.siteName, '启动浏览器(自动Cloudflare绕过模式)...');
|
||
|
||
// 随机视口大小(模拟不同设备)
|
||
const viewports = [
|
||
{ width: 1920, height: 1080 },
|
||
{ width: 1366, height: 768 },
|
||
{ width: 1536, height: 864 },
|
||
{ width: 1440, height: 900 }
|
||
];
|
||
const viewport = viewports[Math.floor(Math.random() * viewports.length)];
|
||
|
||
// 使用 puppeteer-real-browser 连接,启用 turnstile 自动处理 Cloudflare
|
||
const result = await connect({
|
||
turnstile: true, // 自动处理 Cloudflare Turnstile
|
||
headless: false,
|
||
args: [
|
||
'--no-sandbox',
|
||
'--disable-setuid-sandbox',
|
||
`--window-size=${viewport.width},${viewport.height}`
|
||
]
|
||
});
|
||
|
||
this.browser = result.browser;
|
||
this.page = result.page;
|
||
|
||
// 设置随机视口
|
||
await this.page.setViewport(viewport);
|
||
|
||
// 设置语言和时区
|
||
await this.page.setExtraHTTPHeaders({
|
||
'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7'
|
||
});
|
||
|
||
// 设置更长的导航超时时间
|
||
await this.page.setDefaultNavigationTimeout(60000);
|
||
|
||
logger.success(this.siteName, `浏览器启动成功 (${viewport.width}x${viewport.height})`);
|
||
|
||
// 浏览器启动后等待一段时间,确保完全准备好
|
||
logger.info(this.siteName, '等待浏览器完全准备...');
|
||
await this.human.randomDelay(3000, 5000);
|
||
}
|
||
|
||
/**
|
||
* 关闭浏览器
|
||
*/
|
||
async closeBrowser() {
|
||
if (this.browser) {
|
||
await this.browser.close();
|
||
logger.info(this.siteName, '浏览器已关闭');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 步骤1: 填写基本信息(First Name, Last Name, Email)- 使用人类行为
|
||
*/
|
||
async step1_fillBasicInfo() {
|
||
logger.info(this.siteName, `[步骤 1/${this.getTotalSteps()}] 填写基本信息`);
|
||
|
||
// 打开注册页面
|
||
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按钮并等待跳转到密码页面
|
||
logger.info(this.siteName, ' → 点击Continue按钮...');
|
||
|
||
// 等待按钮激活
|
||
await this.page.waitForSelector('button:not([disabled])', { timeout: 10000 });
|
||
logger.info(this.siteName, ' → 按钮已激活');
|
||
|
||
// 使用通用方法处理页面跳转
|
||
const checkPageChanged = async () => {
|
||
// 检查是否有密码输入框
|
||
const hasPasswordField = await this.page.evaluate(() => {
|
||
return !!document.querySelector('#password');
|
||
});
|
||
|
||
// 检查URL是否改变
|
||
const currentUrl = this.page.url();
|
||
const urlChanged = currentUrl !== this.siteUrl;
|
||
|
||
return hasPasswordField || urlChanged;
|
||
};
|
||
|
||
await this.clickButtonAndWaitForPageChange(checkPageChanged, 5, '点击Continue', 'Continue');
|
||
|
||
// 额外等待确保页面稳定
|
||
await this.human.randomDelay(2000, 3000);
|
||
|
||
this.currentStep = 1;
|
||
logger.success(this.siteName, `步骤 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按钮
|
||
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, ' → 按钮已激活');
|
||
} catch (e) {
|
||
logger.warn(this.siteName, ' → 按钮等待超时,尝试继续');
|
||
}
|
||
|
||
// 使用通用方法点击按钮并等待页面跳转
|
||
// 密码页面点击后会出现Cloudflare验证,检查验证元素是否出现
|
||
const checkPageChanged = async () => {
|
||
// 检查是否出现了Cloudflare验证页面的特征
|
||
const hasCloudflare = await this.page.$('iframe[src*="challenges.cloudflare.com"]').then(el => !!el).catch(() => false);
|
||
// 或者检查密码输入框是否消失
|
||
const noPasswordField = await this.page.$('#password').then(el => !el).catch(() => true);
|
||
return hasCloudflare || noPasswordField;
|
||
};
|
||
|
||
const success = await this.clickButtonAndWaitForPageChange(checkPageChanged, 3, '点击Continue', 'Continue');
|
||
|
||
if (!success) {
|
||
// 如果点击失败,尝试按Enter键
|
||
logger.warn(this.siteName, ' → 点击失败,尝试按Enter键');
|
||
await this.page.keyboard.press('Enter');
|
||
await this.human.randomDelay(2000, 3000);
|
||
}
|
||
|
||
this.currentStep = 2;
|
||
logger.success(this.siteName, `步骤 2 完成`);
|
||
}
|
||
|
||
/**
|
||
* Cloudflare Turnstile验证(步骤2.5)- 使用通用处理器
|
||
*/
|
||
async handleCloudflareVerification() {
|
||
const cloudflareMode = DEFAULT_CONFIG.cloudflare.mode;
|
||
|
||
// 自定义检测函数:检查Continue按钮是否激活
|
||
const customCheck = async () => {
|
||
try {
|
||
const buttonEnabled = await this.page.evaluate(() => {
|
||
const button = document.querySelector('button');
|
||
return button && !button.disabled;
|
||
});
|
||
return buttonEnabled;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
const handler = new CloudflareHandler(this.page, this.human, this.siteName, cloudflareMode, customCheck);
|
||
const result = await handler.handle();
|
||
|
||
// 如果验证通过,点击Continue按钮进入下一页
|
||
if (result === 'passed') {
|
||
logger.info(this.siteName, '[Cloudflare] 点击Continue按钮进入验证码页面...');
|
||
|
||
try {
|
||
let pageChanged = false;
|
||
let attempts = 0;
|
||
const maxAttempts = 10; // 最多尝试10次
|
||
|
||
while (!pageChanged && attempts < maxAttempts) {
|
||
attempts++;
|
||
|
||
// 查找并点击Continue按钮
|
||
const button = await this.page.$('button:not([disabled])');
|
||
if (button) {
|
||
logger.info(this.siteName, `[Cloudflare] 第${attempts}次点击Continue...`);
|
||
await button.click();
|
||
await this.human.randomDelay(2000, 3000);
|
||
|
||
// 检查是否有错误提示
|
||
const hasError = await this.page.evaluate(() => {
|
||
const errorMsg = document.querySelector('p.caption1.text-sk-error');
|
||
return errorMsg && errorMsg.textContent.includes('An error occurred');
|
||
});
|
||
|
||
if (hasError) {
|
||
logger.warn(this.siteName, '[Cloudflare] 检测到错误提示,重新尝试...');
|
||
await this.human.randomDelay(2000, 3000);
|
||
continue;
|
||
}
|
||
|
||
// 检查页面是否已改变
|
||
const checkResult = await this.page.evaluate(() => {
|
||
// 方法1: 检查是否有"Check your inbox"文本
|
||
const hasCheckInbox = document.body.textContent.includes('Check your inbox');
|
||
|
||
// 方法2: 检查按钮是否被禁用
|
||
const button = document.querySelector('button');
|
||
const buttonDisabled = button && button.disabled;
|
||
|
||
// 方法3: 检查是否还有"verify that you are human"文本
|
||
const stillOnVerifyPage = document.body.textContent.includes('verify that you are human');
|
||
|
||
return {
|
||
hasCheckInbox,
|
||
buttonDisabled,
|
||
stillOnVerifyPage
|
||
};
|
||
});
|
||
|
||
logger.info(this.siteName, `[Cloudflare] 页面状态: inbox=${checkResult.hasCheckInbox}, buttonDisabled=${checkResult.buttonDisabled}, stillVerify=${checkResult.stillOnVerifyPage}`);
|
||
|
||
// 判断是否成功跳转
|
||
if (checkResult.hasCheckInbox || (!checkResult.stillOnVerifyPage && checkResult.buttonDisabled)) {
|
||
pageChanged = true;
|
||
logger.success(this.siteName, '[Cloudflare] ✓ 已进入验证码页面');
|
||
break;
|
||
}
|
||
|
||
// 如果还在验证页面,继续等待
|
||
logger.info(this.siteName, '[Cloudflare] 页面未跳转,继续尝试...');
|
||
await this.human.randomDelay(2000, 3000);
|
||
} else {
|
||
logger.warn(this.siteName, '[Cloudflare] 未找到可点击的按钮');
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!pageChanged) {
|
||
logger.warn(this.siteName, `[Cloudflare] ${maxAttempts}次尝试后页面仍未跳转`);
|
||
}
|
||
|
||
// 额外等待确保页面稳定
|
||
await this.human.randomDelay(2000, 3000);
|
||
|
||
} catch (e) {
|
||
logger.warn(this.siteName, `[Cloudflare] 点击按钮失败: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 步骤3: Cloudflare验证并进入验证码页面
|
||
*/
|
||
async step3_cloudflareVerification() {
|
||
logger.info(this.siteName, `[步骤 3/${this.getTotalSteps()}] Cloudflare验证`);
|
||
|
||
// puppeteer-real-browser 的 turnstile 会自动处理 Cloudflare
|
||
logger.info(this.siteName, '[Cloudflare] turnstile 自动处理中...');
|
||
|
||
// 等待一段时间让 Cloudflare 自动验证完成
|
||
await this.human.randomDelay(5000, 8000);
|
||
logger.success(this.siteName, '[Cloudflare] ✓ 自动验证完成');
|
||
|
||
// 点击 Cloudflare 页面的 Continue 按钮进入验证码页面
|
||
logger.info(this.siteName, '[Cloudflare] 点击Continue按钮进入验证码页面...');
|
||
|
||
const currentUrl = this.page.url();
|
||
logger.info(this.siteName, ` → 当前URL: ${currentUrl}`);
|
||
|
||
// 使用通用方法检测页面跳转
|
||
const checkPageChanged = async () => {
|
||
const newUrl = this.page.url();
|
||
// 检查URL是否变化或是否有验证码输入框
|
||
const hasCodeInput = await this.page.$('input[type="text"]').then(el => !!el).catch(() => false);
|
||
return newUrl !== currentUrl || hasCodeInput;
|
||
};
|
||
|
||
// 点击Continue按钮并等待页面跳转
|
||
const success = await this.clickButtonAndWaitForPageChange(checkPageChanged, 5, '点击Continue', 'Continue');
|
||
|
||
if (!success) {
|
||
throw new Error('Cloudflare验证后页面未成功跳转到验证码页面');
|
||
}
|
||
|
||
this.currentStep = 3;
|
||
logger.success(this.siteName, `步骤 3 完成`);
|
||
}
|
||
|
||
/**
|
||
* 步骤4: 获取邮箱验证码并填写
|
||
*/
|
||
async step4_emailVerificationCode() {
|
||
logger.info(this.siteName, `[步骤 4/${this.getTotalSteps()}] 邮箱验证码`);
|
||
|
||
try {
|
||
// 等待验证码页面加载
|
||
await this.human.readPage(1, 2);
|
||
|
||
// 延迟5秒后再获取验证码,让邮件有足够时间到达
|
||
logger.info(this.siteName, ' → 延迟5秒,等待邮件到达...');
|
||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||
|
||
// 获取验证码(从邮箱)
|
||
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, ' → 验证码已填写完成');
|
||
|
||
// 等待按钮激活(填完验证码后按钮会自动启用)
|
||
logger.info(this.siteName, ' → 等待按钮激活...');
|
||
await this.human.randomDelay(2000, 3000);
|
||
|
||
// 等待按钮激活
|
||
try {
|
||
await this.page.waitForSelector('button:not([disabled])', { timeout: 10000 });
|
||
logger.success(this.siteName, ' → 按钮已激活');
|
||
} catch (e) {
|
||
logger.warn(this.siteName, ` → 按钮等待超时: ${e.message}`);
|
||
}
|
||
|
||
// 点击按钮并等待页面跳转
|
||
const checkPageChanged = async () => {
|
||
// 检查是否离开了验证码页面(URL改变或页面元素改变)
|
||
const currentUrl = this.page.url();
|
||
return !currentUrl.includes('/register') ||
|
||
await this.page.$('input[type="text"]').then(el => !el).catch(() => true);
|
||
};
|
||
|
||
await this.clickButtonAndWaitForPageChange(checkPageChanged, 3, '点击Create account');
|
||
|
||
this.currentStep = 4;
|
||
logger.success(this.siteName, `步骤 4 完成`);
|
||
|
||
} else {
|
||
logger.error(this.siteName, ' → 未找到验证码输入框!');
|
||
logger.warn(this.siteName, ' → 请手动输入验证码: ' + code);
|
||
|
||
// 等待用户手动输入
|
||
await this.human.randomDelay(30000, 30000);
|
||
|
||
this.currentStep = 4;
|
||
}
|
||
|
||
} catch (error) {
|
||
logger.error(this.siteName, `邮箱验证失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 步骤5: 跳过问卷调查
|
||
*/
|
||
async step5_skipSurvey() {
|
||
logger.info(this.siteName, `[步骤 5/${this.getTotalSteps()}] 跳过问卷`);
|
||
|
||
try {
|
||
// 等待页面加载
|
||
await this.human.readPage(2, 3);
|
||
|
||
// 查找并点击"Skip this step"按钮
|
||
logger.info(this.siteName, ' → 查找"Skip this step"按钮...');
|
||
|
||
// 方式1: 通过button文本查找
|
||
const buttons = await this.page.$$('button');
|
||
let skipButton = null;
|
||
|
||
for (const button of buttons) {
|
||
const text = await this.page.evaluate(el => el.textContent?.trim(), button);
|
||
if (text && text.toLowerCase().includes('skip')) {
|
||
skipButton = button;
|
||
logger.info(this.siteName, ` → 找到按钮: "${text}"`);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (skipButton) {
|
||
logger.info(this.siteName, ' → 点击"Skip this step"按钮...');
|
||
await skipButton.click();
|
||
|
||
// 等待页面跳转
|
||
await this.human.randomDelay(2000, 3000);
|
||
|
||
this.currentStep = 5;
|
||
logger.success(this.siteName, `步骤 5 完成`);
|
||
} else {
|
||
logger.warn(this.siteName, ' → 未找到Skip按钮,可能已跳过此页面');
|
||
this.currentStep = 5;
|
||
}
|
||
|
||
} catch (error) {
|
||
logger.error(this.siteName, `跳过问卷失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 步骤6: 选择计划
|
||
*/
|
||
async step6_selectPlan() {
|
||
logger.info(this.siteName, `[步骤 6/${this.getTotalSteps()}] 选择计划`);
|
||
|
||
try {
|
||
// 等待页面加载
|
||
await this.human.readPage(2, 3);
|
||
|
||
// 查找并点击"Select plan"按钮
|
||
logger.info(this.siteName, ' → 查找"Select plan"按钮...');
|
||
|
||
// 方式1: 通过按钮文本查找
|
||
const buttons = await this.page.$$('button');
|
||
let selectButton = null;
|
||
|
||
for (const button of buttons) {
|
||
const text = await this.page.evaluate(el => el.textContent?.trim(), button);
|
||
if (text && text.toLowerCase().includes('select plan')) {
|
||
selectButton = button;
|
||
logger.info(this.siteName, ` → 找到按钮: "${text}"`);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (selectButton) {
|
||
logger.info(this.siteName, ' → 点击"Select plan"按钮...');
|
||
await selectButton.click();
|
||
|
||
// 等待页面跳转到支付页面
|
||
logger.info(this.siteName, ' → 等待进入支付页面...');
|
||
await this.human.randomDelay(3000, 5000);
|
||
|
||
// 验证是否进入支付页面(检查是否有Stripe元素或支付表单)
|
||
const hasPaymentForm = await this.page.$('button[data-testid="card-accordion-item-button"]').then(el => !!el).catch(() => false);
|
||
const hasStripeCheckout = this.page.url().includes('checkout.stripe.com');
|
||
|
||
if (hasPaymentForm || hasStripeCheckout) {
|
||
logger.success(this.siteName, ' → ✓ 已进入支付页面');
|
||
this.currentStep = 6;
|
||
logger.success(this.siteName, `步骤 6 完成`);
|
||
} else {
|
||
logger.warn(this.siteName, ' → 未进入支付页面,可能需要手动处理');
|
||
logger.info(this.siteName, ` → 当前URL: ${this.page.url()}`);
|
||
this.currentStep = 6;
|
||
}
|
||
} else {
|
||
logger.warn(this.siteName, ' → 未找到Select plan按钮');
|
||
// 检查是否已经在支付页面
|
||
const hasPaymentForm = await this.page.$('button[data-testid="card-accordion-item-button"]').then(el => !!el).catch(() => false);
|
||
if (hasPaymentForm) {
|
||
logger.success(this.siteName, ' → 已在支付页面,跳过选择计划');
|
||
this.currentStep = 6;
|
||
} else {
|
||
throw new Error('未找到Select plan按钮,且不在支付页面');
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
logger.error(this.siteName, `选择计划失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 步骤7: 填写支付信息
|
||
*/
|
||
async step7_fillPayment() {
|
||
logger.info(this.siteName, `[步骤 7/${this.getTotalSteps()}] 填写支付信息`);
|
||
|
||
try {
|
||
// 等待页面加载
|
||
await this.human.readPage(3, 5);
|
||
|
||
// 1. 生成信用卡信息(使用银联卡)
|
||
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}`);
|
||
|
||
// 2. 首先找到并点击银行卡区域展开表单
|
||
logger.info(this.siteName, ' → 点击展开银行卡支付区域...');
|
||
try {
|
||
// 点击银行卡区域的按钮来展开表单
|
||
const cardButton = await this.page.$('button[data-testid="card-accordion-item-button"]');
|
||
if (cardButton) {
|
||
await cardButton.click();
|
||
await this.human.randomDelay(2000, 3000);
|
||
logger.success(this.siteName, ' → ✓ 银行卡区域已展开');
|
||
} else {
|
||
// 如果按钮不存在,尝试点击radio
|
||
const cardRadio = await this.page.$('input[type="radio"][value="card"]');
|
||
if (cardRadio) {
|
||
await cardRadio.click();
|
||
await this.human.randomDelay(2000, 3000);
|
||
logger.success(this.siteName, ' → ✓ 已选择银行卡');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
logger.warn(this.siteName, ` → 展开银行卡区域失败: ${e.message},继续尝试...`);
|
||
}
|
||
|
||
// 等待Stripe表单iframe加载
|
||
logger.info(this.siteName, ' → 等待支付表单加载...');
|
||
await this.human.randomDelay(3000, 5000);
|
||
|
||
// 3. 填写卡号(使用humanType逐字符输入)
|
||
logger.info(this.siteName, ' → 填写卡号...');
|
||
try {
|
||
await this.page.waitForSelector('#cardNumber', { timeout: 15000 });
|
||
await this.page.click('#cardNumber');
|
||
await this.human.randomDelay(500, 800);
|
||
// 清空
|
||
await this.page.evaluate(() => document.querySelector('#cardNumber').value = '');
|
||
// 使用人类行为输入
|
||
await this.human.humanType(this.page, '#cardNumber', card.number);
|
||
logger.success(this.siteName, ` → ✓ 卡号已填写: ${card.number}`);
|
||
await this.human.randomDelay(1500, 2000);
|
||
} catch (e) {
|
||
logger.error(this.siteName, ` → 填写卡号失败: ${e.message}`);
|
||
throw e;
|
||
}
|
||
|
||
// 4. 填写有效期(月份/年份)
|
||
logger.info(this.siteName, ' → 填写有效期...');
|
||
try {
|
||
await this.page.click('#cardExpiry');
|
||
await this.human.randomDelay(300, 500);
|
||
await this.page.evaluate(() => document.querySelector('#cardExpiry').value = '');
|
||
const expiry = `${card.month}${card.year}`; // 格式: MMYY
|
||
await this.human.humanType(this.page, '#cardExpiry', expiry);
|
||
logger.success(this.siteName, ` → ✓ 有效期已填写: ${expiry}`);
|
||
await this.human.randomDelay(1500, 2000);
|
||
} catch (e) {
|
||
logger.error(this.siteName, ` → 填写有效期失败: ${e.message}`);
|
||
throw e;
|
||
}
|
||
|
||
// 5. 填写CVC
|
||
logger.info(this.siteName, ' → 填写CVC...');
|
||
try {
|
||
await this.page.click('#cardCvc');
|
||
await this.human.randomDelay(300, 500);
|
||
await this.page.evaluate(() => document.querySelector('#cardCvc').value = '');
|
||
await this.human.humanType(this.page, '#cardCvc', card.cvv);
|
||
logger.success(this.siteName, ` → ✓ CVC已填写: ${card.cvv}`);
|
||
await this.human.randomDelay(1500, 2000);
|
||
} catch (e) {
|
||
logger.error(this.siteName, ` → 填写CVC失败: ${e.message}`);
|
||
throw e;
|
||
}
|
||
|
||
// 6. 填写持卡人姓名
|
||
logger.info(this.siteName, ' → 填写持卡人姓名...');
|
||
try {
|
||
await this.page.click('#billingName');
|
||
await this.human.randomDelay(300, 500);
|
||
await this.page.evaluate(() => document.querySelector('#billingName').value = '');
|
||
const fullName = `${this.accountData.firstName} ${this.accountData.lastName}`;
|
||
await this.human.humanType(this.page, '#billingName', fullName);
|
||
logger.success(this.siteName, ` → ✓ 姓名已填写: ${fullName}`);
|
||
await this.human.randomDelay(1500, 2000);
|
||
} catch (e) {
|
||
logger.error(this.siteName, ` → 填写姓名失败: ${e.message}`);
|
||
throw e;
|
||
}
|
||
|
||
// 7. 选择地址:中国澳门特别行政区
|
||
logger.info(this.siteName, ' → 选择地址:中国澳门特别行政区...');
|
||
try {
|
||
await this.page.waitForSelector('#billingCountry', { timeout: 15000 });
|
||
await this.page.select('#billingCountry', 'MO');
|
||
logger.success(this.siteName, ' → ✓ 地址已选择: 中国澳门特别行政区');
|
||
await this.human.randomDelay(2000, 3000);
|
||
} catch (e) {
|
||
logger.error(this.siteName, ` → 选择地址失败: ${e.message}`);
|
||
throw e;
|
||
}
|
||
|
||
// 8. 填写地址信息(如果需要)
|
||
// 等待地址字段加载
|
||
await this.human.randomDelay(1000, 2000);
|
||
|
||
// 检查是否需要填写地址行1和行2
|
||
const addressFields = await this.page.$$('input[placeholder*="地址"]');
|
||
if (addressFields.length > 0) {
|
||
logger.info(this.siteName, ' → 填写地址信息...');
|
||
// 填写地址:kowloon
|
||
await addressFields[0].type('kowloon', { delay: 100 });
|
||
logger.success(this.siteName, ' → ✓ 地址行1已填写: kowloon');
|
||
if (addressFields[1]) {
|
||
await this.human.randomDelay(300, 500);
|
||
await addressFields[1].type('kowloon', { delay: 100 });
|
||
logger.success(this.siteName, ' → ✓ 地址行2已填写: kowloon');
|
||
}
|
||
}
|
||
|
||
// 9. 取消勾选"保存我的信息"
|
||
logger.info(this.siteName, ' → 检查并取消勾选保存信息...');
|
||
try {
|
||
const saveCheckbox = await this.page.$('input[type="checkbox"]');
|
||
if (saveCheckbox) {
|
||
const isChecked = await this.page.evaluate(el => el.checked, saveCheckbox);
|
||
if (isChecked) {
|
||
await saveCheckbox.click();
|
||
logger.success(this.siteName, ' → ✓ 已取消勾选保存信息');
|
||
await this.human.randomDelay(500, 1000);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
logger.warn(this.siteName, ` → 取消勾选失败: ${e.message}`);
|
||
}
|
||
|
||
// 10. 点击订阅按钮
|
||
logger.info(this.siteName, ' → 点击订阅按钮...');
|
||
await this.human.randomDelay(2000, 3000);
|
||
|
||
const submitButton = await this.page.$('button[type="submit"][data-testid="hosted-payment-submit-button"]');
|
||
if (submitButton) {
|
||
await submitButton.click();
|
||
logger.success(this.siteName, ' → ✓ 已点击订阅按钮');
|
||
|
||
// 等待处理
|
||
await this.human.randomDelay(5000, 7000);
|
||
|
||
this.currentStep = 7;
|
||
logger.success(this.siteName, `步骤 7 完成`);
|
||
} else {
|
||
logger.warn(this.siteName, ' → 未找到订阅按钮');
|
||
this.currentStep = 7;
|
||
}
|
||
|
||
} 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}`);
|
||
|
||
if (this.currentStep < this.getTotalSteps()) {
|
||
logger.info(this.siteName, `剩余步骤需要手动完成或等待后续开发`);
|
||
}
|
||
|
||
// 不自动关闭浏览器,让用户查看结果
|
||
if (!options.keepBrowserOpen) {
|
||
logger.info(this.siteName, '10秒后关闭浏览器...');
|
||
await this.page.waitForTimeout(10000);
|
||
await this.closeBrowser();
|
||
} else {
|
||
logger.info(this.siteName, '浏览器保持打开状态');
|
||
}
|
||
|
||
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) {
|
||
// 忽略截图错误
|
||
}
|
||
}
|
||
|
||
await this.closeBrowser();
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = WindsurfRegister;
|