diff --git a/package.json b/package.json index 4c5ddd3..dab435d 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,14 @@ "2captcha-ts": "^2.4.1", "axios": "^1.13.2", "commander": "^11.0.0", + "crawlee": "^3.15.3", "dotenv": "^17.2.3", "imap": "^0.8.19", + "js-yaml": "^4.1.1", "mailparser": "^3.6.5", "mysql2": "^3.6.5", "node-capsolver": "^1.2.0", - "puppeteer": "npm:rebrowser-puppeteer@^23.9.0", + "puppeteer": "npm:rebrowser-puppeteer@^23.10.3", "puppeteer-real-browser": "^1.4.4" }, "engines": { diff --git a/src/automation-framework/actions/click-action.js b/src/automation-framework/actions/click-action.js new file mode 100644 index 0000000..276f6f5 --- /dev/null +++ b/src/automation-framework/actions/click-action.js @@ -0,0 +1,84 @@ +const BaseAction = require('../core/base-action'); +const SmartSelector = require('../core/smart-selector'); + +/** + * 点击动作 + */ +class ClickAction extends BaseAction { + async execute() { + const selector = this.config.selector || this.config.find; + + if (!selector) { + throw new Error('缺少选择器配置'); + } + + this.log('info', '执行点击'); + + // 查找元素 + const smartSelector = SmartSelector.fromConfig(selector, this.page); + const element = await smartSelector.find(this.config.timeout || 10000); + + if (!element) { + throw new Error(`无法找到元素: ${JSON.stringify(selector)}`); + } + + // 滚动到可视区域 + await element.evaluate((el) => { + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }); + + await new Promise(resolve => setTimeout(resolve, 300)); + + // 点击 + await element.click(); + + this.log('debug', '✓ 点击完成'); + + // 等待页面变化(如果配置了) + if (this.config.waitForPageChange) { + await this.waitForPageChange(this.config.checkSelector); + } + + // 可选的等待时间 + if (this.config.waitAfter) { + await new Promise(resolve => setTimeout(resolve, this.config.waitAfter)); + } + + return { success: true }; + } + + /** + * 等待页面内容变化 + */ + async waitForPageChange(checkSelector, timeout = 15000) { + this.log('debug', '等待页面变化...'); + + const startTime = Date.now(); + const initialUrl = this.page.url(); + + while (Date.now() - startTime < timeout) { + // 检查 URL 是否变化 + if (this.page.url() !== initialUrl) { + this.log('debug', '✓ URL 已变化'); + return true; + } + + // 检查特定元素是否出现 + if (checkSelector) { + const smartSelector = SmartSelector.fromConfig(checkSelector, this.page); + const newElement = await smartSelector.find(1000); + if (newElement) { + this.log('debug', '✓ 页面内容已变化'); + return true; + } + } + + await new Promise(resolve => setTimeout(resolve, 500)); + } + + this.log('warn', '等待页面变化超时'); + return false; + } +} + +module.exports = ClickAction; diff --git a/src/automation-framework/actions/custom-action.js b/src/automation-framework/actions/custom-action.js new file mode 100644 index 0000000..b8b3002 --- /dev/null +++ b/src/automation-framework/actions/custom-action.js @@ -0,0 +1,31 @@ +const BaseAction = require('../core/base-action'); + +/** + * 自定义动作 - 调用适配器中的自定义函数 + */ +class CustomAction extends BaseAction { + async execute() { + const handler = this.config.handler; + const params = this.config.params || {}; + + if (!handler) { + throw new Error('缺少处理函数名称'); + } + + this.log('info', `执行自定义函数: ${handler}`); + + // 检查适配器中是否存在该函数 + if (typeof this.context.adapter[handler] !== 'function') { + throw new Error(`自定义处理函数不存在: ${handler}`); + } + + // 调用自定义函数 + const result = await this.context.adapter[handler](params); + + this.log('debug', '✓ 自定义函数执行完成'); + + return result; + } +} + +module.exports = CustomAction; diff --git a/src/automation-framework/actions/fill-form-action.js b/src/automation-framework/actions/fill-form-action.js new file mode 100644 index 0000000..b4da5df --- /dev/null +++ b/src/automation-framework/actions/fill-form-action.js @@ -0,0 +1,145 @@ +const BaseAction = require('../core/base-action'); +const SmartSelector = require('../core/smart-selector'); + +/** + * 填充表单动作 + */ +class FillFormAction extends BaseAction { + async execute() { + const fields = this.config.fields; + const humanLike = this.config.humanLike !== false; // 默认使用人类行为 + + if (!fields || typeof fields !== 'object') { + throw new Error('表单字段配置无效'); + } + + this.log('info', `填写表单,共 ${Object.keys(fields).length} 个字段`); + + // 填写每个字段 + for (const [key, fieldConfig] of Object.entries(fields)) { + await this.fillField(key, fieldConfig, humanLike); + } + + this.log('info', '✓ 表单填写完成'); + + return { success: true }; + } + + /** + * 填写单个字段 + */ + async fillField(key, fieldConfig, humanLike) { + let selector, value; + + // 支持两种配置格式 + if (typeof fieldConfig === 'object' && fieldConfig.find) { + // 完整配置: { find: [...], value: "..." } + selector = fieldConfig.find; + value = this.replaceVariables(fieldConfig.value); + } else { + // 简化配置: { selector: value } + selector = key; + value = this.replaceVariables(fieldConfig); + } + + // 查找元素 + const smartSelector = SmartSelector.fromConfig(selector, this.page); + const element = await smartSelector.find(10000); + + if (!element) { + throw new Error(`无法找到字段: ${JSON.stringify(selector)}`); + } + + this.log('debug', ` → 填写字段: ${key}`); + + // 清空字段(增强清空逻辑,支持 Stripe 等复杂表单) + await element.click({ clickCount: 3 }); + await new Promise(resolve => setTimeout(resolve, 100)); + + // 多次 Backspace 确保彻底清空 + const clearTimes = fieldConfig.clearTimes || this.config.clearTimes || 25; + for (let i = 0; i < clearTimes; i++) { + await this.page.keyboard.press('Backspace'); + } + await new Promise(resolve => setTimeout(resolve, 200)); + + if (humanLike) { + // 人类行为模拟 + await this.typeHumanLike(element, value); + } else { + // 直接输入 + await element.type(value, { delay: 100 }); + } + + // 触发事件 + await this.page.evaluate((el) => { + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + }, element); + } + + /** + * 模拟人类输入 + */ + async typeHumanLike(element, text) { + for (const char of text) { + await element.type(char, { + delay: Math.random() * 100 + 50 // 50-150ms 随机延迟 + }); + } + + // 随机暂停 + if (Math.random() > 0.7) { + await new Promise(resolve => setTimeout(resolve, Math.random() * 500 + 200)); + } + } + + /** + * 提交表单 + */ + async submitForm(submitConfig) { + this.log('info', ' → 提交表单'); + + const selector = submitConfig.find || submitConfig; + const smartSelector = SmartSelector.fromConfig(selector, this.page); + const button = await smartSelector.find(10000); + + if (!button) { + throw new Error(`无法找到提交按钮: ${JSON.stringify(selector)}`); + } + + // 等待按钮可点击 + await this.waitForButtonEnabled(button); + + // 点击 + await button.click(); + + // 等待提交后的延迟 + if (submitConfig.waitAfter) { + await new Promise(resolve => setTimeout(resolve, submitConfig.waitAfter)); + } + } + + /** + * 等待按钮启用 + */ + async waitForButtonEnabled(button, timeout = 30000) { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + const isEnabled = await this.page.evaluate((btn) => { + return !btn.disabled; + }, button); + + if (isEnabled) { + return; + } + + await new Promise(resolve => setTimeout(resolve, 500)); + } + + throw new Error('按钮未启用(超时)'); + } +} + +module.exports = FillFormAction; diff --git a/src/automation-framework/actions/navigate-action.js b/src/automation-framework/actions/navigate-action.js new file mode 100644 index 0000000..5021aef --- /dev/null +++ b/src/automation-framework/actions/navigate-action.js @@ -0,0 +1,29 @@ +const BaseAction = require('../core/base-action'); + +/** + * 导航动作 - 打开页面 + */ +class NavigateAction extends BaseAction { + async execute() { + const url = this.replaceVariables(this.config.url); + const options = this.config.options || { + waitUntil: 'networkidle2', + timeout: 30000 + }; + + this.log('info', `导航到: ${url}`); + + await this.page.goto(url, options); + + // 可选的等待时间 + if (this.config.waitAfter) { + await new Promise(resolve => setTimeout(resolve, this.config.waitAfter)); + } + + this.log('info', `✓ 页面加载完成`); + + return { success: true, url }; + } +} + +module.exports = NavigateAction; diff --git a/src/automation-framework/actions/retry-block-action.js b/src/automation-framework/actions/retry-block-action.js new file mode 100644 index 0000000..f8b0216 --- /dev/null +++ b/src/automation-framework/actions/retry-block-action.js @@ -0,0 +1,147 @@ +const BaseAction = require('./base-action'); +const logger = require('../../tools/account-register/utils/logger'); + +/** + * 重试块动作 - 将一组步骤作为整体进行重试 + * + * 配置示例: + * - action: retryBlock + * name: "支付流程" + * maxRetries: 5 + * retryDelay: 2000 + * onRetryBefore: + * - action: custom + * handler: "regenerateCard" + * steps: + * - action: fillForm + * fields: {...} + * - action: click + * selector: {...} + */ +class RetryBlockAction extends BaseAction { + async execute() { + const { + steps = [], + maxRetries = 3, + retryDelay = 1000, + onRetryBefore = [], + onRetryAfter = [] + } = this.config; + + const blockName = this.config.name || 'RetryBlock'; + + if (!steps || steps.length === 0) { + throw new Error('RetryBlock 必须包含至少一个步骤'); + } + + let lastError = null; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + if (attempt > 0) { + this.log('info', `${blockName} - 第 ${attempt + 1} 次重试...`); + + // 执行重试前的钩子 + if (onRetryBefore.length > 0) { + this.log('debug', '执行重试前钩子...'); + await this.executeHooks(onRetryBefore); + } + + // 延迟 + if (retryDelay > 0) { + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + } + + // 执行步骤块 + this.log('debug', `执行 ${steps.length} 个步骤...`); + await this.executeSteps(steps); + + // 执行成功后的钩子(仅首次成功时) + if (attempt > 0 && onRetryAfter.length > 0) { + this.log('debug', '执行重试后钩子...'); + await this.executeHooks(onRetryAfter); + } + + // 成功,跳出循环 + if (attempt > 0) { + this.log('success', `✓ ${blockName} 在第 ${attempt + 1} 次尝试后成功`); + } + + return { success: true, attempts: attempt + 1 }; + + } catch (error) { + lastError = error; + + if (attempt < maxRetries) { + this.log('warn', `${blockName} 执行失败: ${error.message}`); + this.log('info', `准备重试 (${attempt + 1}/${maxRetries})...`); + } else { + this.log('error', `${blockName} 在 ${maxRetries + 1} 次尝试后仍然失败`); + } + } + } + + // 所有重试都失败 + throw new Error(`${blockName} 失败: ${lastError.message}`); + } + + /** + * 执行钩子函数 + */ + async executeHooks(hooks) { + for (const hookConfig of hooks) { + await this.executeStep(hookConfig); + } + } + + /** + * 执行步骤列表 + */ + async executeSteps(steps) { + for (const stepConfig of steps) { + await this.executeStep(stepConfig); + } + } + + /** + * 执行单个步骤 + */ + async executeStep(stepConfig) { + const actionType = stepConfig.action; + + // 动态加载对应的 Action + const ActionClass = this.getActionClass(actionType); + + const action = new ActionClass( + this.page, + stepConfig, + this.context + ); + + return await action.execute(); + } + + /** + * 根据 action 类型获取 Action 类 + */ + getActionClass(actionType) { + const actionMap = { + navigate: require('./navigate-action'), + fillForm: require('./fill-form-action'), + click: require('./click-action'), + wait: require('./wait-action'), + custom: require('./custom-action') + }; + + const ActionClass = actionMap[actionType]; + + if (!ActionClass) { + throw new Error(`未知的 action 类型: ${actionType}`); + } + + return ActionClass; + } +} + +module.exports = RetryBlockAction; diff --git a/src/automation-framework/actions/wait-action.js b/src/automation-framework/actions/wait-action.js new file mode 100644 index 0000000..39f4853 --- /dev/null +++ b/src/automation-framework/actions/wait-action.js @@ -0,0 +1,112 @@ +const BaseAction = require('../core/base-action'); +const SmartSelector = require('../core/smart-selector'); + +/** + * 等待动作 + */ +class WaitAction extends BaseAction { + async execute() { + const type = this.config.type || 'delay'; + + switch (type) { + case 'delay': + return await this.waitDelay(); + + case 'element': + return await this.waitForElement(); + + case 'navigation': + return await this.waitForNavigation(); + + case 'condition': + return await this.waitForCondition(); + + default: + throw new Error(`未知的等待类型: ${type}`); + } + } + + /** + * 固定延迟 + */ + async waitDelay() { + const duration = this.config.duration || this.config.ms || 1000; + this.log('debug', `等待 ${duration}ms`); + await new Promise(resolve => setTimeout(resolve, duration)); + return { success: true }; + } + + /** + * 等待元素出现 + */ + async waitForElement() { + const selector = this.config.selector || this.config.find; + const timeout = this.config.timeout || 10000; + + if (!selector) { + throw new Error('缺少选择器配置'); + } + + this.log('debug', '等待元素出现'); + + const smartSelector = SmartSelector.fromConfig(selector, this.page); + const element = await smartSelector.find(timeout); + + if (!element) { + throw new Error(`元素未出现(超时 ${timeout}ms)`); + } + + this.log('debug', '✓ 元素已出现'); + return { success: true }; + } + + /** + * 等待页面导航 + */ + async waitForNavigation() { + const timeout = this.config.timeout || 30000; + + this.log('debug', '等待页面导航'); + + await this.page.waitForNavigation({ + waitUntil: this.config.waitUntil || 'networkidle2', + timeout + }); + + this.log('debug', '✓ 导航完成'); + return { success: true }; + } + + /** + * 等待自定义条件 + */ + async waitForCondition() { + const handler = this.config.handler; + const timeout = this.config.timeout || 10000; + + if (!handler) { + throw new Error('缺少条件处理函数'); + } + + this.log('debug', '等待自定义条件'); + + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + // 调用适配器中的条件判断函数 + if (typeof this.context.adapter[handler] === 'function') { + const result = await this.context.adapter[handler](); + if (result) { + this.log('debug', '✓ 条件满足'); + return { success: true }; + } + } + + await new Promise(resolve => setTimeout(resolve, 500)); + } + + throw new Error('条件未满足(超时)'); + } +} + +module.exports = WaitAction; diff --git a/src/automation-framework/configs/sites/windsurf.yaml b/src/automation-framework/configs/sites/windsurf.yaml new file mode 100644 index 0000000..232827d --- /dev/null +++ b/src/automation-framework/configs/sites/windsurf.yaml @@ -0,0 +1,203 @@ +# Windsurf 注册自动化配置 +site: + name: Windsurf + url: https://windsurf.com/account/register + +# 工作流定义 +workflow: + # ==================== 步骤 1: 填写基本信息 ==================== + - action: navigate + name: "打开注册页面" + url: "{{site.url}}" + options: + waitUntil: networkidle2 + timeout: 30000 + waitAfter: 2000 + + - action: fillForm + name: "填写基本信息" + humanLike: true + fields: + firstName: + find: + - css: '#firstName' + - name: 'firstName' + value: "{{account.firstName}}" + + lastName: + find: + - css: '#lastName' + - name: 'lastName' + value: "{{account.lastName}}" + + email: + find: + - css: '#email' + - type: 'email' + value: "{{account.email}}" + + - action: click + name: "勾选同意条款" + selector: + - css: 'input[type="checkbox"]' + optional: true + waitAfter: 500 + + - action: click + name: "点击 Continue (基本信息)" + selector: + - css: 'button[type="submit"]' + - text: 'Continue' + waitAfter: 2000 + + # ==================== 步骤 2: 设置密码 ==================== + - action: wait + name: "等待密码页面" + type: element + find: + - css: '#password' + timeout: 15000 + + - action: fillForm + name: "设置密码" + humanLike: true + fields: + password: + find: + - css: 'input[type="password"]' + - placeholder: 'Password' + value: "{{account.password}}" + + passwordConfirm: + find: + - css: 'input[placeholder*="confirmation"]' + - css: 'input[placeholder*="Confirm"]' + value: "{{account.password}}" + + - action: click + name: "提交密码" + selector: + - css: 'button[type="submit"]' + - text: 'Continue' + waitAfter: 2000 + + # ==================== 步骤 2.5: Cloudflare Turnstile 验证 ==================== + - action: custom + name: "Cloudflare Turnstile 验证" + handler: "handleTurnstile" + params: + timeout: 30000 + optional: true + + # ==================== 步骤 3: 邮箱验证 ==================== + # 需要自定义处理:获取邮件验证码 + 填写6个输入框 + - action: custom + name: "邮箱验证" + handler: "handleEmailVerification" + params: + timeout: 120000 + + # ==================== 步骤 4: 跳过问卷调查 ==================== + - action: click + name: "跳过问卷" + selector: + - text: "Skip this step" + - text: "skip" + waitAfter: 2000 + + # ==================== 步骤 5: 选择计划 ==================== + - action: click + name: "选择计划" + selector: + - text: "Select plan" + - text: "Continue" + - text: "Get started" + waitAfter: 2000 + + # ==================== 步骤 6: 填写支付信息(带重试) ==================== + - action: retryBlock + name: "支付流程" + maxRetries: 5 + retryDelay: 2000 + onRetryBefore: + # 重试前重新生成银行卡 + - action: custom + handler: "regenerateCard" + + steps: + # 6.1 选择银行卡支付方式 + - action: click + name: "选择银行卡支付" + selector: + - css: 'input[type="radio"][value="card"]' + waitAfter: 3000 + + # 6.2 等待支付表单加载 + - action: wait + name: "等待支付表单" + type: element + find: + - css: '#cardNumber' + timeout: 30000 + + # 6.3 填写银行卡信息(使用重新生成的卡号) + - action: fillForm + name: "填写银行卡信息" + humanLike: false + fields: + cardNumber: + find: + - css: '#cardNumber' + value: "{{card.number}}" + + cardExpiry: + find: + - css: '#cardExpiry' + value: "{{card.month}}{{card.year}}" + + cardCvc: + find: + - css: '#cardCvc' + value: "{{card.cvv}}" + + billingName: + find: + - css: '#billingName' + value: "{{account.firstName}} {{account.lastName}}" + + # 6.4 选择澳门地址(动态字段,需要自定义处理) + - action: custom + name: "选择澳门地址" + handler: "selectBillingAddress" + + # 6.5 处理 hCaptcha + - action: custom + name: "hCaptcha 验证" + handler: "handleHCaptcha" + params: + timeout: 120000 + + # 6.6 提交支付并检查结果(失败会触发重试) + - action: custom + name: "提交并验证支付" + handler: "submitAndVerifyPayment" + + # ==================== 步骤 7: 获取订阅信息 ==================== + - action: custom + name: "获取订阅信息" + handler: "getSubscriptionInfo" + optional: true + + # ==================== 步骤 8: 保存到数据库 ==================== + - action: custom + name: "保存到数据库" + handler: "saveToDatabase" + optional: true + +# 错误处理配置 +errorHandling: + screenshot: true + retry: + enabled: true + maxAttempts: 3 + delay: 2000 diff --git a/src/automation-framework/core/action-registry.js b/src/automation-framework/core/action-registry.js new file mode 100644 index 0000000..3b3233c --- /dev/null +++ b/src/automation-framework/core/action-registry.js @@ -0,0 +1,45 @@ +/** + * 动作注册表 - 管理所有可用的动作类型 + */ +class ActionRegistry { + constructor() { + this.actions = new Map(); + } + + /** + * 注册动作 + * @param {string} name - 动作名称 + * @param {Class} ActionClass - 动作类 + */ + register(name, ActionClass) { + this.actions.set(name, ActionClass); + } + + /** + * 获取动作类 + * @param {string} name - 动作名称 + * @returns {Class|null} + */ + get(name) { + return this.actions.get(name) || null; + } + + /** + * 检查动作是否存在 + * @param {string} name - 动作名称 + * @returns {boolean} + */ + has(name) { + return this.actions.has(name); + } + + /** + * 获取所有已注册的动作名称 + * @returns {string[]} + */ + list() { + return Array.from(this.actions.keys()); + } +} + +module.exports = ActionRegistry; diff --git a/src/automation-framework/core/base-action.js b/src/automation-framework/core/base-action.js new file mode 100644 index 0000000..d8e667b --- /dev/null +++ b/src/automation-framework/core/base-action.js @@ -0,0 +1,55 @@ +/** + * 动作基类 + */ +class BaseAction { + constructor(context, config) { + this.context = context; + this.config = config; + this.page = context.page; + this.logger = context.logger; + } + + /** + * 执行动作(子类必须实现) + */ + async execute() { + throw new Error('子类必须实现 execute 方法'); + } + + /** + * 替换配置中的变量 + * @param {string} value - 包含变量的字符串 (如 "{{account.email}}") + * @returns {string} - 替换后的值 + */ + replaceVariables(value) { + if (typeof value !== 'string') return value; + + return value.replace(/\{\{(.+?)\}\}/g, (match, path) => { + const keys = path.trim().split('.'); + let result = this.context.data; + + for (const key of keys) { + if (result && typeof result === 'object') { + result = result[key]; + } else { + return match; // 无法解析,返回原始值 + } + } + + return result !== undefined ? result : match; + }); + } + + /** + * 记录日志 + */ + log(level, message) { + if (this.logger && this.logger[level]) { + this.logger[level](this.context.siteName || 'Automation', message); + } else { + console.log(`[${level.toUpperCase()}] ${message}`); + } + } +} + +module.exports = BaseAction; diff --git a/src/automation-framework/core/site-adapter.js b/src/automation-framework/core/site-adapter.js new file mode 100644 index 0000000..cc5dbeb --- /dev/null +++ b/src/automation-framework/core/site-adapter.js @@ -0,0 +1,123 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const WorkflowEngine = require('./workflow-engine'); + +/** + * 站点适配器基类 - 所有网站的基类 + */ +class SiteAdapter { + constructor(context, siteName) { + this.context = context; + this.siteName = siteName; + this.useConfig = true; // 默认使用配置文件 + + // 快捷访问 + this.page = context.page; + this.logger = context.logger; + + // 加载配置 + if (this.useConfig) { + this.config = this.loadConfig(siteName); + this.context.siteName = this.config.site?.name || siteName; + } + } + + /** + * 加载站点配置 + * @param {string} siteName - 站点名称 + * @returns {Object} + */ + loadConfig(siteName) { + const configPath = path.join(__dirname, '../configs/sites', `${siteName}.yaml`); + + if (!fs.existsSync(configPath)) { + throw new Error(`配置文件不存在: ${configPath}`); + } + + const configContent = fs.readFileSync(configPath, 'utf8'); + return yaml.load(configContent); + } + + /** + * 生命周期钩子 - 工作流执行前 + */ + async beforeWorkflow() { + this.log('debug', '执行 beforeWorkflow 钩子'); + // 子类可以重写 + } + + /** + * 生命周期钩子 - 工作流执行后 + */ + async afterWorkflow() { + this.log('debug', '执行 afterWorkflow 钩子'); + // 子类可以重写 + } + + /** + * 生命周期钩子 - 错误处理 + * @param {Error} error - 错误对象 + */ + async onError(error) { + this.log('error', `工作流执行失败: ${error.message}`); + + // 截图 + if (this.page) { + try { + const screenshotPath = path.join( + __dirname, + '../../logs', + `error-${Date.now()}.png` + ); + await this.page.screenshot({ path: screenshotPath, fullPage: true }); + this.log('info', `错误截图已保存: ${screenshotPath}`); + } catch (e) { + this.log('warn', `截图失败: ${e.message}`); + } + } + + // 子类可以重写 + } + + /** + * 执行入口 + * @returns {Promise} + */ + async execute() { + try { + // 执行前钩子 + await this.beforeWorkflow(); + + // 创建工作流引擎 + const engine = new WorkflowEngine(this.context, this.config); + this.context.engine = engine; + this.context.adapter = this; + + // 执行工作流 + const result = await engine.execute(); + + // 执行后钩子 + await this.afterWorkflow(); + + return result; + + } catch (error) { + await this.onError(error); + throw error; + } + } + + /** + * 记录日志 + */ + log(level, message) { + if (this.logger && this.logger[level]) { + this.logger[level](this.siteName, message); + } else { + console.log(`[${level.toUpperCase()}] ${message}`); + } + } +} + +module.exports = SiteAdapter; diff --git a/src/automation-framework/core/smart-selector.js b/src/automation-framework/core/smart-selector.js new file mode 100644 index 0000000..65beb8a --- /dev/null +++ b/src/automation-framework/core/smart-selector.js @@ -0,0 +1,184 @@ +/** + * 智能选择器 - 支持多策略元素查找 + */ +class SmartSelector { + constructor(page) { + this.page = page; + this.strategies = []; + } + + /** + * 从配置构建选择器 + * @param {Object|Array|string} config - 选择器配置 + * @param {Object} page - Puppeteer page 对象 + * @returns {SmartSelector} + */ + static fromConfig(config, page) { + const selector = new SmartSelector(page); + + if (typeof config === 'string') { + // 简单 CSS 选择器 + selector.css(config); + } else if (Array.isArray(config)) { + // 多策略 + config.forEach(strategy => { + if (strategy.css) selector.css(strategy.css); + if (strategy.xpath) selector.xpath(strategy.xpath); + if (strategy.text) selector.text(strategy.text); + if (strategy.placeholder) selector.placeholder(strategy.placeholder); + if (strategy.label) selector.label(strategy.label); + if (strategy.type) selector.type(strategy.type); + if (strategy.role) selector.role(strategy.role); + if (strategy.testid) selector.testid(strategy.testid); + if (strategy.name) selector.name(strategy.name); + }); + } else if (typeof config === 'object') { + // 单个策略对象 + if (config.css) selector.css(config.css); + if (config.xpath) selector.xpath(config.xpath); + if (config.text) selector.text(config.text); + if (config.placeholder) selector.placeholder(config.placeholder); + if (config.label) selector.label(config.label); + if (config.type) selector.type(config.type); + if (config.role) selector.role(config.role); + if (config.testid) selector.testid(config.testid); + if (config.name) selector.name(config.name); + } + + return selector; + } + + css(selector) { + this.strategies.push({ + type: 'css', + find: async () => await this.page.$(selector) + }); + return this; + } + + xpath(xpath) { + this.strategies.push({ + type: 'xpath', + find: async () => { + const elements = await this.page.$x(xpath); + return elements[0] || null; + } + }); + return this; + } + + text(text) { + this.strategies.push({ + type: 'text', + find: async () => { + return await this.page.evaluateHandle((searchText) => { + const walker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_TEXT, + null, + false + ); + + let node; + while (node = walker.nextNode()) { + if (node.textContent.trim() === searchText.trim()) { + return node.parentElement; + } + } + return null; + }, text); + } + }); + return this; + } + + placeholder(placeholder) { + this.strategies.push({ + type: 'placeholder', + find: async () => await this.page.$(`[placeholder="${placeholder}"]`) + }); + return this; + } + + label(labelText) { + this.strategies.push({ + type: 'label', + find: async () => { + return await this.page.evaluateHandle((text) => { + const labels = Array.from(document.querySelectorAll('label')); + const label = labels.find(l => l.textContent.trim() === text.trim()); + if (label && label.htmlFor) { + return document.getElementById(label.htmlFor); + } + return null; + }, labelText); + } + }); + return this; + } + + type(inputType) { + this.strategies.push({ + type: 'type', + find: async () => await this.page.$(`input[type="${inputType}"]`) + }); + return this; + } + + role(role) { + this.strategies.push({ + type: 'role', + find: async () => await this.page.$(`[role="${role}"]`) + }); + return this; + } + + testid(testid) { + this.strategies.push({ + type: 'testid', + find: async () => await this.page.$(`[data-testid="${testid}"]`) + }); + return this; + } + + name(name) { + this.strategies.push({ + type: 'name', + find: async () => await this.page.$(`[name="${name}"]`) + }); + return this; + } + + /** + * 查找元素(尝试所有策略) + * @param {number} timeout - 超时时间(毫秒) + * @returns {Promise} + */ + async find(timeout = 10000) { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + for (const strategy of this.strategies) { + try { + const element = await strategy.find(); + if (element && element.asElement && element.asElement()) { + return element.asElement(); + } + if (element) { + return element; + } + } catch (error) { + // 继续尝试下一个策略 + continue; + } + } + + // 等待一小段时间再重试 + await new Promise(resolve => setTimeout(resolve, 500)); + } + + return null; + } +} + +module.exports = SmartSelector; diff --git a/src/automation-framework/core/workflow-engine.js b/src/automation-framework/core/workflow-engine.js new file mode 100644 index 0000000..fd5c4c7 --- /dev/null +++ b/src/automation-framework/core/workflow-engine.js @@ -0,0 +1,161 @@ +const ActionRegistry = require('./action-registry'); + +// 导入内置动作 +const NavigateAction = require('../actions/navigate-action'); +const FillFormAction = require('../actions/fill-form-action'); +const ClickAction = require('../actions/click-action'); +const WaitAction = require('../actions/wait-action'); +const CustomAction = require('../actions/custom-action'); +const RetryBlockAction = require('../actions/retry-block-action'); + +/** + * 工作流引擎 - 执行配置驱动的自动化流程 + */ +class WorkflowEngine { + constructor(context, config) { + this.context = context; + this.config = config; + this.actionRegistry = new ActionRegistry(); + this.currentStep = 0; + + // 注册内置动作 + this.registerBuiltinActions(); + } + + /** + * 注册内置动作 + */ + registerBuiltinActions() { + this.actionRegistry.register('navigate', NavigateAction); + this.actionRegistry.register('fillForm', FillFormAction); + this.actionRegistry.register('click', ClickAction); + this.actionRegistry.register('wait', WaitAction); + this.actionRegistry.register('custom', CustomAction); + this.actionRegistry.register('retryBlock', RetryBlockAction); + } + + /** + * 执行工作流 + * @returns {Promise} - 执行结果 + */ + async execute() { + // 检查是否使用配置 + if (this.context.adapter && this.context.adapter.useConfig === false) { + // 完全自定义模式 + this.log('info', '使用完全自定义模式'); + return await this.context.adapter.execute(); + } + + // 配置驱动模式 + const workflow = this.config.workflow; + + if (!workflow || workflow.length === 0) { + throw new Error('工作流配置为空'); + } + + this.log('info', `开始执行工作流,共 ${workflow.length} 个步骤`); + + return await this.executeActions(workflow); + } + + /** + * 执行动作列表 + * @param {Array} actions - 动作配置数组 + * @returns {Promise} + */ + async executeActions(actions) { + const results = []; + + for (let i = 0; i < actions.length; i++) { + const actionConfig = actions[i]; + this.currentStep = i + 1; + + const stepName = actionConfig.name || `步骤 ${this.currentStep}`; + this.log('info', `[${this.currentStep}/${actions.length}] ${stepName}`); + + try { + const result = await this.executeAction(actionConfig); + results.push({ step: i, success: true, result }); + + } catch (error) { + this.log('error', `步骤失败: ${error.message}`); + + // 检查是否是可选步骤 + if (actionConfig.optional) { + this.log('warn', '可选步骤失败,继续执行'); + results.push({ step: i, success: false, error: error.message, optional: true }); + continue; + } + + // 检查重试配置 + if (actionConfig.retry) { + const success = await this.retryAction(actionConfig); + if (success) { + results.push({ step: i, success: true, retried: true }); + continue; + } + } + + // 不可恢复的错误 + throw error; + } + } + + this.log('info', '工作流执行完成'); + return { success: true, results }; + } + + /** + * 执行单个动作 + * @param {Object} config - 动作配置 + * @returns {Promise} + */ + async executeAction(config) { + const ActionClass = this.actionRegistry.get(config.action); + + if (!ActionClass) { + throw new Error(`未知动作类型: ${config.action}`); + } + + const action = new ActionClass(this.context, config); + return await action.execute(); + } + + /** + * 重试动作 + * @param {Object} config - 动作配置 + * @returns {Promise} + */ + async retryAction(config) { + const maxAttempts = config.retry.maxAttempts || 3; + const delay = config.retry.delay || 2000; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + this.log('warn', `重试第 ${attempt}/${maxAttempts} 次...`); + + try { + await new Promise(resolve => setTimeout(resolve, delay)); + await this.executeAction(config); + this.log('info', '重试成功'); + return true; + } catch (error) { + this.log('error', `重试失败: ${error.message}`); + } + } + + return false; + } + + /** + * 记录日志 + */ + log(level, message) { + if (this.context.logger && this.context.logger[level]) { + this.context.logger[level](this.context.siteName || 'WorkflowEngine', message); + } else { + console.log(`[${level.toUpperCase()}] ${message}`); + } + } +} + +module.exports = WorkflowEngine; diff --git a/src/automation-framework/index.js b/src/automation-framework/index.js new file mode 100644 index 0000000..2d65b6d --- /dev/null +++ b/src/automation-framework/index.js @@ -0,0 +1,85 @@ +const WindsurfAdapter = require('./sites/windsurf-adapter'); +const logger = require('../shared/logger'); +const BrowserManager = require('../tools/account-register/utils/browser-manager'); + +/** + * 自动化工厂 - 统一入口 + */ +class AutomationFactory { + /** + * 使用 AdsPower 注册(主要方法) + */ + static async registerWithAdsPower(siteName, adspowerUserId) { + logger.info('AutomationFactory', `========================================`); + logger.info('AutomationFactory', `开始 ${siteName} 注册流程`); + logger.info('AutomationFactory', `AdsPower Profile: ${adspowerUserId}`); + logger.info('AutomationFactory', `========================================`); + + const browserManager = new BrowserManager({ + profileId: adspowerUserId, + siteName: 'AutomationFactory' + }); + + try { + // 1. 启动并连接到 AdsPower 浏览器 + await browserManager.launch(); + + const browser = browserManager.browser; + const page = browserManager.page; + + // 4. 创建上下文 + const context = { + page, + browser, + logger, + data: {}, + adspowerUserId + }; + + // 5. 创建适配器 + let adapter; + + switch (siteName.toLowerCase()) { + case 'windsurf': + adapter = new WindsurfAdapter(context); + break; + + default: + throw new Error(`不支持的站点: ${siteName}`); + } + + // 6. 执行自动化流程 + logger.info('AutomationFactory', '开始执行自动化流程...'); + const result = await adapter.execute(); + + // 7. 返回结果 + return { + success: true, + siteName, + accountData: context.data.account, + cardInfo: context.data.card, + result + }; + + } catch (error) { + logger.error('AutomationFactory', `注册失败: ${error.message}`); + console.error(error); + + return { + success: false, + error: error.message + }; + + } finally { + // 8. 关闭浏览器 + try { + await browserManager.close(); + } catch (e) { + logger.warn('AutomationFactory', `关闭浏览器失败: ${e.message}`); + } + } + } +} + + +module.exports = AutomationFactory; diff --git a/src/automation-framework/sites/windsurf-adapter.js b/src/automation-framework/sites/windsurf-adapter.js new file mode 100644 index 0000000..e1fc39a --- /dev/null +++ b/src/automation-framework/sites/windsurf-adapter.js @@ -0,0 +1,402 @@ +const SiteAdapter = require('../core/site-adapter'); +const AccountDataGenerator = require('../../tools/account-register/generator'); +const CardGenerator = require('../../tools/card-generator/generator'); + +/** + * Windsurf 站点适配器 + */ +class WindsurfAdapter extends SiteAdapter { + constructor(context) { + super(context, 'windsurf'); + + // 数据生成器 + this.dataGen = new AccountDataGenerator(); + this.cardGen = new CardGenerator(); + } + + /** + * 工作流执行前 - 生成账户数据 + */ + async beforeWorkflow() { + await super.beforeWorkflow(); + + this.log('info', '生成账户数据...'); + + // 生成账户数据 + const accountData = this.dataGen.generateAccount(); + const cardInfo = this.cardGen.generate(); + + // 存储到上下文 + this.context.data = { + site: this.config.site, + account: accountData, + card: cardInfo + }; + + this.log('info', `账户邮箱: ${accountData.email}`); + this.log('info', `卡号: ${cardInfo.number}`); + } + + /** + * 工作流执行后 - 保存结果 + */ + async afterWorkflow() { + await super.afterWorkflow(); + + this.log('info', '注册流程完成'); + + // 这里可以保存到数据库 + // await this.saveToDatabase(); + } + + /** + * 步骤 2.5: Cloudflare Turnstile 验证 + */ + async handleTurnstile(params) { + const { timeout = 30000 } = params; + + this.log('info', 'Cloudflare Turnstile 人机验证'); + + try { + // 等待 Turnstile 验证框出现 + await new Promise(resolve => setTimeout(resolve, 2000)); + + // 检查是否有 Turnstile + const hasTurnstile = await this.page.evaluate(() => { + return !!document.querySelector('iframe[src*="challenges.cloudflare.com"]') || + !!document.querySelector('.cf-turnstile') || + document.body.textContent.includes('Please verify that you are human'); + }); + + if (hasTurnstile) { + this.log('info', '检测到 Turnstile 验证,等待自动完成...'); + + // 等待验证通过(检查按钮是否启用或页面是否变化) + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + const isPassed = await this.page.evaluate(() => { + // 检查是否有成功标记 + const successMark = document.querySelector('svg[data-status="success"]') || + document.querySelector('[aria-label*="success"]') || + document.querySelector('.cf-turnstile-success'); + + // 或者检查 Continue 按钮是否启用 + const continueBtn = Array.from(document.querySelectorAll('button')).find(btn => + btn.textContent.trim() === 'Continue' + ); + const btnEnabled = continueBtn && !continueBtn.disabled; + + return !!successMark || btnEnabled; + }); + + if (isPassed) { + this.log('success', '✓ Turnstile 验证通过'); + + // 点击 Continue 按钮 + const continueBtn = await this.page.evaluateHandle(() => { + return Array.from(document.querySelectorAll('button')).find(btn => + btn.textContent.trim() === 'Continue' + ); + }); + + if (continueBtn) { + await continueBtn.asElement().click(); + this.log('info', '已点击 Continue 按钮'); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + return { success: true }; + } + + await new Promise(resolve => setTimeout(resolve, 500)); + } + + throw new Error('Turnstile 验证超时'); + + } else { + this.log('info', '未检测到 Turnstile,跳过'); + return { success: true, skipped: true }; + } + + } catch (error) { + this.log('warn', `Turnstile 处理失败: ${error.message}`); + // Turnstile 是可选的,失败也继续 + return { success: true, error: error.message }; + } + } + + /** + * 步骤 3: 邮箱验证 + */ + async handleEmailVerification(params) { + const { timeout = 120000 } = params; + + this.log('info', '开始邮箱验证'); + + // 导入邮箱服务 + const EmailVerificationService = require('../../tools/account-register/email-verification'); + if (!this.emailService) { + this.emailService = new EmailVerificationService(); + } + + try { + // 等待2秒让邮件到达 + await new Promise(resolve => setTimeout(resolve, 2000)); + + // 获取验证码 + this.log('info', `从邮箱获取验证码: ${this.context.data.account.email}`); + const code = await this.emailService.getVerificationCode( + 'windsurf', + this.context.data.account.email, + timeout / 1000 + ); + + this.log('success', `✓ 验证码: ${code}`); + + // 等待输入框出现 + await this.page.waitForSelector('input[type="text"]', { timeout: 10000 }); + + // 获取所有输入框 + const inputs = await this.page.$$('input[type="text"]'); + this.log('info', `找到 ${inputs.length} 个输入框`); + + if (inputs.length >= 6 && code.length === 6) { + // 填写6位验证码 + this.log('info', '填写6位验证码...'); + for (let i = 0; i < 6; i++) { + await inputs[i].click(); + await new Promise(resolve => setTimeout(resolve, 100)); + await inputs[i].type(code[i].toUpperCase()); + await new Promise(resolve => setTimeout(resolve, 300)); + } + + this.log('success', '✓ 验证码已填写'); + + // 等待跳转到问卷页面 + this.log('info', '等待页面跳转...'); + const startTime = Date.now(); + + while (Date.now() - startTime < 60000) { + const currentUrl = this.page.url(); + + if (currentUrl.includes('/account/onboarding') && currentUrl.includes('page=source')) { + this.log('success', '✓ 邮箱验证成功'); + return { success: true }; + } + + await new Promise(resolve => setTimeout(resolve, 500)); + } + + throw new Error('等待页面跳转超时'); + } else { + throw new Error('输入框数量不正确'); + } + + } catch (error) { + this.log('error', `邮箱验证失败: ${error.message}`); + throw error; + } + } + + /** + * 重新生成银行卡(用于重试) + */ + regenerateCard() { + const newCard = this.cardGen.generate(); + this.context.data.card = newCard; + this.log('info', `重新生成卡号: ${newCard.number}`); + return newCard; + } + + /** + * 选择澳门地址(处理动态地址字段) + */ + async selectBillingAddress(params) { + // 选择国家/地区 + await this.page.select('#billingCountry', 'MO'); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // 填写动态地址字段 + const addressFields = await this.page.$$('input[placeholder*="地址"]'); + if (addressFields.length > 0) { + await addressFields[0].type('Macau', { delay: 100 }); + if (addressFields[1]) { + await new Promise(resolve => setTimeout(resolve, 300)); + await addressFields[1].type('Macao', { delay: 100 }); + } + } + + return { success: true }; + } + + /** + * 处理 hCaptcha + */ + async handleHCaptcha() { + this.log('info', '检查 hCaptcha...'); + + await new Promise(resolve => setTimeout(resolve, 2000)); + + // 检查是否有 hCaptcha + const hasCaptcha = await this.page.evaluate(() => { + const stripeFrame = document.querySelector('iframe[src*="hcaptcha-inner"]'); + const hcaptchaDiv = document.querySelector('.h-captcha'); + const hcaptchaFrame = document.querySelector('iframe[src*="hcaptcha.com"]'); + return !!(stripeFrame || hcaptchaDiv || hcaptchaFrame); + }); + + if (hasCaptcha) { + this.log('info', '检测到 hCaptcha,等待自动完成...'); + + // 等待验证完成(检查 token) + const startTime = Date.now(); + const maxWaitTime = 120000; // 最多120秒 + + while (Date.now() - startTime < maxWaitTime) { + // 检查页面是否跳转(支付成功) + const currentUrl = this.page.url(); + if (!currentUrl.includes('stripe.com') && !currentUrl.includes('checkout.stripe.com')) { + this.log('success', '✓ 支付成功,页面已跳转'); + return; + } + + // 检查 token 是否填充 + const verified = await this.page.evaluate(() => { + const response = document.querySelector('[name="h-captcha-response"]') || + document.querySelector('[name="g-recaptcha-response"]'); + return response && response.value && response.value.length > 20; + }); + + if (verified) { + this.log('success', '✓ hCaptcha 验证完成'); + return; + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + throw new Error('hCaptcha 验证超时'); + + } else { + this.log('info', '未检测到 hCaptcha'); + } + } + + /** + * 提交并验证支付(供 retryBlock 使用) + */ + async submitAndVerifyPayment(params = {}) { + this.log('info', '提交支付...'); + + // 查找提交按钮 + const submitButton = await this.page.evaluateHandle(() => { + const buttons = Array.from(document.querySelectorAll('button')); + return buttons.find(btn => { + const text = btn.textContent?.trim(); + return text && (text.includes('订阅') || text.includes('Subscribe') || + text.includes('支付') || text.includes('Pay')); + }); + }); + + if (!submitButton) { + throw new Error('未找到提交按钮'); + } + + await submitButton.asElement().click(); + this.log('info', '已点击提交按钮'); + + // 等待支付结果 + await new Promise(resolve => setTimeout(resolve, 3000)); + + // 检测支付结果 + const result = await this.checkPaymentResult(); + + if (result.success) { + this.log('success', '✓ 支付成功'); + return { success: true }; + } + + if (result.cardDeclined) { + // 抛出异常,触发 retryBlock 重试 + throw new Error(`银行卡被拒绝: ${result.message}`); + } + + // 默认认为成功 + this.log('info', '支付状态未知,视为成功'); + return { success: true }; + } + + /** + * 检测支付结果 + */ + async checkPaymentResult() { + const currentUrl = this.page.url(); + + // 检查是否跳转成功(离开 Stripe) + if (!currentUrl.includes('stripe.com') && !currentUrl.includes('checkout.stripe.com')) { + return { success: true, message: '页面已跳转' }; + } + + // 检查页面上的错误信息 + const errorInfo = await this.page.evaluate(() => { + // 查找错误消息 + const errorTexts = [ + 'card was declined', + '卡片被拒绝', + 'Your card was declined', + 'declined', + 'insufficient funds', + '余额不足' + ]; + + const bodyText = document.body.textContent || ''; + + for (const errorText of errorTexts) { + if (bodyText.toLowerCase().includes(errorText.toLowerCase())) { + return { + cardDeclined: true, + message: errorText + }; + } + } + + return { cardDeclined: false }; + }); + + if (errorInfo.cardDeclined) { + return { + success: false, + cardDeclined: true, + message: errorInfo.message + }; + } + + // 默认认为成功 + return { success: true }; + } + + /** + * 步骤 7: 获取订阅信息 + */ + async getSubscriptionInfo(params) { + this.log('info', '获取订阅信息'); + + // TODO: 实现获取订阅信息逻辑 + + return { success: true }; + } + + /** + * 步骤 8: 保存到数据库 + */ + async saveToDatabase(params) { + this.log('info', '保存到数据库'); + + // TODO: 实现数据库保存逻辑 + + return { success: true }; + } +} + +module.exports = WindsurfAdapter; diff --git a/test-new-framework.js b/test-new-framework.js new file mode 100644 index 0000000..87ffc38 --- /dev/null +++ b/test-new-framework.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node +/** + * 测试新框架 + */ +require('dotenv').config(); +const AutomationFactory = require('./src/automation-framework'); +const logger = require('./src/shared/logger'); + +async function test() { + try { + logger.info('Test', '========================================'); + logger.info('Test', '🚀 开始测试新框架'); + logger.info('Test', '========================================'); + + // 使用 AdsPower 浏览器(从 .env 或参数获取) + const adspowerUserId = process.env.ADSPOWER_USER_ID || 'k1728p8l'; + + logger.info('Test', `使用 Profile: ${adspowerUserId}`); + + const result = await AutomationFactory.registerWithAdsPower('windsurf', adspowerUserId); + + logger.info('Test', '========================================'); + + if (result.success) { + logger.success('Test', '✅ 注册成功!'); + logger.info('Test', `邮箱: ${result.accountData?.email}`); + logger.info('Test', `密码: ${result.accountData?.password}`); + logger.info('Test', `卡号: ${result.cardInfo?.number}`); + } else { + logger.error('Test', '❌ 注册失败'); + logger.error('Test', `错误: ${result.error}`); + } + + logger.info('Test', '========================================'); + + } catch (error) { + logger.error('Test', `测试异常: ${error.message}`); + console.error(error); + process.exit(1); + } +} + +test();