dasdasd
This commit is contained in:
parent
d52f97027b
commit
b61aa2c8e9
@ -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": {
|
||||
|
||||
84
src/automation-framework/actions/click-action.js
Normal file
84
src/automation-framework/actions/click-action.js
Normal 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;
|
||||
31
src/automation-framework/actions/custom-action.js
Normal file
31
src/automation-framework/actions/custom-action.js
Normal 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;
|
||||
145
src/automation-framework/actions/fill-form-action.js
Normal file
145
src/automation-framework/actions/fill-form-action.js
Normal 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;
|
||||
29
src/automation-framework/actions/navigate-action.js
Normal file
29
src/automation-framework/actions/navigate-action.js
Normal 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;
|
||||
147
src/automation-framework/actions/retry-block-action.js
Normal file
147
src/automation-framework/actions/retry-block-action.js
Normal 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;
|
||||
112
src/automation-framework/actions/wait-action.js
Normal file
112
src/automation-framework/actions/wait-action.js
Normal 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;
|
||||
203
src/automation-framework/configs/sites/windsurf.yaml
Normal file
203
src/automation-framework/configs/sites/windsurf.yaml
Normal 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
|
||||
45
src/automation-framework/core/action-registry.js
Normal file
45
src/automation-framework/core/action-registry.js
Normal 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;
|
||||
55
src/automation-framework/core/base-action.js
Normal file
55
src/automation-framework/core/base-action.js
Normal 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;
|
||||
123
src/automation-framework/core/site-adapter.js
Normal file
123
src/automation-framework/core/site-adapter.js
Normal 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;
|
||||
184
src/automation-framework/core/smart-selector.js
Normal file
184
src/automation-framework/core/smart-selector.js
Normal 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;
|
||||
161
src/automation-framework/core/workflow-engine.js
Normal file
161
src/automation-framework/core/workflow-engine.js
Normal 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;
|
||||
85
src/automation-framework/index.js
Normal file
85
src/automation-framework/index.js
Normal 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;
|
||||
402
src/automation-framework/sites/windsurf-adapter.js
Normal file
402
src/automation-framework/sites/windsurf-adapter.js
Normal 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
43
test-new-framework.js
Normal 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();
|
||||
Loading…
Reference in New Issue
Block a user