/** * 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 * @param {number} options.waitButtonTimeout - 等待按钮激活的超时时间(毫秒),默认30000 * @param {number} options.waitContentTimeout - 等待内容变化的超时时间(毫秒),默认15000 * @param {string} options.actionName - 操作名称(用于日志) * @returns {Promise} - 是否成功完成 */ 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;