This commit is contained in:
dengqichen 2025-11-18 20:51:45 +08:00
parent d52f97027b
commit b61aa2c8e9
16 changed files with 1852 additions and 1 deletions

View File

@ -24,12 +24,14 @@
"2captcha-ts": "^2.4.1", "2captcha-ts": "^2.4.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"commander": "^11.0.0", "commander": "^11.0.0",
"crawlee": "^3.15.3",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"imap": "^0.8.19", "imap": "^0.8.19",
"js-yaml": "^4.1.1",
"mailparser": "^3.6.5", "mailparser": "^3.6.5",
"mysql2": "^3.6.5", "mysql2": "^3.6.5",
"node-capsolver": "^1.2.0", "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" "puppeteer-real-browser": "^1.4.4"
}, },
"engines": { "engines": {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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<Object>}
*/
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;

View File

@ -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<ElementHandle|null>}
*/
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;

View File

@ -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<Object>} - 执行结果
*/
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<Object>}
*/
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<any>}
*/
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<boolean>}
*/
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;

View File

@ -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;

View File

@ -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;

43
test-new-framework.js Normal file
View File

@ -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();