1442 lines
52 KiB
JavaScript
1442 lines
52 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');
|
||
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;
|