1525 lines
58 KiB
JavaScript
1525 lines
58 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');
|
||
|
||
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.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;
|
||
}
|
||
|
||
/**
|
||
* 初始化浏览器 - 使用 AdsPower 指纹浏览器
|
||
*/
|
||
async initBrowser(options = {}) {
|
||
const puppeteer = require('puppeteer');
|
||
const axios = require('axios');
|
||
|
||
logger.info(this.siteName, '启动 AdsPower 指纹浏览器...');
|
||
|
||
// 检查 AdsPower 配置
|
||
const adspowerUserId = process.env.ADSPOWER_USER_ID;
|
||
if (!adspowerUserId) {
|
||
logger.error(this.siteName, '');
|
||
logger.error(this.siteName, '❌ 未配置 ADSPOWER_USER_ID');
|
||
logger.error(this.siteName, '');
|
||
logger.error(this.siteName, '请在 .env 文件中配置:');
|
||
logger.error(this.siteName, 'ADSPOWER_USER_ID=your_profile_id');
|
||
logger.error(this.siteName, '');
|
||
throw new Error('未配置 AdsPower 用户ID');
|
||
}
|
||
|
||
const apiBase = process.env.ADSPOWER_API || 'http://local.adspower.net:50325';
|
||
const apiKey = process.env.ADSPOWER_API_KEY;
|
||
|
||
const startUrl = `${apiBase}/api/v1/browser/start?user_id=${encodeURIComponent(adspowerUserId)}`;
|
||
|
||
// 配置请求头
|
||
const headers = {};
|
||
if (apiKey && apiKey.trim()) {
|
||
headers['Authorization'] = `Bearer ${apiKey}`;
|
||
logger.info(this.siteName, '✓ 使用 API Key 认证');
|
||
}
|
||
|
||
logger.info(this.siteName, ` → 启动 AdsPower 配置: ${adspowerUserId}`);
|
||
|
||
try {
|
||
const response = await axios.get(startUrl, { headers });
|
||
const data = response.data;
|
||
|
||
if (data.code !== 0) {
|
||
logger.error(this.siteName, '');
|
||
logger.error(this.siteName, `AdsPower API 返回错误: ${JSON.stringify(data)}`);
|
||
logger.error(this.siteName, '');
|
||
logger.error(this.siteName, '解决方案:');
|
||
logger.error(this.siteName, '1. 确保 AdsPower 应用已启动并登录');
|
||
logger.error(this.siteName, '2. 检查配置文件 ID 是否正确: ' + adspowerUserId);
|
||
logger.error(this.siteName, '3. 如果需要 API Key,请在 AdsPower 设置中生成');
|
||
logger.error(this.siteName, '4. 尝试在 AdsPower 中手动打开一次浏览器配置');
|
||
logger.error(this.siteName, '');
|
||
throw new Error(`AdsPower 启动失败: ${data.msg || JSON.stringify(data)}`);
|
||
}
|
||
|
||
// 获取 WebSocket 端点
|
||
const wsEndpoint = data.data.ws && (
|
||
data.data.ws.puppeteer ||
|
||
data.data.ws.selenium ||
|
||
data.data.ws.ws ||
|
||
data.data.ws
|
||
);
|
||
|
||
if (!wsEndpoint) {
|
||
throw new Error('AdsPower 未返回 WebSocket 端点');
|
||
}
|
||
|
||
logger.info(this.siteName, ` → WebSocket: ${wsEndpoint}`);
|
||
|
||
// 连接到 AdsPower 浏览器
|
||
this.browser = await puppeteer.connect({
|
||
browserWSEndpoint: wsEndpoint,
|
||
defaultViewport: null
|
||
});
|
||
|
||
// 获取已存在的页面
|
||
const pages = await this.browser.pages();
|
||
this.page = pages[0] || await this.browser.newPage();
|
||
|
||
// 关闭多余的标签页(AdsPower 需要至少保留一个)
|
||
if (pages.length > 1) {
|
||
for (let i = 1; i < pages.length; i++) {
|
||
try {
|
||
await pages[i].close();
|
||
} catch (e) {
|
||
// 忽略关闭失败
|
||
}
|
||
}
|
||
}
|
||
|
||
logger.success(this.siteName, '✓ AdsPower 浏览器连接成功');
|
||
logger.info(this.siteName, '✓ 使用真实指纹,可同时绕过 Cloudflare 和 Stripe');
|
||
|
||
logger.info(this.siteName, '等待浏览器完全准备...');
|
||
await this.human.randomDelay(2000, 3000);
|
||
|
||
} catch (error) {
|
||
logger.error(this.siteName, '');
|
||
logger.error(this.siteName, `❌ AdsPower 连接失败: ${error.message}`);
|
||
logger.error(this.siteName, '');
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 关闭浏览器
|
||
*/
|
||
async closeBrowser() {
|
||
if (this.browser) {
|
||
await this.browser.close();
|
||
logger.info(this.siteName, '浏览器已关闭');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 步骤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} 次)`);
|
||
}
|
||
|
||
/**
|
||
* 步骤6: 填写支付信息
|
||
*/
|
||
async step6_fillPayment() {
|
||
logger.info(this.siteName, `[步骤 6/${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}`);
|
||
|
||
// 保存卡信息供后续使用
|
||
this.cardInfo = {
|
||
number: card.number,
|
||
month: card.month,
|
||
year: card.year,
|
||
cvv: card.cvv,
|
||
country: 'MO'
|
||
};
|
||
|
||
// 2. 点击选择"银行卡"支付方式
|
||
logger.info(this.siteName, ' → 选择银行卡支付方式...');
|
||
const cardRadio = await this.page.$('input[type="radio"][value="card"]');
|
||
if (cardRadio) {
|
||
await cardRadio.click();
|
||
logger.success(this.siteName, ' → ✓ 已点击银行卡选项');
|
||
|
||
// 等待支付表单完全加载(等待骨架屏消失,表单元素出现)
|
||
logger.info(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}`);
|
||
}
|
||
}
|
||
|
||
// 3. 填写卡号
|
||
logger.info(this.siteName, ' → 填写卡号...');
|
||
await this.page.waitForSelector('#cardNumber', { visible: true, timeout: 10000 });
|
||
await this.page.click('#cardNumber');
|
||
await this.human.randomDelay(500, 1000);
|
||
await this.page.type('#cardNumber', card.number, { delay: 250 });
|
||
|
||
// 4. 填写有效期(月份/年份)
|
||
logger.info(this.siteName, ' → 填写有效期...');
|
||
await this.page.click('#cardExpiry');
|
||
await this.human.randomDelay(300, 500);
|
||
const expiry = `${card.month}${card.year}`; // 格式: MMYY
|
||
await this.page.type('#cardExpiry', expiry, { delay: 250 });
|
||
|
||
// 5. 填写CVC
|
||
logger.info(this.siteName, ' → 填写CVC...');
|
||
await this.page.click('#cardCvc');
|
||
await this.human.randomDelay(300, 500);
|
||
await this.page.type('#cardCvc', card.cvv, { delay: 250 });
|
||
|
||
// 6. 填写持卡人姓名
|
||
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 });
|
||
|
||
// 7. 选择地址:中国澳门特别行政区
|
||
logger.info(this.siteName, ' → 选择地址:中国澳门特别行政区...');
|
||
await this.page.select('#billingCountry', 'MO');
|
||
await this.human.randomDelay(1000, 2000);
|
||
|
||
// 8. 填写地址信息(如果需要)
|
||
// 等待地址字段加载
|
||
await this.human.randomDelay(1000, 2000);
|
||
|
||
// 检查是否需要填写地址行1和行2
|
||
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 });
|
||
}
|
||
}
|
||
|
||
// 9. 点击订阅按钮并检测卡片拒绝(支持重试)
|
||
logger.info(this.siteName, ' → 点击订阅按钮...');
|
||
await this.human.randomDelay(2000, 3000);
|
||
const maxRetries = 5; // 最多重试5次
|
||
let retryCount = 0;
|
||
let paymentSuccess = false;
|
||
|
||
while (!paymentSuccess && retryCount < maxRetries) {
|
||
const submitButton = await this.page.$('button[type="submit"][data-testid="hosted-payment-submit-button"]');
|
||
if (!submitButton) {
|
||
logger.warn(this.siteName, ' → 未找到订阅按钮');
|
||
break;
|
||
}
|
||
|
||
if (retryCount > 0) {
|
||
logger.info(this.siteName, ` → 第 ${retryCount + 1} 次尝试提交...`);
|
||
}
|
||
|
||
await submitButton.click();
|
||
logger.success(this.siteName, ' → ✓ 已点击订阅按钮');
|
||
|
||
// 等待一下让页面响应
|
||
await this.human.randomDelay(2000, 3000);
|
||
|
||
// 检测是否出现"银行卡被拒绝"错误
|
||
const cardRejected = await this.page.evaluate(() => {
|
||
const errorDiv = document.querySelector('.FieldError-container');
|
||
if (errorDiv) {
|
||
const errorText = errorDiv.textContent;
|
||
return errorText.includes('银行卡被拒绝') ||
|
||
errorText.includes('card was declined') ||
|
||
errorText.includes('被拒绝');
|
||
}
|
||
return false;
|
||
});
|
||
|
||
if (cardRejected) {
|
||
retryCount++;
|
||
logger.warn(this.siteName, ` → ⚠️ 银行卡被拒绝!(第 ${retryCount}/${maxRetries} 次)`);
|
||
|
||
if (retryCount >= maxRetries) {
|
||
logger.error(this.siteName, ` → ✗ 已达到最大重试次数 (${maxRetries})`);
|
||
throw new Error(`银行卡被拒绝,已重试 ${maxRetries} 次`);
|
||
}
|
||
|
||
// 生成新的银行卡
|
||
logger.info(this.siteName, ' → 生成新的银行卡信息...');
|
||
const cardGen = new CardGenerator();
|
||
const newCard = cardGen.generate('unionpay');
|
||
logger.info(this.siteName, ` → 新卡号: ${newCard.number}`);
|
||
logger.info(this.siteName, ` → 有效期: ${newCard.month}/${newCard.year}`);
|
||
logger.info(this.siteName, ` → CVV: ${newCard.cvv}`);
|
||
|
||
// 更新卡信息
|
||
this.cardInfo = {
|
||
number: newCard.number,
|
||
month: newCard.month,
|
||
year: newCard.year,
|
||
cvv: newCard.cvv,
|
||
country: 'MO'
|
||
};
|
||
|
||
// 清空并重新填写卡号
|
||
logger.info(this.siteName, ' → 清空并重新填写卡号...');
|
||
const cardNumberField = await this.page.$('#cardNumber');
|
||
await cardNumberField.click({ clickCount: 3 });
|
||
await this.page.keyboard.press('Backspace');
|
||
await this.human.randomDelay(500, 1000);
|
||
await cardNumberField.type(newCard.number, { delay: 250 });
|
||
|
||
// 清空并重新填写有效期
|
||
logger.info(this.siteName, ' → 清空并重新填写有效期...');
|
||
const cardExpiryField = await this.page.$('#cardExpiry');
|
||
await cardExpiryField.click({ clickCount: 3 });
|
||
await this.page.keyboard.press('Backspace');
|
||
await this.human.randomDelay(300, 500);
|
||
const expiry = `${newCard.month}${newCard.year}`;
|
||
await cardExpiryField.type(expiry, { delay: 250 });
|
||
|
||
// 清空并重新填写CVC
|
||
logger.info(this.siteName, ' → 清空并重新填写CVC...');
|
||
const cardCvcField = await this.page.$('#cardCvc');
|
||
await cardCvcField.click({ clickCount: 3 });
|
||
await this.page.keyboard.press('Backspace');
|
||
await this.human.randomDelay(300, 500);
|
||
await cardCvcField.type(newCard.cvv, { delay: 250 });
|
||
|
||
logger.success(this.siteName, ' → ✓ 已更新银行卡信息,准备重试...');
|
||
await this.human.randomDelay(2000, 3000);
|
||
|
||
// 继续下一次循环重试
|
||
continue;
|
||
}
|
||
|
||
// 没有错误,等待支付处理完成
|
||
logger.info(this.siteName, ' → 等待支付处理...');
|
||
logger.info(this.siteName, ' → 将持续等待直到支付完成(无时间限制)...');
|
||
|
||
const paymentStartTime = Date.now();
|
||
let paymentComplete = false;
|
||
|
||
while (!paymentComplete) {
|
||
const currentUrl = this.page.url();
|
||
|
||
// 检测是否已经离开 Stripe 页面(支付成功的标志)
|
||
if (!currentUrl.includes('stripe.com') && !currentUrl.includes('checkout.stripe.com')) {
|
||
paymentComplete = true;
|
||
paymentSuccess = true;
|
||
const totalTime = ((Date.now() - paymentStartTime) / 1000).toFixed(1);
|
||
|
||
// 记录注册时间(支付成功的时间)
|
||
this.registrationTime = new Date();
|
||
logger.success(this.siteName, ` → ✓ 支付成功!已离开Stripe页面 (耗时: ${totalTime}秒)`);
|
||
logger.info(this.siteName, ` → 当前页面: ${currentUrl}`);
|
||
logger.info(this.siteName, ` → 注册时间: ${this.registrationTime.toLocaleString('zh-CN')}`);
|
||
break;
|
||
}
|
||
|
||
// 每5秒输出一次进度
|
||
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));
|
||
}
|
||
}
|
||
|
||
// 额外等待页面稳定
|
||
await this.human.randomDelay(2000, 3000);
|
||
|
||
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.info(this.siteName, '');
|
||
logger.info(this.siteName, '┌─────────────────────────────────────────────────────┐');
|
||
logger.info(this.siteName, '│ 订阅信息汇总 │');
|
||
logger.info(this.siteName, '└─────────────────────────────────────────────────────┘');
|
||
|
||
if (this.registrationTime) {
|
||
logger.info(this.siteName, `📅 注册时间: ${this.registrationTime.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
})}`);
|
||
}
|
||
|
||
if (quotaInfo) {
|
||
logger.info(this.siteName, `📊 使用配额: ${quotaInfo.used} / ${quotaInfo.total}`);
|
||
logger.info(this.siteName, `💎 剩余配额: ${quotaInfo.total - parseFloat(quotaInfo.used)}`);
|
||
}
|
||
|
||
if (billingInfo && billingInfo.days) {
|
||
logger.info(this.siteName, `🔄 下次账单: ${billingInfo.days} 天后`);
|
||
logger.info(this.siteName, `📆 账单日期: ${billingInfo.date}`);
|
||
}
|
||
|
||
logger.info(this.siteName, '');
|
||
|
||
// 保存订阅信息供后续使用
|
||
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 {
|
||
// 清除所有浏览器数据(类似 Ctrl+Shift+Delete)
|
||
logger.info(this.siteName, ' → 清除所有浏览器数据(Cookies、Cache、Storage等)...');
|
||
try {
|
||
// 使用 Chrome DevTools Protocol 进行深度清理
|
||
const client = await this.page.target().createCDPSession();
|
||
|
||
// 1. 清除浏览器 Cookies
|
||
await client.send('Network.clearBrowserCookies');
|
||
logger.success(this.siteName, ' → ✓ 已清除所有 Cookies');
|
||
|
||
// 2. 清除浏览器缓存
|
||
await client.send('Network.clearBrowserCache');
|
||
logger.success(this.siteName, ' → ✓ 已清除浏览器缓存');
|
||
|
||
// 3. 清除所有存储数据(localStorage, sessionStorage, IndexedDB, WebSQL, Cache Storage, Service Workers)
|
||
await client.send('Storage.clearDataForOrigin', {
|
||
origin: '*',
|
||
storageTypes: 'all'
|
||
});
|
||
logger.success(this.siteName, ' → ✓ 已清除所有存储数据');
|
||
|
||
// 4. 额外清理:访问目标网站并清除其存储
|
||
await this.page.goto('https://windsurf.com', { waitUntil: 'domcontentloaded' });
|
||
await this.page.evaluate(() => {
|
||
try {
|
||
localStorage.clear();
|
||
sessionStorage.clear();
|
||
} catch (e) {}
|
||
});
|
||
|
||
// 5. 关闭 CDP 会话
|
||
await client.detach();
|
||
|
||
logger.success(this.siteName, ' → ✓ 浏览器数据清除完成(全新状态)');
|
||
|
||
} catch (e) {
|
||
logger.warn(this.siteName, ` → 清除浏览器数据失败: ${e.message}`);
|
||
}
|
||
|
||
// 关闭浏览器
|
||
logger.info(this.siteName, ' → 关闭浏览器...');
|
||
await this.closeBrowser();
|
||
logger.success(this.siteName, ' → ✓ 浏览器已关闭');
|
||
|
||
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;
|