This commit is contained in:
dengqichen 2025-11-21 17:59:49 +08:00
parent 2f7e29d687
commit 0ec8b0153d
61 changed files with 13652 additions and 0 deletions

View File

@ -0,0 +1,13 @@
# AdsPower配置必需
ADSPOWER_USER_ID=your-profile-id
ADSPOWER_API=http://local.adspower.net:50325
# Windsurf账号信息
WINDSURF_EMAIL=your-email@example.com
WINDSURF_PASSWORD=your-password
WINDSURF_FIRSTNAME=John
WINDSURF_LASTNAME=Doe
# 通用测试账号(可选,作为默认值)
TEST_EMAIL=test@example.com
TEST_PASSWORD=test123

8
browser-automation-ts/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
node_modules/
dist/
*.log
.env
.DS_Store
*.tsbuildinfo
coverage/
.vscode/

View File

@ -0,0 +1,298 @@
# AccountGenerator 迁移报告
## ✅ 已完成 - 100%兼容
### 迁移状态
`src/shared/libs/account-generator/` 迁移到 `src/tools/AccountGeneratorTool.ts`
**状态:完全一致** ✅
---
## 📊 功能对比
| 功能 | 旧框架 | 新Tool | 状态 |
|------|--------|--------|------|
| **邮箱域名** | `qichen111.asia` | `qichen111.asia` | ✅ |
| **邮箱前缀** | 8-12位随机 | 8-12位随机 | ✅ |
| **名字库** | 20+男性/女性/中性 | 完全相同 | ✅ |
| **姓氏库** | 30+ | 完全相同 | ✅ |
| **中文名** | 支持 | 支持 | ✅ |
| **密码策略** | email/random | email/random | ✅ |
| **密码生成** | 复杂规则+打乱 | 完全相同 | ✅ |
| **返回字段** | 8个字段 | 8个字段 | ✅ |
| **批量生成** | 支持 | 支持 | ✅ |
---
## 🎯 返回数据结构(完全一致)
### 旧框架
```javascript
{
firstName: 'John',
lastName: 'Smith',
fullName: 'John Smith',
email: 'abc123xyz@qichen111.asia',
username: 'randomuser',
password: 'abc123xyz@qichen111.asia', // strategy: 'email'
passwordStrategy: 'email',
timestamp: '2025-11-21T07:00:00.000Z',
phone: '15551234567' // 可选
}
```
### 新Tool
```typescript
{
firstName: 'John',
lastName: 'Smith',
fullName: 'John Smith',
email: 'abc123xyz@qichen111.asia',
username: 'randomuser',
password: 'abc123xyz@qichen111.asia', // strategy: 'email'
passwordStrategy: 'email',
timestamp: '2025-11-21T07:00:00.000Z',
phone: '15551234567' // 可选
}
```
**完全相同!** ✅
---
## 🔧 配置选项(完全一致)
### 旧框架
```javascript
generateAccount({
email: {
domain: 'qichen111.asia',
pattern: 'user_{random}'
},
password: {
strategy: 'email', // or 'random'
length: 12,
includeUppercase: true,
includeLowercase: true,
includeNumbers: true,
includeSpecial: true
},
name: {
gender: 'male', // male/female/neutral
locale: 'zh-CN' // en/zh-CN
},
includePhone: true
})
```
### 新Tool
```typescript
generate({
email: {
domain: 'qichen111.asia',
pattern: 'user_{random}'
},
password: {
strategy: 'email', // or 'random'
length: 12,
includeUppercase: true,
includeLowercase: true,
includeNumbers: true,
includeSpecial: true
},
name: {
gender: 'male', // male/female/neutral
locale: 'zh-CN' // en/zh-CN
},
includePhone: true
})
```
**完全相同!** ✅
---
## 💡 核心逻辑对比
### 1. 邮箱生成
#### 旧框架
```javascript
generatePrefix(pattern) {
if (pattern) {
return pattern.replace('{random}', this.generateRandomString());
}
const length = randomInt(8, 12);
return this.generateRandomString(length);
}
```
#### 新Tool
```typescript
private generateEmailPrefix(pattern?: string): string {
if (pattern) {
return pattern.replace('{random}', this.generateRandomString());
}
const length = this.randomInt(8, 12);
return this.generateRandomString(length);
}
```
**逻辑一致** ✅
### 2. 名字生成
#### 旧框架
```javascript
generateFullName(options) {
const firstName = this.generateFirstName(options);
const lastName = this.generateLastName(options);
return {
firstName,
lastName,
fullName: options.locale === 'zh-CN'
? `${lastName}${firstName}`
: `${firstName} ${lastName}`
};
}
```
#### 新Tool
```typescript
private generateName(options: any = {}): { firstName: string; lastName: string; fullName: string } {
const locale = options?.locale || 'en';
if (locale === 'zh-CN') {
const firstName = this.getRandomItem(this.chineseFirstNames);
const lastName = this.getRandomItem(this.chineseLastNames);
return {
firstName,
lastName,
fullName: `${lastName}${firstName}`
};
}
// ... 英文名字逻辑
return {
firstName,
lastName,
fullName: `${firstName} ${lastName}`
};
}
```
**逻辑一致** ✅
### 3. 密码生成
#### 旧框架
```javascript
generate(options) {
// 确保满足最小要求
if (includeLowercase && minLowercase > 0) {
for (let i = 0; i < minLowercase; i++) {
password += this.lowercase.charAt(randomInt(0, this.lowercase.length - 1));
}
}
// ... 其他字符类型
// 填充剩余长度
while (password.length < length) {
password += chars.charAt(randomInt(0, chars.length - 1));
}
// 打乱顺序
return this.shuffle(password);
}
```
#### 新Tool
```typescript
private generatePassword(options: any = {}): string {
// 确保满足最小要求
if (includeLowercase && minLowercase > 0) {
for (let i = 0; i < minLowercase; i++) {
password += this.lowercase.charAt(this.randomInt(0, this.lowercase.length - 1));
}
}
// ... 其他字符类型
// 填充剩余长度
while (password.length < length) {
password += chars.charAt(this.randomInt(0, chars.length - 1));
}
// 打乱顺序
return this.shuffle(password);
}
```
**逻辑完全一致** ✅
---
## 🚀 使用示例
### 在WindsurfAdapter中
```typescript
class WindsurfAdapter extends BaseAdapter {
protected registerTools(): void {
// 注册工具(与旧框架配置一致)
this.registerTool(new AccountGeneratorTool({
email: {
domain: 'qichen111.asia' // 默认域名
},
password: {
strategy: 'email' // 使用邮箱作为密码
},
includePhone: true
}));
}
async beforeWorkflow(context: any) {
// 生成账号
const accountGen = this.getTool<AccountGeneratorTool>('account-generator');
context.data.account = await accountGen.generate();
// 输出示例:
// {
// firstName: 'James',
// lastName: 'Williams',
// fullName: 'James Williams',
// email: 'abc123xyz@qichen111.asia',
// username: 'randomuser',
// password: 'abc123xyz@qichen111.asia',
// passwordStrategy: 'email',
// timestamp: '2025-11-21T07:00:00.000Z',
// phone: '15551234567'
// }
}
}
```
---
## ✅ 迁移验证清单
- [x] 邮箱域名默认值一致
- [x] 邮箱前缀长度范围一致 (8-12)
- [x] 名字库完全一致 (20+男性/女性/中性)
- [x] 姓氏库完全一致 (30+)
- [x] 中文名字支持
- [x] passwordStrategy 支持 (email/random)
- [x] 密码生成规则一致(最小字符数、打乱)
- [x] 返回字段一致8个字段
- [x] timestamp 格式一致 (ISO)
- [x] 可选字段支持 (phone)
- [x] 批量生成支持
- [x] 配置选项接口一致
---
## 🎉 结论
**AccountGeneratorTool 已完全迁移与旧框架保持100%兼容!**
- ✅ 所有功能
- ✅ 所有配置
- ✅ 所有返回字段
- ✅ 所有生成逻辑
可以安全使用!

View File

@ -0,0 +1,274 @@
# 如何使用自动化框架
## 🚀 快速开始
### 1. 安装依赖
```bash
cd browser-automation-ts
npm install
```
### 2. 配置环境变量
创建 `.env` 文件或设置环境变量:
```bash
# AdsPower配置必需
ADSPOWER_USER_ID=your-profile-id
ADSPOWER_API=http://local.adspower.net:50325
# 账号信息(按网站命名)
# Windsurf
WINDSURF_EMAIL=your-email@example.com
WINDSURF_PASSWORD=your-password
# Stripe
STRIPE_EMAIL=your-email@example.com
STRIPE_PASSWORD=your-password
# 通用测试账号(可选,作为默认值)
TEST_EMAIL=test@example.com
TEST_PASSWORD=test123
```
### 3. 添加网站配置
将网站的YAML配置文件放到 `configs/sites/` 目录:
```
browser-automation-ts/
├── configs/
│ └── sites/
│ ├── windsurf.yaml ← 你的配置文件
│ ├── stripe.yaml
│ └── github.yaml
```
### 4. 运行自动化
```bash
# 运行Windsurf自动化
npm run run -- windsurf
# 运行Stripe自动化
npm run run -- stripe
# 运行任意网站
npm run run -- <网站名称>
```
---
## 📝 创建新网站配置
### YAML配置格式
```yaml
# configs/sites/my-site.yaml
site: my-site
workflow:
# 1. 导航到网站
- action: navigate
name: "打开首页"
url: https://example.com
# 2. 等待页面加载
- action: wait
type: delay
duration: 2000
# 3. 点击按钮
- action: click
name: "点击登录按钮"
selector: "#login-button"
# 4. 填写表单
- action: fillForm
name: "填写登录表单"
fields:
email: "{{account.email}}"
password: "{{account.password}}"
# 5. 提交
- action: click
selector: "button[type='submit']"
# 6. 验证成功
- action: verify
name: "验证登录成功"
conditions:
success:
- urlContains: "/dashboard"
- elementExists: ".user-profile"
```
### 支持的Action类型
| Action | 说明 | 示例 |
|--------|------|------|
| **navigate** | 导航到URL | `url: "https://example.com"` |
| **click** | 点击元素 | `selector: "#button"` |
| **wait** | 等待 | `type: delay, duration: 2000` |
| **fillForm** | 填写表单 | `fields: { email: "{{account.email}}" }` |
| **verify** | 验证条件 | `conditions: { success: [...] }` |
| **custom** | 自定义逻辑 | `handler: "myFunction"` |
| **scroll** | 滚动页面 | `type: bottom` |
| **extract** | 提取数据 | `selector: ".data", saveTo: "result"` |
| **retryBlock** | 重试块 | `steps: [...], maxRetries: 3` |
### 变量替换
在YAML中可以使用变量
```yaml
# 账号数据(从环境变量加载)
email: "{{account.email}}"
password: "{{account.password}}"
# 网站配置
url: "{{site.url}}"
# 环境变量
apiKey: "{{env.API_KEY}}"
# 默认值
timeout: "{{config.timeout|30000}}"
```
---
## 🔧 环境变量命名规则
### 格式:`网站名_字段名`
```bash
# 网站名转大写,连字符改下划线
# windsurf → WINDSURF
WINDSURF_EMAIL=xxx
WINDSURF_PASSWORD=xxx
# my-site → MY_SITE
MY_SITE_EMAIL=xxx
MY_SITE_PASSWORD=xxx
```
### 支持的字段
- `EMAIL` - 邮箱
- `PASSWORD` - 密码
- `USERNAME` - 用户名
- `PHONE` - 手机号
- `APIKEY` - API密钥
- `TOKEN` - 令牌
---
## 📂 项目结构
```
browser-automation-ts/
├── cli/ # CLI工具
│ └── run.ts # 主执行文件
├── configs/ # 配置文件
│ └── sites/ # 网站YAML配置
│ ├── windsurf.yaml
│ └── ...
├── src/ # 源代码
│ ├── core/ # 核心类
│ ├── providers/ # Provider实现
│ │ └── adspower/ # AdsPower Provider
│ │ ├── actions/ # 9个Action类
│ │ └── core/ # ActionFactory等
│ └── workflow/ # WorkflowEngine
└── package.json
```
---
## 🎯 使用示例
### 示例1运行Windsurf自动化
```bash
# 1. 复制windsurf.yaml到configs/sites/
cp ../src/tools/automation-framework/configs/sites/windsurf.yaml configs/sites/
# 2. 设置环境变量
export ADSPOWER_USER_ID=your-profile-id
export WINDSURF_EMAIL=your-email@example.com
export WINDSURF_PASSWORD=your-password
# 3. 运行
npm run run -- windsurf
```
### 示例2添加新网站
```bash
# 1. 创建配置文件
cat > configs/sites/github.yaml << EOF
site: github
workflow:
- action: navigate
url: https://github.com/login
- action: fillForm
fields:
login: "{{account.email}}"
password: "{{account.password}}"
- action: click
selector: "input[type='submit']"
EOF
# 2. 设置环境变量
export GITHUB_EMAIL=your-email@example.com
export GITHUB_PASSWORD=your-password
# 3. 运行
npm run run -- github
```
---
## 🐛 调试
### 查看可用配置
运行不带参数的命令会列出所有可用配置:
```bash
npm run run
```
### 输出说明
执行时会显示:
- ✅ 成功的步骤
- ❌ 失败的步骤
- ⏸️ 等待状态
- 📊 最终统计
### 常见问题
**Q: "Config file not found"**
- 确认YAML文件在 `configs/sites/` 目录下
- 文件名与运行命令匹配(不含.yaml扩展名
**Q: "AdsPower Profile ID is required"**
- 设置 `ADSPOWER_USER_ID` 环境变量
**Q: "Element not found"**
- 检查selector是否正确
- 增加wait时间
- 使用SmartSelector的多策略
---
## 🎉 完成!
现在你可以:
1. 编写YAML配置
2. 设置环境变量
3. 运行 `npm run run -- 网站名`
4. 自动化执行✨

View File

@ -0,0 +1,141 @@
# TypeScript架构实施总结
## ✅ 已完成
### 1. 项目结构
```
browser-automation-ts/
├── src/
│ ├── core/ ✅ 核心抽象层(通用)
│ │ ├── interfaces/ ✅ 接口定义
│ │ ├── base/ ✅ 抽象基类
│ │ └── types/ ✅ 类型定义
│ ├── workflow/ ✅ 工作流引擎(通用!)
│ │ └── WorkflowEngine.ts
│ ├── providers/ ✅ Provider实现特定
│ │ └── adspower/ ✅ AdsPower实现
│ │ ├── AdsPowerProvider.ts
│ │ ├── actions/ ⏳ TODO
│ │ └── core/ ⏳ TODO
│ ├── factory/ ✅ 工厂类
│ └── index.ts ✅ 主入口
├── tests/ ✅ 测试文件
├── docs/ ✅ 文档
│ └── ARCHITECTURE.md ✅ 架构设计文档
├── package.json ✅ 配置
├── tsconfig.json ✅ TS配置
└── jest.config.js ✅ 测试配置
```
### 2. 核心组件
#### 接口层(强制规范)
- ✅ `IBrowserProvider` - Provider接口
- ✅ `IAction` - Action接口
- ✅ `IActionFactory` - ActionFactory接口
#### 抽象基类(共享实现)
- ✅ `BaseBrowserProvider` - Provider基类
- ✅ `BaseAction` - Action基类
#### 类型系统
- ✅ `BrowserProviderType` - Provider类型枚举
- ✅ `IBrowserCapabilities` - 能力定义
- ✅ `ILaunchOptions` - 启动选项
- ✅ `IActionConfig` - Action配置
- ✅ `IActionResult` - Action结果
#### 工厂模式
- ✅ `BrowserFactory` - Provider工厂泛型+类型安全)
#### Provider实现
- ✅ `AdsPowerProvider` - 完整实现
### 3. OOP特性体现
| 特性 | 实现 |
|------|------|
| **封装** | interface + abstract + private/protected |
| **继承** | extends BaseBrowserProvider |
| **多态** | IBrowserProvider接口不同Provider实现 |
| **类型安全** | TypeScript编译时检查 |
| **依赖注入** | 工厂模式 + 构造函数注入 |
## ⏳ TODO需继续实现
### Phase 2
- [ ] AdsPower Actions实现
- [ ] AdsPower WorkflowEngine实现
- [ ] AdsPower SmartSelector实现
- [ ] AdsPower ActionFactory实现
### Phase 3
- [ ] Playwright Provider实现
- [ ] Playwright Actions实现
- [ ] Playwright Core实现
### Phase 4
- [ ] 依赖注入容器
- [ ] Provider验证器
- [ ] 完整测试覆盖
## 🚀 使用方法
### 安装依赖
```bash
cd browser-automation-ts
npm install
```
### 编译
```bash
npm run build
```
### 运行测试
```bash
npm test
```
### 基本使用
```typescript
import { BrowserFactory, BrowserProviderType } from './src';
const provider = BrowserFactory.create(BrowserProviderType.ADSPOWER, {
profileId: 'k1728p8l'
});
await provider.launch();
const page = provider.getPage();
await provider.close();
```
## 📊 对比老架构
| 特性 | 老架构(JS) | 新架构(TS) |
|------|-----------|-----------|
| 类型检查 | ❌ 运行时 | ✅ 编译时 |
| IDE支持 | ⚠️ 一般 | ✅ 完美 |
| 重构安全 | ❌ 手动 | ✅ 自动 |
| 接口强制 | ❌ 文档 | ✅ 编译器 |
| 抽象类 | ⚠️ 约定 | ✅ 强制 |
## 🔄 迁移计划
1. ✅ **Phase 1**: 基础架构(已完成)
2. ⏳ **Phase 2**: 迁移AdsPower完整功能
3. ⏳ **Phase 3**: 添加Playwright
4. ⏳ **Phase 4**: 完整测试
5. ⏳ **Phase 5**: 替换老项目
## 📝 注意事项
1. **Lint错误正常** - 运行`npm install`后会解决
2. **独立项目** - 与老项目完全隔离
3. **渐进式** - 可以并存测试
4. **向后兼容** - API设计与老版本相似
---
**创建时间:** 2025-11-21
**状态:** Phase 1 完成 ✅

View File

@ -0,0 +1,166 @@
# TypeScript迁移进度报告
## ✅ 已完成的工作
### 1. 核心类创建(`src/core/`
- ✅ **SmartSelector.ts** - 智能选择器,支持多策略元素查找
- 路径:`src/core/selectors/SmartSelector.ts`
- 支持CSS、XPath、Text、Placeholder等多种选择策略
- ✅ **CustomErrors.ts** - 自定义错误类
- 路径:`src/core/errors/CustomErrors.ts`
- 包含AutomationError, ElementNotFoundError, TimeoutError, ValidationError, ConfigurationError, RetryExhaustedError
### 2. AdsPower Provider BaseAction`src/providers/adspower/core/`
- ✅ **BaseAction.ts** - 增强版BaseAction
- 路径:`src/providers/adspower/core/BaseAction.ts`
- 特性:
- 变量替换系统(支持`{{account.email}}`、默认值等)
- 人类行为延迟方法randomDelay, thinkDelay, pauseDelay等
- ActionContext接口包含page, logger, data, adapter等
### 3. Action类迁移9个
所有Action类已从旧框架复制并完成TypeScript转换
| Action | 路径 | 状态 |
|--------|------|------|
| ClickAction | `src/providers/adspower/actions/ClickAction.ts` | ✅ 已转换 |
| WaitAction | `src/providers/adspower/actions/WaitAction.ts` | ✅ 已转换 |
| NavigateAction | `src/providers/adspower/actions/NavigateAction.ts` | ✅ 已转换 |
| CustomAction | `src/providers/adspower/actions/CustomAction.ts` | ✅ 已转换 |
| VerifyAction | `src/providers/adspower/actions/VerifyAction.ts` | ✅ 已转换 |
| FillFormAction | `src/providers/adspower/actions/FillFormAction.ts` | ✅ 已转换 |
| ScrollAction | `src/providers/adspower/actions/ScrollAction.ts` | ✅ 已转换 |
| ExtractAction | `src/providers/adspower/actions/ExtractAction.ts` | ✅ 已转换 |
| RetryBlockAction | `src/providers/adspower/actions/RetryBlockAction.ts` | ✅ 已转换 |
**转换内容:**
- ✅ 所有import/export改为ES6模块语法
- ✅ 所有方法添加返回类型注解
- ✅ 所有参数添加类型注解
- ✅ evaluate回调添加类型标注
- ✅ catch块error变量添加`any`类型
- ✅ import路径修复指向正确的核心类
### 4. ActionFactory更新
- ✅ **ActionFactory.ts** - 注册所有9个Action类
- 路径:`src/providers/adspower/core/ActionFactory.ts`
- 已注册click, wait, navigate, custom, verify, fillForm, scroll, extract, retryBlock
---
## 📊 当前目录结构
```
browser-automation-ts/
├── src/
│ ├── core/ # 核心类跨Provider共享
│ │ ├── base/
│ │ │ ├── BaseAction.ts # 抽象基础Action
│ │ │ └── BaseBrowserProvider.ts
│ │ ├── interfaces/
│ │ │ ├── IAction.ts
│ │ │ ├── IBrowserProvider.ts
│ │ │ └── ISmartSelector.ts
│ │ ├── types/
│ │ │ └── index.ts
│ │ ├── selectors/
│ │ │ └── SmartSelector.ts # ✅ 新增
│ │ └── errors/
│ │ └── CustomErrors.ts # ✅ 新增
│ ├── providers/
│ │ └── adspower/
│ │ ├── core/
│ │ │ ├── BaseAction.ts # ✅ 新增(增强版)
│ │ │ └── ActionFactory.ts # ✅ 已更新
│ │ ├── actions/ # ✅ 全部迁移完成
│ │ │ ├── ClickAction.ts
│ │ │ ├── WaitAction.ts
│ │ │ ├── NavigateAction.ts
│ │ │ ├── CustomAction.ts
│ │ │ ├── VerifyAction.ts
│ │ │ ├── FillFormAction.ts
│ │ │ ├── ScrollAction.ts
│ │ │ ├── ExtractAction.ts
│ │ │ └── RetryBlockAction.ts
│ │ └── AdsPowerProvider.ts
│ ├── workflow/
│ │ └── WorkflowEngine.ts
│ └── factory/
│ └── ProviderFactory.ts
└── tests/
```
---
## 🎯 下一步任务
### 短期任务
1. **修复剩余TypeScript错误**
- VerifyAction中的类型兼容性问题`boolean | null`
- 确保所有文件编译通过
2. **测试Action类**
- 编写单元测试验证Action功能
- 确保旧框架功能完整保留
3. **集成到WorkflowEngine**
- 更新WorkflowEngine使用AdsPower Provider
- 测试完整workflow执行
### 中期任务
4. **添加其他Provider**
- Playwright Provider
- Puppeteer Provider作为fallback
5. **完善文档**
- API文档
- 使用示例
- 迁移指南
---
## 🔧 技术要点
### TypeScript转换规范
```typescript
// ❌ 旧JS写法
const BaseAction = require('../core/base-action');
async execute() { ... }
catch (error) { ... }
// ✅ 新TS写法
import BaseAction from '../core/BaseAction';
async execute(): Promise<any> { ... }
catch (error: any) { ... }
```
### Import路径规则
```typescript
// Provider内部的BaseAction
import BaseAction from '../core/BaseAction';
// 跨层级的核心类
import SmartSelector from '../../../core/selectors/SmartSelector';
import { ConfigurationError } from '../../../core/errors/CustomErrors';
```
---
## ⚠️ 已知问题
1. **Jest类型定义缺失**
- 位置:`tests/basic.test.ts`
- 解决:运行 `npm install` 安装@types/jest
2. **VerifyAction类型兼容**
- 错误:`boolean | null` 不能分配给 `boolean`
- 待修复
---
## 📝 备注
- 所有旧框架代码保留在 `src/tools/automation-framework/`
- 新架构代码在 `browser-automation-ts/`
- 两套代码暂时独立,确保平滑迁移

View File

@ -0,0 +1,248 @@
# 插件系统实现状态
## ✅ 已完成
### 1. 核心基础设施
#### ITool.ts - Tool基础接口和抽象类
```typescript
interface ITool<TConfig> {
readonly name: string;
initialize(config: TConfig): Promise<void>;
cleanup?(): Promise<void>;
healthCheck?(): Promise<boolean>;
}
abstract class BaseTool<TConfig> implements ITool<TConfig> {
// 强制子类实现配置验证
protected abstract validateConfig(config: TConfig): void;
// 强制子类实现初始化逻辑
protected abstract doInitialize(): Promise<void>;
// 提供状态检查
protected ensureInitialized(): void;
}
```
**作用:**
- ✅ 强制所有Tool实现统一接口
- ✅ 提供模板方法模式保证初始化流程
- ✅ 自动状态检查防止未初始化调用
#### BaseAdapter.ts - Adapter基础类
```typescript
abstract class BaseAdapter implements ISiteAdapter {
// 强制子类注册工具
protected abstract registerTools(): void;
// 强制子类声明依赖
protected abstract getRequiredTools(): string[];
// 提供工具管理
protected registerTool(tool: ITool): void;
protected getTool<T>(name: string): T;
}
```
**作用:**
- ✅ 强制Adapter注册工具
- ✅ 自动验证必需工具是否注册
- ✅ 类型安全的工具获取
- ✅ 统一的初始化流程
### 2. 第一个Tool实现
#### AccountGeneratorTool - 账号生成器
```typescript
class AccountGeneratorTool extends BaseTool<AccountGeneratorConfig> {
async generate(): Promise<AccountData>
}
```
**功能:**
- ✅ 生成随机邮箱
- ✅ 生成强密码
- ✅ 生成随机姓名
- ✅ 生成手机号
- ✅ 支持配置(域名、密码长度等)
### 3. 示例Adapter实现
#### WindsurfAdapter
```typescript
class WindsurfAdapter extends BaseAdapter {
protected registerTools() {
// 注册工具并配置
this.registerTool(new AccountGeneratorTool({
emailDomain: 'tempmail.com',
passwordLength: 12
}));
}
getHandlers() {
// 提供custom action处理
return { generateCard, handleEmailVerification, ... };
}
}
```
**演示了:**
- ✅ 如何注册工具
- ✅ 如何配置工具
- ✅ 如何使用工具
- ✅ 如何编排业务逻辑
---
## 📋 架构图
```
┌─────────────────────────────────────────┐
│ ISiteAdapter (接口) │
└──────────────┬──────────────────────────┘
┌──────────────▼──────────────────────────┐
│ BaseAdapter (抽象基类) │
│ ┌──────────────────────────────────┐ │
│ │ 工具管理 │ │
│ │ - registerTool() │ │
│ │ - getTool() │ │
│ │ - validateRequiredTools() │ │
│ └──────────────────────────────────┘ │
└──────────────┬──────────────────────────┘
┌──────────────▼──────────────────────────┐
│ WindsurfAdapter (具体实现) │
│ ┌──────────────────────────────────┐ │
│ │ 注册工具 │ │
│ │ - AccountGeneratorTool │ │
│ │ - DatabaseTool (TODO) │ │
│ │ - EmailTool (TODO) │ │
│ └──────────────────────────────────┘ │
│ ┌──────────────────────────────────┐ │
│ │ 业务逻辑 │ │
│ │ - generateCard() │ │
│ │ - handleEmailVerification() │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
---
## 🚀 当前可运行
```bash
# 设置环境变量
export ADSPOWER_USER_ID="your-id"
# 运行Windsurf会自动生成账号
pnpm run run -- windsurf
```
**会发生什么:**
1. ✅ 加载 windsurf.yaml
2. ✅ 加载 windsurf-adapter.ts
3. ✅ 注册 AccountGeneratorTool
4. ✅ 验证必需工具
5. ✅ 初始化工具
6. ✅ 自动生成账号数据
7. ✅ 执行workflow
8. ✅ 调用custom handlers
---
## ⏳ 待实现的Tool
按优先级:
### 1. DatabaseTool (高优先级)
```typescript
class DatabaseTool extends BaseTool<DatabaseConfig> {
async connect(): Promise<void>
async query(sql: string, params?: any[]): Promise<any>
async save(table: string, data: any): Promise<void>
async close(): Promise<void>
}
```
**用途:**
- 保存账号数据
- 获取卡片数据
- 标记卡为已使用
### 2. CardGeneratorTool (高优先级)
```typescript
class CardGeneratorTool extends BaseTool<CardGeneratorConfig> {
async generate(): Promise<CardData>
async markAsUsed(cardNumber: string): Promise<void>
}
```
**配置:**
- source: 'database' | 'api' | 'mock'
- binFilter: string[]
- reuseDelay: number
### 3. EmailTool (中优先级)
```typescript
class EmailTool extends BaseTool<EmailConfig> {
async connect(): Promise<void>
async getVerificationCode(options: any): Promise<string>
async close(): Promise<void>
}
```
**配置:**
- protocol: 'imap' | 'pop3' | 'api'
- server: string
- codePattern: RegExp
### 4. CaptchaTool (低优先级)
```typescript
class CaptchaTool extends BaseTool<CaptchaConfig> {
async solve(type: string, params: any): Promise<any>
}
```
---
## 💡 设计优势总结
### 1. 强制规范
- ❌ 不能忘记实现 `validateConfig()`
- ❌ 不能忘记注册必需的Tool
- ❌ 不能在未初始化时调用Tool
- ✅ 编译时+运行时双重检查
### 2. 类型安全
```typescript
// ✅ TypeScript知道返回类型
const accountGen = this.getTool<AccountGeneratorTool>('account-generator');
const account = await accountGen.generate(); // 有代码提示
```
### 3. 配置驱动
```typescript
// 同一个Tool不同配置 = 不同实例
// A网站
new AccountGeneratorTool({ emailDomain: 'mail.com' })
// B网站
new AccountGeneratorTool({ emailDomain: 'qq.com' })
```
### 4. 易扩展
```typescript
// 添加新Tool
// 1. 继承BaseTool
// 2. 实现3个方法
// 3. 在Adapter中注册
// 完成!
```
---
## 🎯 下一步
1. 实现 `DatabaseTool`
2. 实现 `CardGeneratorTool`
3. 更新 `WindsurfAdapter` 使用所有Tool
4. 测试完整流程
**现在的架构:规范即代码,无法违反!** 🎉

View File

@ -0,0 +1,143 @@
# 快速开始 - 5分钟运行你的第一个自动化
## ⚡ 3步开始
### 步骤1安装依赖
```bash
cd browser-automation-ts
npm install
```
### 步骤2复制配置文件
```bash
# 将windsurf.yaml复制到configs目录
cp ../src/tools/automation-framework/configs/sites/windsurf.yaml configs/sites/
```
### 步骤3运行
```bash
# 设置必需的环境变量
export ADSPOWER_USER_ID=your-profile-id
export WINDSURF_EMAIL=your-email
export WINDSURF_PASSWORD=your-password
# 执行自动化
npm run run -- windsurf
```
---
## 🎯 就是这么简单!
你刚才做了什么:
1. ✅ 安装了TypeScript自动化框架
2. ✅ 使用了旧框架的YAML配置完全兼容
3. ✅ 运行了完整的Windsurf自动化流程
---
## 📖 添加新网站1分钟
### 1. 创建YAML配置
```bash
# 创建新网站配置
cat > configs/sites/mysite.yaml << EOF
site: mysite
workflow:
- action: navigate
url: https://mysite.com
- action: click
selector: "#login"
- action: fillForm
fields:
email: "{{account.email}}"
password: "{{account.password}}"
EOF
```
### 2. 设置账号信息
```bash
export MYSITE_EMAIL=your-email
export MYSITE_PASSWORD=your-password
```
### 3. 执行
```bash
npm run run -- mysite
```
---
## 💡 核心概念
### 通用执行器
- **1个工具运行所有网站**
- 只需编写YAML配置
- 不需要编写代码
### 工作流程
```
YAML配置 → 加载 → WorkflowEngine → AdsPower → 浏览器自动化
```
### 配置即代码
```yaml
workflow:
- action: navigate # 导航
- action: click # 点击
- action: fillForm # 填表
- action: verify # 验证
```
---
## 🔍 查看更多
- 📚 完整文档:`HOW-TO-USE.md`
- 🏗️ 架构说明:`docs/ARCHITECTURE.md`
- 📦 迁移进度:`MIGRATION-PROGRESS.md`
---
## ⚙️ 环境变量说明
### 必需
```bash
ADSPOWER_USER_ID=xxx # AdsPower配置ID
```
### 账号信息(按网站)
```bash
WINDSURF_EMAIL=xxx
WINDSURF_PASSWORD=xxx
STRIPE_EMAIL=xxx
STRIPE_PASSWORD=xxx
# 规则网站名_字段名大写
```
---
## 🎉 恭喜!
你已经掌握了新框架的使用方法!
现在可以:
- ✅ 复用旧框架的所有YAML配置
- ✅ 添加新网站只需创建YAML
- ✅ 享受TypeScript的类型安全
- ✅ 使用9个完整迁移的Action类
**开始自动化吧!** 🚀

View File

@ -0,0 +1,41 @@
# Browser Automation Framework (TypeScript)
企业级浏览器自动化框架 - 全新TypeScript架构
## 目录结构
```
browser-automation-ts/
├── src/
│ ├── core/ # 核心抽象层
│ │ ├── interfaces/ # 接口定义
│ │ ├── base/ # 抽象基类
│ │ └── types/ # 类型定义
│ │
│ ├── providers/ # 浏览器提供商
│ │ ├── adspower/ # AdsPower实现
│ │ └── playwright/ # Playwright实现
│ │
│ ├── actions/ # 动作系统(抽象)
│ ├── workflow/ # 工作流引擎
│ ├── factory/ # 工厂模式
│ └── di/ # 依赖注入
├── dist/ # 编译输出
├── tests/ # 测试
└── docs/ # 文档
```
## 特性
- ✅ TypeScript 严格模式
- ✅ 完整的 OOP封装、继承、多态
- ✅ 编译时类型检查
- ✅ 依赖注入
- ✅ 策略模式 + 工厂模式
## 与老项目关系
- **独立项目** - 完全独立,不依赖老代码
- **测试后迁移** - 验证通过后替换老项目
- **渐进式** - 可与老项目并存

View File

@ -0,0 +1,43 @@
# 🚀 快速运行指南
## 第一步:设置环境变量
在PowerShell中设置临时
```powershell
# 必需 - AdsPower配置
$env:ADSPOWER_USER_ID="your-profile-id"
# 必需 - Windsurf账号
$env:WINDSURF_EMAIL="your-email@example.com"
$env:WINDSURF_PASSWORD="your-password"
$env:WINDSURF_FIRSTNAME="John"
$env:WINDSURF_LASTNAME="Doe"
```
或者创建 `.env` 文件(复制 `.env.example`
```bash
cp .env.example .env
# 然后编辑 .env 文件填写真实信息
```
## 第二步:运行
```bash
cd browser-automation-ts
pnpm run run -- windsurf
```
## 🎯 就是这么简单!
运行后你会看到:
- 🚀 Browser Automation Executor
- ✅ 每个步骤的执行状态
- 📊 最终执行摘要
## 📝 添加新网站
1. 在 `configs/sites/` 创建新YAML
2. 设置对应的环境变量网站名_字段名
3. 运行:`pnpm run run -- 网站名`

View File

@ -0,0 +1,195 @@
# Tool V2 - 可拼接式设计
## 🎯 设计理念
### 核心原则
1. **松耦合** - 工具之间通过接口通信,不直接依赖
2. **可替换** - 任何工具都可以被同类工具替换
3. **可组合** - 像乐高一样自由组合
4. **依赖注入** - 通过服务名获取依赖,而非硬编码
---
## 📦 架构示意图
```
┌─────────────────────────────────────────┐
│ IToolContext (上下文) │
│ ┌─────────────┐ ┌───────────────┐ │
│ │ Data Store │ │ Service Bus │ │
│ └─────────────┘ └───────────────┘ │
└─────────────────────────────────────────┘
↑ ↑
│ │
┌────────┴────────┬───────┴────────┐
│ │ │
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Tool A │ │ Tool B │ │ Tool C │
│provides │ │requires │ │provides │
│storage │ │storage │ │generator │
└─────────┘ └──────────┘ └──────────┘
```
---
## 💡 使用示例
### 场景1使用MySQL存储
```typescript
const toolManager = new ToolManager();
// 注册工具(顺序无关!)
toolManager.register(new CardGeneratorTool());
toolManager.register(new MySQLStorageTool()); // 提供storage
toolManager.register(new AccountGeneratorTool());
// 自动解决依赖并初始化
await toolManager.initializeAll();
// 使用
const context = toolManager.getContext();
const cardGen = context.getService<IGeneratorService>('card-generator');
const card = await cardGen.generate();
```
### 场景2切换到Redis存储只需改一行
```typescript
const toolManager = new ToolManager();
toolManager.register(new CardGeneratorTool());
toolManager.register(new RedisStorageTool()); // 替换成Redis
toolManager.register(new AccountGeneratorTool());
await toolManager.initializeAll(); // 其他代码完全不用改
```
---
## 🔌 可拼接的优势
### 1. 完全解耦
```typescript
// ❌ 旧设计 - 耦合
class CardGenerator {
constructor(private db: MySQLDatabase) {} // 硬编码依赖MySQL
}
// ✅ 新设计 - 解耦
class CardGeneratorTool {
readonly requires = ['storage']; // 只声明需要storage接口
async initialize(context: IToolContext) {
// 不关心谁提供storage只要符合IStorageService接口即可
const storage = context.getService<IStorageService>('storage');
}
}
```
### 2. 任意组合
```typescript
// 组合1MySQL + 本地卡生成器
toolManager.register(new MySQLStorageTool());
toolManager.register(new LocalCardGenerator());
// 组合2Redis + API卡生成器
toolManager.register(new RedisStorageTool());
toolManager.register(new APICardGenerator());
// 组合3内存存储 + 测试卡生成器(用于测试)
toolManager.register(new MemoryStorageTool());
toolManager.register(new MockCardGenerator());
```
### 3. 插件化
```typescript
// 添加新功能,完全不影响现有工具
toolManager.register(new LoggerTool()); // 日志工具
toolManager.register(new MetricsTool()); // 监控工具
toolManager.register(new CacheTool()); // 缓存工具
```
---
## 🏗️ 在Adapter中使用
### windsurf-adapter.ts
```typescript
import { ToolManager } from '../../src/tools/ToolManager';
import { AccountGeneratorTool } from '../../src/tools/AccountGeneratorTool';
import { CardGeneratorTool } from '../../src/tools/CardGeneratorTool';
import { MySQLStorageTool } from '../../src/tools/MySQLStorageTool';
class WindsurfAdapter implements ISiteAdapter {
private toolManager: ToolManager;
constructor() {
this.toolManager = new ToolManager();
// 拼接需要的工具(像搭积木)
this.toolManager.register(new MySQLStorageTool());
this.toolManager.register(new AccountGeneratorTool());
this.toolManager.register(new CardGeneratorTool());
// 想要更多功能?继续注册!
// this.toolManager.register(new EmailHandlerTool());
// this.toolManager.register(new CaptchaSolverTool());
}
async initialize(context: any): Promise<void> {
// 一键初始化所有工具
await this.toolManager.initializeAll();
const toolContext = this.toolManager.getContext();
// 生成账号
if (!context.data.account?.email) {
const accountGen = toolContext.getService('account-generator');
context.data.account = await accountGen.generate();
}
}
getHandlers() {
const toolContext = this.toolManager.getContext();
return {
generateCard: async () => {
const cardGen = toolContext.getService('card-generator');
return await cardGen.generate();
},
saveToDatabase: async () => {
const storage = toolContext.getService('storage');
await storage.save('account:xxx', this.context.data.account);
}
};
}
}
```
---
## ✅ 对比总结
| 特性 | V1设计 | V2设计 |
|------|--------|--------|
| **耦合度** | 高(直接依赖) | 低(接口依赖) |
| **可替换** | 难(需改代码) | 易(改配置) |
| **测试** | 难需mock具体类 | 易mock接口 |
| **扩展** | 难(需修改现有代码) | 易(只需添加工具) |
| **维护** | 难(改一处影响多处) | 易(工具独立) |
---
## 🎉 这才是真正的"可拼接"
就像:
- **USB接口** - 不管是键盘、鼠标、U盘只要符合USB规范就能插上
- **乐高积木** - 不同的积木可以自由组合
- **插座** - 不管什么电器,只要符合电压规范就能用
**工具之间通过标准接口storage、generator、validator等通信而不是硬编码依赖关系**

View File

@ -0,0 +1,183 @@
# Tools迁移计划
## 🎯 目标
`src/shared/libs/` 中的JS工具迁移到 `browser-automation-ts/src/tools/`统一使用TypeScript和ITool规范。
---
## 📊 迁移清单
### 1. AccountGenerator ✅ (已创建)
- **源文件**: `src/shared/libs/account-generator.js`
- **目标**: `browser-automation-ts/src/tools/AccountGenerator.ts`
- **状态**: ✅ 完成新实现符合ITool规范
- **接口**: `IAccountGenerator`
### 2. CardGenerator
- **源文件**: `src/shared/libs/card-generator.js`
- **目标**: `browser-automation-ts/src/tools/CardGenerator.ts`
- **状态**: ⏳ 待迁移
- **接口**: `ICardGenerator`
- **依赖**: Database
- **功能**:
- 从数据库获取未使用的卡
- 标记卡为已使用
- BIN去重逻辑
### 3. EmailHandler
- **源文件**: `src/shared/libs/email-handler.js` (如果存在)
- **目标**: `browser-automation-ts/src/tools/EmailHandler.ts`
- **状态**: ⏳ 待创建
- **接口**: `IEmailHandler`
- **功能**:
- 连接邮箱服务
- 获取验证码
- 解析邮件内容
### 4. CaptchaSolver
- **源文件**: `src/shared/libs/captcha-solver.js`
- **目标**: `browser-automation-ts/src/tools/CaptchaSolver.ts`
- **状态**: ⏳ 待迁移
- **接口**: `ICaptchaHandler`
- **功能**:
- Turnstile处理
- hCaptcha处理
- reCAPTCHA处理
### 5. DatabaseClient
- **源文件**: `src/shared/libs/database.js`
- **目标**: `browser-automation-ts/src/tools/DatabaseClient.ts`
- **状态**: ⏳ 待迁移
- **接口**: `IDatabaseClient`
- **功能**:
- MySQL连接
- 保存账号数据
- 保存卡片数据
- 查询操作
---
## 🏗️ 最终架构
```
browser-automation-ts/
├── src/
│ ├── core/ # 核心Provider、Action、WorkflowEngine
│ ├── adapters/ # 适配器接口
│ └── tools/ # 业务工具符合ITool规范
│ ├── ITool.ts # ✅ 工具接口规范
│ ├── AccountGenerator.ts # ✅ 账号生成器
│ ├── CardGenerator.ts # ⏳ 卡生成器
│ ├── EmailHandler.ts # ⏳ 邮箱处理器
│ ├── CaptchaSolver.ts # ⏳ 验证码处理器
│ └── DatabaseClient.ts # ⏳ 数据库客户端
├── configs/
│ └── sites/
│ ├── windsurf.yaml # 流程定义
│ └── windsurf-adapter.ts # 业务逻辑使用tools
└── cli/
└── run.ts # 通用执行器
```
---
## 💡 使用示例
### Windsurf Adapter示例
```typescript
import { WindsurfAdapter } from './windsurf-adapter';
import { AccountGenerator } from '../../src/tools/AccountGenerator';
import { CardGenerator } from '../../src/tools/CardGenerator';
import { DatabaseClient } from '../../src/tools/DatabaseClient';
class WindsurfAdapter implements ISiteAdapter {
private accountGen: AccountGenerator;
private cardGen: CardGenerator;
private db: DatabaseClient;
async initialize(context: any) {
// 初始化工具
this.accountGen = new AccountGenerator({
emailDomain: 'tempmail.com'
});
await this.accountGen.initialize();
this.db = new DatabaseClient({
host: 'localhost',
database: 'accounts'
});
await this.db.initialize();
this.cardGen = new CardGenerator({
database: this.db
});
await this.cardGen.initialize();
// 生成账号(如果需要)
if (!context.data.account.email) {
context.data.account = await this.accountGen.generate();
}
}
getHandlers() {
return {
generateCard: async () => {
const card = await this.cardGen.generate();
this.context.data.card = card;
return { success: true, data: card };
},
saveToDatabase: async () => {
await this.db.saveAccount(this.context.data.account);
await this.db.saveCard(this.context.data.card);
return { success: true };
}
};
}
}
```
---
## ✅ 迁移优势
1. **类型安全** - 全TypeScript编译时检查
2. **统一规范** - 所有工具实现ITool接口
3. **易测试** - 接口明确方便mock
4. **可扩展** - 新工具遵循同样规范
5. **解耦** - 工具独立adapter组合使用
---
## 📅 迁移计划
### 第一阶段:核心工具(当前)
- [x] ITool接口规范
- [x] AccountGenerator
- [ ] DatabaseClient优先
- [ ] CardGenerator
### 第二阶段:扩展工具
- [ ] EmailHandler
- [ ] CaptchaSolver
### 第三阶段:测试和优化
- [ ] 单元测试
- [ ] 集成测试
- [ ] 性能优化
---
## 🚀 开始使用
当前可以直接使用:
```bash
# 只需设置AdsPower ID
export ADSPOWER_USER_ID="your-id"
# 运行(会自动生成账号)
pnpm run run -- windsurf
```
账号数据完全由 `AccountGenerator` 自动生成!

View File

@ -0,0 +1,55 @@
/**
* 配置检查工具
* 运行node check-config.js windsurf
*/
const siteName = process.argv[2] || 'windsurf';
const sitePrefix = siteName.toUpperCase().replace(/-/g, '_');
console.log('🔍 Configuration Checker\n');
console.log(`Site: ${siteName}\n`);
// 检查AdsPower
console.log('📌 AdsPower Config:');
console.log(` ADSPOWER_USER_ID: ${process.env.ADSPOWER_USER_ID ? '✅ Set' : '❌ Missing'}`);
console.log(` ADSPOWER_API: ${process.env.ADSPOWER_API || 'http://local.adspower.net:50325 (default)'}\n`);
// 检查账号信息
console.log(`📌 ${siteName} Account:`);
const fields = ['EMAIL', 'PASSWORD', 'FIRSTNAME', 'LASTNAME', 'USERNAME', 'PHONE'];
fields.forEach(field => {
const envKey = `${sitePrefix}_${field}`;
const value = process.env[envKey];
const status = value ? '✅' : '⚠️';
const display = value ? (field.includes('PASSWORD') ? '***' : value) : 'Not set';
console.log(` ${envKey}: ${status} ${display}`);
});
// 检查配置文件
const fs = require('fs');
const path = require('path');
const configPath = path.join(__dirname, 'configs', 'sites', `${siteName}.yaml`);
console.log(`\n📌 Config File:`);
console.log(` Path: ${configPath}`);
console.log(` Exists: ${fs.existsSync(configPath) ? '✅' : '❌'}`);
// 总结
console.log('\n' + '='.repeat(60));
const adsOk = !!process.env.ADSPOWER_USER_ID;
const emailOk = !!process.env[`${sitePrefix}_EMAIL`];
const passOk = !!process.env[`${sitePrefix}_PASSWORD`];
const configOk = fs.existsSync(configPath);
if (adsOk && emailOk && passOk && configOk) {
console.log('✅ Ready to run!');
console.log(`\nRun: pnpm run run -- ${siteName}`);
} else {
console.log('❌ Missing required configuration:');
if (!adsOk) console.log(' - Set ADSPOWER_USER_ID');
if (!emailOk) console.log(` - Set ${sitePrefix}_EMAIL`);
if (!passOk) console.log(` - Set ${sitePrefix}_PASSWORD`);
if (!configOk) console.log(` - Add ${siteName}.yaml to configs/sites/`);
}
console.log('='.repeat(60));

View File

@ -0,0 +1,295 @@
/**
*
* YAML配置自动化执行任意网站的操作流程
*
*
* npm run run -- windsurf # Windsurf自动化
* npm run run -- stripe # Stripe自动化
* npm run run -- <网站名> #
*
* configs/sites/<网站名>.yaml
*/
import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'js-yaml';
import { AdsPowerProvider } from '../src/providers/adspower/AdsPowerProvider';
import { WorkflowEngine } from '../src/workflow/WorkflowEngine';
import { ISiteAdapter, EmptyAdapter } from '../src/adapters/ISiteAdapter';
interface WorkflowConfig {
site: string;
workflow: any[];
errorHandling?: any;
variables?: any;
url?: string;
siteConfig?: {
url?: string;
name?: string;
[key: string]: any;
};
}
class AutomationRunner {
private siteName: string;
private configPath: string;
private adapterPath: string;
private adapter: ISiteAdapter | null = null;
constructor(siteName: string) {
this.siteName = siteName;
this.configPath = path.join(__dirname, '../configs/sites', `${siteName}.yaml`);
this.adapterPath = path.join(__dirname, '../configs/sites', `${siteName}-adapter.ts`);
}
async run() {
console.log('🚀 Browser Automation Executor\n');
console.log(`Site: ${this.siteName}`);
console.log(`Config: ${this.configPath}\n`);
// 1. 检查配置文件
if (!fs.existsSync(this.configPath)) {
console.error(`❌ Config file not found: ${this.configPath}`);
console.log('\n💡 Available configs:');
this.listAvailableConfigs();
process.exit(1);
}
// 2. 加载配置
const config = this.loadConfig();
console.log(`✅ Loaded workflow with ${config.workflow.length} steps\n`);
// 2.5. 加载Adapter如果存在
await this.loadAdapter();
// 3. 初始化Provider
const provider = await this.initializeProvider();
try {
// 4. 启动浏览器
const result = await provider.launch();
console.log('✅ Browser launched successfully\n');
// 5. 准备Context
const context = this.buildContext(result, config);
// 5.5. 初始化Adapter
if (this.adapter) {
await this.adapter.initialize(context);
if (this.adapter.beforeWorkflow) {
await this.adapter.beforeWorkflow(context);
}
}
// 6. 创建并执行WorkflowEngine
const engine = new WorkflowEngine(
config.workflow,
context,
provider.getActionFactory()
);
console.log('▶️ Starting workflow execution...\n');
const workflowResult = await engine.execute();
// 7. 显示结果
this.displayResults(workflowResult, config.workflow.length);
// 7.5. Adapter后处理
if (this.adapter && this.adapter.afterWorkflow) {
await this.adapter.afterWorkflow(context, workflowResult);
}
// 8. 等待查看
console.log('⏸️ Waiting 5 seconds before closing...');
await new Promise(resolve => setTimeout(resolve, 5000));
} catch (error: any) {
console.error('\n❌ Fatal error:', error.message);
console.error(error.stack);
} finally {
await this.cleanup(provider);
}
}
private loadConfig(): WorkflowConfig {
const content = fs.readFileSync(this.configPath, 'utf8');
return yaml.load(content) as WorkflowConfig;
}
/**
*
*/
private async loadAdapter(): Promise<void> {
// 检查是否存在adapter文件支持.ts和.js
const tsPath = this.adapterPath;
const jsPath = this.adapterPath.replace('.ts', '.js');
let adapterPath: string | null = null;
if (fs.existsSync(tsPath)) {
adapterPath = tsPath;
} else if (fs.existsSync(jsPath)) {
adapterPath = jsPath;
}
if (!adapterPath) {
console.log(`⚠️ No adapter found for ${this.siteName}, using empty adapter`);
this.adapter = new EmptyAdapter();
return;
}
try {
console.log(`🔌 Loading adapter: ${path.basename(adapterPath)}`);
// 动态导入adapter
const adapterModule = await import(adapterPath);
const AdapterClass = adapterModule.default;
if (!AdapterClass) {
throw new Error('Adapter must have a default export');
}
this.adapter = new AdapterClass();
console.log(`✅ Adapter loaded: ${this.adapter!.name}\n`);
} catch (error: any) {
console.error(`❌ Failed to load adapter: ${error.message}`);
console.log('Using empty adapter as fallback\n');
this.adapter = new EmptyAdapter();
}
}
private async initializeProvider(): Promise<AdsPowerProvider> {
console.log('🌐 Initializing AdsPower Provider...');
return new AdsPowerProvider({
profileId: process.env.ADSPOWER_USER_ID,
siteName: this.siteName.charAt(0).toUpperCase() + this.siteName.slice(1)
});
}
private buildContext(launchResult: any, config: WorkflowConfig): any {
// 创建空context - Adapter负责填充数据
return {
page: launchResult.page,
browser: launchResult.browser,
logger: console,
data: {
account: {}, // Adapter会在beforeWorkflow中填充
...config.variables
},
siteConfig: config.siteConfig || {
url: config.url || '',
name: config.site || this.siteName
},
config: config,
siteName: this.siteName,
// 注入adapter的handlers
adapter: this.adapter ? this.adapter.getHandlers() : {}
};
}
private displayResults(result: any, totalSteps: number): void {
console.log('\n' + '='.repeat(60));
console.log('📊 Workflow Execution Summary');
console.log('='.repeat(60));
console.log(`Site: ${this.siteName}`);
console.log(`Status: ${result.success ? '✅ SUCCESS' : '❌ FAILED'}`);
console.log(`Steps Completed: ${result.steps}/${totalSteps}`);
console.log(`Duration: ${(result.duration / 1000).toFixed(2)}s`);
console.log(`Errors: ${result.errors.length}`);
if (result.errors.length > 0) {
console.log('\n❌ Errors:');
result.errors.forEach((err: any, i: number) => {
console.log(` ${i + 1}. Step ${err.step} (${err.name}): ${err.error}`);
});
}
console.log('='.repeat(60) + '\n');
}
private async cleanup(provider: AdsPowerProvider): Promise<void> {
try {
console.log('\n🔒 Closing browser...');
await provider.close();
console.log('✅ Browser closed successfully');
// 清理adapter资源
if (this.adapter && this.adapter.cleanup) {
await this.adapter.cleanup();
}
} catch (e: any) {
console.error('⚠️ Error closing browser:', e.message);
}
}
private listAvailableConfigs(): void {
const configsDir = path.join(__dirname, '../configs/sites');
if (!fs.existsSync(configsDir)) {
console.log(' (No configs directory found)');
return;
}
const files = fs.readdirSync(configsDir)
.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
.map(f => f.replace(/\.(yaml|yml)$/, ''));
if (files.length === 0) {
console.log(' (No config files found)');
} else {
files.forEach(name => console.log(` - ${name}`));
}
}
}
// 主程序
async function main() {
const args = process.argv.slice(2);
// 参数格式node cli/run.js <browser-provider> <browser-params> <site>
// 示例node cli/run.js adspower k1728p8l windsurf
if (args.length < 3) {
console.error('❌ Usage: node cli/run.js <browser-provider> <browser-params> <site>');
console.error('\nArguments:');
console.error(' browser-provider 浏览器提供商 (adspower, playwright, etc)');
console.error(' browser-params 浏览器参数 (AdsPower的profileId, 或 - 表示默认)');
console.error(' site 网站名称 (windsurf, stripe, etc)\n');
console.error('Examples:');
console.error(' node cli/run.js adspower k1728p8l windsurf');
console.error(' node cli/run.js playwright - windsurf');
console.error(' node cli/run.js adspower j9abc123 stripe\n');
process.exit(1);
}
const browserProvider = args[0];
const browserParams = args[1];
const siteName = args[2];
console.log('🚀 Browser Automation Executor\n');
console.log(`Browser: ${browserProvider}`);
console.log(`Params: ${browserParams}`);
console.log(`Site: ${siteName}\n`);
// 设置浏览器参数
if (browserProvider === 'adspower') {
if (browserParams !== '-') {
process.env.ADSPOWER_USER_ID = browserParams;
console.log(`📍 AdsPower Profile: ${browserParams}`);
}
}
const runner = new AutomationRunner(siteName);
try {
await runner.run();
console.log('\n✅ Automation completed successfully!');
process.exit(0);
} catch (error: any) {
console.error('\n❌ Automation failed:', error.message);
console.error(error.stack);
process.exit(1);
}
}
main();

View File

@ -0,0 +1,386 @@
/**
* Windsurf网站适配器
* 使
*/
import { BaseAdapter } from '../../src/adapters/BaseAdapter';
import { AccountGeneratorTool, AccountData } from '../../src/tools/AccountGeneratorTool';
import { DatabaseTool } from '../../src/tools/DatabaseTool';
import { EmailTool } from '../../src/tools/EmailTool';
import { CardGeneratorTool } from '../../src/tools/card/CardGeneratorTool';
export class WindsurfAdapter extends BaseAdapter {
readonly name = 'windsurf';
/**
*
*/
protected getRequiredTools(): string[] {
return ['account-generator', 'database', 'email', 'card-generator'];
}
/**
*
*/
protected registerTools(): void {
// 注册账号生成器Tool实例 + 配置)
this.registerTool(
new AccountGeneratorTool(),
{
email: {
domain: 'qichen111.asia'
},
password: {
strategy: 'email',
length: 12
},
includePhone: true
}
);
// 注册数据库工具(使用旧框架的真实配置)
this.registerTool(
new DatabaseTool(),
{
type: 'mysql',
host: process.env.MYSQL_HOST || '172.22.222.111',
port: parseInt(process.env.MYSQL_PORT || '3306'),
username: process.env.MYSQL_USER || 'windsurf-auto-register',
password: process.env.MYSQL_PASSWORD || 'Qichen5210523',
database: process.env.MYSQL_DATABASE || 'windsurf-auto-register'
}
);
// 注册邮箱工具使用QQ邮箱IMAP与旧框架配置一致
this.registerTool(
new EmailTool(),
{
type: 'imap',
user: process.env.EMAIL_USER || '1695551@qq.com',
password: process.env.EMAIL_PASS || 'iogmboamejdsbjdh', // QQ邮箱授权码
host: process.env.EMAIL_HOST || 'imap.qq.com',
port: parseInt(process.env.EMAIL_PORT || '993'),
tls: true,
tlsOptions: {
rejectUnauthorized: false
},
checkInterval: 3 // 每3秒检查一次与旧框架一致
}
);
// 注册卡片生成器
this.registerTool(
new CardGeneratorTool(),
{
type: 'unionpay'
// 注意:数据库工具会在初始化时自动传入
}
);
}
/**
* Workflow执行前 -
*/
async beforeWorkflow(context: any): Promise<void> {
console.log('▶️ Windsurf workflow starting...');
// 如果没有账号数据,自动生成
if (!context.data.account || !context.data.account.email) {
console.log('📝 Generating account data...');
const accountGen = this.getTool<AccountGeneratorTool>('account-generator');
context.data.account = await accountGen.generate();
console.log(`✓ Generated: ${context.data.account.email}`);
}
}
/**
* Workflow执行后 -
*/
async afterWorkflow(context: any, result: any): Promise<void> {
console.log('✅ Windsurf workflow completed');
// TODO: 保存数据到数据库
// const db = this.getTool<DatabaseTool>('database');
// await db.save('accounts', context.data.account);
}
/**
* custom action的处理函数
*/
getHandlers(): Record<string, (...args: any[]) => Promise<any>> {
return {
// 生成银行卡使用CardGeneratorTool与旧框架100%一致)
generateCard: async () => {
console.log('💳 Generating card...');
const cardGen = this.getTool<CardGeneratorTool>('card-generator');
const card = await cardGen.generate('unionpay'); // 使用银联卡
console.log(`✓ Generated card: ${card.number.slice(-4)} (${card.issuer})`);
this.context.data.card = card;
return { success: true, data: card };
},
// 处理Cloudflare Turnstile验证完全复制旧框架逻辑
handleTurnstile: async (params: any) => {
const {
timeout = 30000,
maxRetries = 3,
retryStrategy = 'refresh'
} = params;
const page = this.context.page;
for (let retryCount = 0; retryCount <= maxRetries; retryCount++) {
try {
if (retryCount > 0) {
console.warn(`Turnstile 超时,执行重试策略: ${retryStrategy} (${retryCount}/${maxRetries})...`);
// 根据策略执行不同的重试行为
await this.executeRetryStrategy(retryStrategy, retryCount);
}
console.log('🔐 Cloudflare Turnstile 人机验证');
// 等待 Turnstile 验证框出现
await new Promise(resolve => setTimeout(resolve, 2000));
// 检查是否有 Turnstile
const hasTurnstile = await 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) {
console.log('检测到 Turnstile 验证,等待自动完成...');
// 等待验证通过(检查按钮是否启用或页面是否变化)
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const isPassed = await 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) {
console.log('✅ Turnstile 验证通过');
// 点击 Continue 按钮
const continueBtn = await page.evaluateHandle(() => {
return Array.from(document.querySelectorAll('button')).find(btn =>
btn.textContent.trim() === 'Continue'
);
});
if (continueBtn) {
await continueBtn.asElement().click();
console.log('已点击 Continue 按钮');
await new Promise(resolve => setTimeout(resolve, 2000));
}
return { success: true };
}
await new Promise(resolve => setTimeout(resolve, 500));
}
// 超时了,如果还有重试次数就继续循环
if (retryCount < maxRetries) {
console.warn(`Turnstile 验证超时(${timeout}ms`);
continue; // 进入下一次重试
} else {
throw new Error('Turnstile 验证超时,已达最大重试次数');
}
} else {
console.log('未检测到 Turnstile跳过');
return { success: true, skipped: true };
}
} catch (error: any) {
if (retryCount >= maxRetries) {
console.error(`Turnstile 处理最终失败: ${error.message}`);
// Turnstile 是可选的,失败也继续(但记录错误)
return { success: true, error: error.message, failed: true };
}
// 否则继续重试
}
}
},
// 处理邮箱验证使用EmailTool与旧框架逻辑100%一致)
handleEmailVerification: async (params: any) => {
const { timeout = 120000 } = params;
const page = this.context.page;
console.log('开始邮箱验证');
const emailTool = this.getTool<EmailTool>('email');
try {
// 等待2秒让邮件到达
await new Promise(resolve => setTimeout(resolve, 2000));
// 获取验证码
const email = this.context.data.account.email;
console.log(`从邮箱获取验证码: ${email}`);
const code = await emailTool.getVerificationCode(
'windsurf',
email,
timeout / 1000
);
console.log(`✓ 验证码: ${code}`);
// 等待输入框出现
await page.waitForSelector('input[type="text"]', { timeout: 10000 });
// 获取所有输入框
const inputs = await page.$$('input[type="text"]');
console.log(`找到 ${inputs.length} 个输入框`);
if (inputs.length >= 6 && code.length === 6) {
// 填写6位验证码
console.log('填写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));
}
console.log('✓ 验证码已填写');
// 等待跳转到问卷页面
console.log('等待页面跳转...');
const startTime = Date.now();
while (Date.now() - startTime < 60000) {
const currentUrl = page.url();
if (currentUrl.includes('/account/onboarding') && currentUrl.includes('page=source')) {
console.log('✓ 邮箱验证成功');
return { success: true };
}
await new Promise(resolve => setTimeout(resolve, 500));
}
throw new Error('等待页面跳转超时');
} else {
throw new Error('输入框数量不正确');
}
} catch (error: any) {
console.error(`邮箱验证失败: ${error.message}`);
throw error;
}
},
// 处理hCaptcha
handleHCaptcha: async () => {
console.log('🤖 Waiting for hCaptcha...');
await new Promise(resolve => setTimeout(resolve, 60000));
return { success: true };
},
// 验证提交按钮点击
verifySubmitClick: async () => {
console.log('✓ Verifying submit button click...');
await new Promise(resolve => setTimeout(resolve, 2000));
return { success: true };
},
// 处理订阅数据
processSubscriptionData: async () => {
console.log('📊 Processing subscription data...');
const quotaData = this.context.data.quotaRaw;
const billingInfo = this.context.data.billingInfo;
console.log('Quota:', quotaData);
console.log('Billing:', billingInfo);
return { success: true };
},
// 保存到数据库(完全复制旧框架逻辑)
saveToDatabase: async () => {
console.log('保存到数据库');
try {
const db = this.getTool<DatabaseTool>('database');
const account = this.context.data.account;
const card = this.context.data.card;
const quotaInfo = this.context.data.quotaInfo;
const billingInfo = this.context.data.billingInfo;
// 准备账号数据(与旧框架字段完全一致)
const accountData = {
email: account.email,
password: account.password,
first_name: account.firstName,
last_name: account.lastName,
registration_time: new Date(),
quota_used: quotaInfo ? parseFloat(quotaInfo.used) : 0,
quota_total: quotaInfo ? parseFloat(quotaInfo.total) : 0,
billing_days: billingInfo ? parseInt(billingInfo.days) : null,
billing_date: billingInfo ? billingInfo.date : null,
payment_card_number: card ? card.number : null,
payment_card_expiry_month: card ? card.month : null,
payment_card_expiry_year: card ? card.year : null,
payment_card_cvv: card ? card.cvv : null,
payment_country: card ? card.country || 'MO' : 'MO',
status: 'active',
is_on_sale: false
};
// 保存到数据库(使用旧框架的表名)
console.log('保存账号信息...');
// 检查是否已存在
const exists = await db.exists('windsurf_accounts', { email: accountData.email });
let accountId;
if (exists) {
// 已存在,更新
await db.update('windsurf_accounts', { email: accountData.email }, accountData);
console.log(`✓ 账号信息已更新: ${accountData.email}`);
} else {
// 不存在,插入
const result = await db.insert('windsurf_accounts', accountData);
accountId = result.insertId;
console.log(`✓ 账号信息已保存到数据库 (ID: ${accountId})`);
}
console.log(` → 邮箱: ${accountData.email}`);
console.log(` → 配额: ${accountData.quota_used} / ${accountData.quota_total}`);
console.log(` → 卡号: ${accountData.payment_card_number}`);
return { success: true, accountId };
} catch (error: any) {
console.error(`保存到数据库失败: ${error.message}`);
// 数据库保存失败不影响注册流程
return { success: true, error: error.message };
}
}
};
}
}
// 导出adapter实例
export default WindsurfAdapter;

View File

@ -0,0 +1,335 @@
# Windsurf 注册自动化配置
site:
name: Windsurf
url: https://windsurf.com/refer?referral_code=55424ec434
# 工作流定义
workflow:
# ==================== 步骤 0: 处理邀请链接 ====================
- action: navigate
name: "打开邀请链接"
url: "https://windsurf.com/refer?referral_code=55424ec434"
options:
waitUntil: 'networkidle2'
timeout: 30000
- action: click
name: "点击接受邀请"
selector:
- text: 'Sign up to accept referral'
selector: 'button'
- css: 'button.bg-sk-aqua'
- css: 'button:has-text("Sign up to accept referral")'
timeout: 30000
waitForNavigation: true
# 验证跳转到注册页面带referral_code
- action: verify
name: "验证跳转到注册页面"
conditions:
success:
- urlContains: "/account/register"
- urlContains: "referral_code=55424ec434"
timeout: 10000
# ==================== 步骤 1: 打开注册页面并填写信息(带重试) ====================
- action: retryBlock
name: "打开注册页面并填写基本信息"
maxRetries: 3
retryDelay: 3000
steps:
# 1.1 验证注册页面元素已加载
- action: wait
name: "等待注册表单加载"
type: element
selector: '#firstName'
timeout: 15000
# 1.2 填写基本信息
- action: fillForm
name: "填写基本信息"
fields:
firstName: "{{account.firstName}}"
lastName: "{{account.lastName}}"
email: "{{account.email}}"
# 1.3 勾选同意条款
- action: click
name: "勾选同意条款"
selector:
- css: 'input[type="checkbox"]'
optional: true
# 1.4 点击 Continue
- action: click
name: "点击 Continue (基本信息)"
selector:
- text: 'Continue'
selector: 'button, a' # 明确指定查找范围:按钮或链接
timeout: 30000
# 验证点击后密码页面出现
verifyAfter:
appears:
- '#password'
- 'input[type="password"]'
# ==================== 步骤 2: 设置密码 ====================
- action: fillForm
name: "设置密码"
fields:
password: "{{account.password}}"
passwordConfirmation: "{{account.password}}"
- action: click
name: "提交密码"
selector:
- text: 'Continue'
selector: 'button, a'
timeout: 30000
# ==================== 步骤 2.5: Cloudflare Turnstile 验证 ====================
- action: custom
name: "Cloudflare Turnstile 验证"
handler: "handleTurnstile"
params:
timeout: 30000
maxRetries: 3
# 重试策略:
# - 'refresh': 刷新页面(适用于保持状态的网站)
# - 'restart': 刷新后重新填写(适用于刷新=重置的网站,如 Windsurf
# - 'wait': 只等待不刷新
retryStrategy: 'restart' # Windsurf 刷新会回到第一步
optional: true
# ==================== 步骤 3: 邮箱验证 ====================
# 需要自定义处理:获取邮件验证码 + 填写6个输入框
- action: custom
name: "邮箱验证"
handler: "handleEmailVerification"
params:
timeout: 120000
# ==================== 步骤 4: 跳过问卷调查 ====================
- action: click
name: "跳过问卷"
selector:
- text: 'Skip this step'
selector: 'button, a'
- text: 'skip'
selector: 'button, a'
exact: false
# ==================== 步骤 5: 选择计划 ====================
- action: click
name: "选择计划"
selector:
- text: 'Select plan'
selector: 'button, a'
timeout: 30000
# 等待跳转到 Stripe 支付页面(通用 URL 等待)
- action: wait
name: "等待跳转到 Stripe"
type: url
urlContains: "stripe.com"
timeout: 20000
# ==================== 步骤 6: 填写支付信息(带重试) ====================
- action: retryBlock
name: "提交支付并验证"
maxRetries: 9999 # 无限重试,直到成功
retryDelay: 15000 # 增加到15秒避免触发Stripe风控
onRetryBefore:
# 生成银行卡(第一次或重试都生成新卡)
- action: custom
handler: "generateCard"
steps:
# 6.1 选择银行卡支付方式(每次重试都重新选择)
- action: click
name: "选择银行卡支付"
selector:
- css: 'input[type="radio"][value="card"]'
verifyAfter:
checked: true # 验证 radio button 是否选中
# 填写支付信息(银行卡+国家+地址)
- action: fillForm
name: "填写支付信息"
fields:
cardNumber: "{{card.number}}"
cardExpiry: "{{card.month}}{{card.year}}"
cardCvc: "{{card.cvv}}"
billingName: "{{account.firstName}} {{account.lastName}}"
billingCountry:
find:
- css: '#billingCountry'
value: "MO"
type: "select"
addressLine1:
find:
- css: '#billingAddressLine1' # 使用 id 选择器,兼容所有语言
- css: 'input[name="billingAddressLine1"]'
- css: 'input[placeholder*="地址"]'
- css: 'input[placeholder*="Address"]'
- css: 'input[placeholder*="Alamat"]' # 印尼语
value: "kowloon"
# 滚动到页面底部(确保订阅按钮可见)
- action: scroll
name: "滚动到订阅按钮"
type: bottom
# 提交支付(内部会等待按钮变为可点击状态)
- action: click
name: "点击提交支付"
selector:
- css: 'button[data-testid="hosted-payment-submit-button"]' # Stripe 官方 testid
- css: 'button.SubmitButton' # Stripe 按钮类名
- css: 'button[type="submit"]' # 通用 submit 按钮
- css: 'button:has(.SubmitButton-IconContainer)' # 包含 icon 容器的按钮
timeout: 30000 # 给足够时间等待按钮从 disabled 变为可点击
waitForEnabled: true # 循环等待按钮激活(不是等待出现,而是等待可点击)
# 验证点击是否生效(检查按钮状态变化)
- action: custom
name: "验证订阅按钮点击生效"
handler: "verifySubmitClick"
# 处理 hCaptcha等待60秒让用户手动完成
- action: custom
name: "hCaptcha 验证"
handler: "handleHCaptcha"
# 验证支付结果(轮询检测成功或失败)
- action: verify
name: "验证支付结果"
conditions:
success:
- urlNotContains: "stripe.com"
- urlNotContains: "checkout.stripe.com"
failure:
# 英文
- textContains: "card was declined"
- textContains: "Your card was declined"
- textContains: "declined"
- textContains: "We are unable to authenticate your payment method"
# 中文
- textContains: "卡片被拒绝"
- textContains: "我们无法验证您的支付方式"
- textContains: "我们未能验证您的支付方式"
- textContains: "请选择另一支付方式并重试"
# 马来语
- textContains: "Kad anda telah ditolak" # 您的卡已被拒绝
- textContains: "Kami tidak dapat mensahihkan kaedah pembayaran"
- textContains: "Sila pilih kaedah pembayaran yang berbeza"
# 印尼语
- textContains: "Kami tidak dapat memverifikasi metode pembayaran"
- textContains: "Silakan pilih metode pembayaran"
- textContains: "kartu ditolak"
# 泰语
- textContains: "บัตรของคุณถูกปฏิเสธ" # 您的卡被拒绝
- textContains: "ไม่สามารถยืนยัน" # 无法验证
- textContains: "เราตรวจสอบสิทธิ์วิธีการชำระเงินของคุณไม่ได้" # 我们无法验证您的支付方式
- textContains: "โปรดเลือกวิธีการชำระเงินอื่น" # 请选择另一个支付方式
# 通用错误元素
- elementExists: ".error-message"
timeout: 15000
pollInterval: 500
onFailure: "throw"
# ==================== 步骤 7: 获取订阅信息 ====================
# 7.1 跳转到订阅使用页面
- action: navigate
name: "跳转订阅页面"
url: "https://windsurf.com/subscription/usage"
# 7.2 提取配额信息
- action: extract
name: "提取配额信息"
selector: "p.caption1.font-medium.text-sk-black\\/80 span.caption3 span"
multiple: true
contextKey: "quotaRaw"
required: false
# 7.3 提取账单周期信息
- action: extract
name: "提取账单周期"
selector: "p.caption1"
extractType: "text"
filter:
contains: "Next billing cycle"
regex: "(\\d+)\\s+days?.*on\\s+([A-Za-z]+\\s+\\d+,\\s+\\d{4})"
saveTo:
days: "$1"
date: "$2"
contextKey: "billingInfo"
required: false
# 7.4 处理提取的数据(自定义)
- action: custom
name: "处理订阅数据"
handler: "processSubscriptionData"
optional: true
# ==================== 步骤 8: 保存到数据库 ====================
- action: custom
name: "保存到数据库"
handler: "saveToDatabase"
optional: true
# ==================== 步骤 9: 退出登录 ====================
# 9.1 滚动到页面底部
- action: scroll
name: "滚动到页面底部"
direction: "bottom"
# 9.2 等待滚动完成
- action: wait
name: "等待滚动完成"
duration: 1000
# 9.3 点击退出登录
- action: click
name: "点击退出登录"
selector:
- css: 'div.body3.cursor-pointer:has-text("Log out")'
- css: 'div.body3:has-text("Log out")'
- xpath: '//div[contains(@class, "body3") and contains(text(), "Log out")]'
- text: "Log out"
timeout: 15000
waitForNavigation: true
# 9.4 验证页面已变化(离开订阅页面)
- action: verify
name: "验证页面已变化"
conditions:
success:
- or:
- urlContains: "/account/login"
- urlContains: "/login"
- urlContains: "/"
failure:
- urlContains: "/subscription/usage"
timeout: 10000
optional: true
# 9.5 验证跳转到登录页
- action: verify
name: "验证跳转到登录页"
conditions:
success:
- or:
- urlContains: "/account/login"
- urlContains: "/login"
timeout: 5000
optional: true
# 错误处理配置
errorHandling:
screenshot: true
retry:
enabled: true
maxAttempts: 3
delay: 2000

View File

@ -0,0 +1,198 @@
# 架构设计文档
## 核心设计原则
**分层原则:通用组件 vs Provider特定组件**
---
## 📐 架构图
```
┌─────────────────────────────────────────┐
│ 通用层 (Universal Layer) │
│ 不调用浏览器特定API所有Provider共享 │
├─────────────────────────────────────────┤
│ - WorkflowEngine │
│ - 接口定义 (IAction, ISmartSelector) │
│ - 类型定义 (Types) │
│ - 抽象基类 (BaseAction, BaseProvider) │
└──────────────┬──────────────────────────┘
↓ 依赖接口
┌─────────────────────────────────────────┐
│ Provider特定层 (Provider Layer) │
│ 调用浏览器特定API每个Provider独立实现 │
├─────────────────────────────────────────┤
│ - Actions (click, input, etc.) │
│ - SmartSelector │
│ - ActionFactory │
│ - BrowserProvider │
└─────────────────────────────────────────┘
```
---
## 🗂️ 目录结构
```
src/
├── core/ # 通用核心层 ✅
│ ├── interfaces/ # 接口定义
│ │ ├── IBrowserProvider.ts # Provider接口
│ │ ├── IAction.ts # Action接口
│ │ └── ISmartSelector.ts # SmartSelector接口
│ ├── base/ # 抽象基类
│ │ ├── BaseBrowserProvider.ts
│ │ └── BaseAction.ts
│ └── types/ # 类型定义
│ └── index.ts
├── workflow/ # 通用工作流 ✅
│ └── WorkflowEngine.ts # 工作流引擎(通用!)
├── factory/ # 工厂模式 ✅
│ └── BrowserFactory.ts
└── providers/ # Provider实现层 ❌
├── adspower/ # AdsPower (Puppeteer)
│ ├── AdsPowerProvider.ts # Provider实现
│ ├── actions/ # AdsPower专用Actions
│ │ ├── ClickAction.ts
│ │ ├── InputAction.ts
│ │ └── ...
│ └── core/ # AdsPower专用Core
│ ├── SmartSelector.ts # Puppeteer实现
│ └── ActionFactory.ts
└── playwright/ # Playwright (未来)
├── PlaywrightProvider.ts
├── actions/ # Playwright专用Actions
└── core/
├── SmartSelector.ts # Playwright实现
└── ActionFactory.ts
```
---
## 📊 组件分类
### ✅ 通用组件所有Provider共享
| 组件 | 位置 | 职责 |
|------|------|------|
| **WorkflowEngine** | `workflow/` | 执行工作流调用Action接口 |
| **接口定义** | `core/interfaces/` | 定义规范IAction, ISmartSelector等 |
| **类型定义** | `core/types/` | 配置、结果等类型 |
| **抽象基类** | `core/base/` | 通用实现逻辑 |
| **BrowserFactory** | `factory/` | 创建Provider实例 |
**特点:**
- ✅ 不调用浏览器特定API
- ✅ 只依赖接口,不依赖实现
- ✅ 所有Provider可以共享代码
---
### ❌ Provider特定组件
| 组件 | 位置 | 职责 | 原因 |
|------|------|------|------|
| **Actions** | `providers/*/actions/` | 执行具体操作 | 需要调用`page.click()`等API |
| **SmartSelector** | `providers/*/core/` | 查找元素 | 需要调用`page.waitForSelector()`等 |
| **ActionFactory** | `providers/*/core/` | 创建Action实例 | 返回Provider特定的Action类 |
| **Provider** | `providers/*/` | 管理浏览器 | 调用Puppeteer/Playwright等API |
**特点:**
- ❌ 调用浏览器特定API
- ❌ 每个Provider独立实现
- ❌ 不能跨Provider共享
---
## 💡 为什么这样设计?
### WorkflowEngine为什么是通用的
```typescript
// WorkflowEngine只调用接口不依赖具体实现
class WorkflowEngine {
async executeStep(step) {
// 从Factory获取Action多态
const ActionClass = this.actionFactory.getAction(step.action);
const action = new ActionClass(step, this.context);
// 执行通过接口调用不关心Puppeteer还是Playwright
await action.execute();
}
}
```
**关键:** 只依赖`IAction`接口,不知道是`PuppeteerClickAction`还是`PlaywrightClickAction`
---
### SmartSelector为什么是Provider特定的
```typescript
// Puppeteer版本
class PuppeteerSmartSelector {
async find(timeout) {
return await this.page.waitForSelector(selector, { timeout });
// ↑ Puppeteer特定API
}
}
// Playwright版本API不同
class PlaywrightSmartSelector {
async find(timeout) {
return await this.page.locator(selector).waitFor({ timeout });
// ↑ Playwright特定API
}
}
```
**关键:** 必须直接调用浏览器API无法抽象
---
## 🔄 工作流程
```
1. 用户创建Provider
BrowserFactory.create('adspower')
2. WorkflowEngine执行
new WorkflowEngine(workflow, context, actionFactory)
3. 执行每个步骤
actionFactory.getAction('click') // 获取AdsPower的ClickAction
action.execute() // 调用Puppeteer API
```
**关键:** WorkflowEngine不知道也不关心是哪个Provider
---
## ✅ 总结
| 层次 | 组件 | 通用/特定 | 原因 |
|------|------|----------|------|
| **业务层** | WorkflowEngine | ✅ 通用 | 只调用接口 |
| **接口层** | IAction, ISmartSelector | ✅ 通用 | 定义规范 |
| **实现层** | Actions, SmartSelector | ❌ Provider特定 | 调用浏览器API |
| **基础层** | Provider | ❌ Provider特定 | 管理浏览器 |
**设计原则:**
- 依赖倒置:上层依赖接口,不依赖实现
- 开闭原则添加新Provider不修改通用层
- 单一职责通用层负责流程Provider层负责实现
---
**创建时间:** 2025-11-21
**状态:** 已修正 ✅

View File

@ -0,0 +1,65 @@
# Getting Started
## 安装依赖
```bash
cd browser-automation-ts
npm install
```
## 编译TypeScript
```bash
npm run build
```
## 运行测试
```bash
npm test
```
## 基本使用
```typescript
import { BrowserFactory, BrowserProviderType } from './src';
// 创建AdsPower Provider
const provider = BrowserFactory.create(BrowserProviderType.ADSPOWER, {
profileId: 'k1728p8l'
});
// 启动浏览器
const { browser, page } = await provider.launch();
// 使用浏览器
await page.goto('https://example.com');
// 关闭
await provider.close();
```
## 目录结构
```
src/
├── core/ # 核心抽象层
│ ├── interfaces/ # 接口定义(强制规范)
│ ├── base/ # 抽象基类(共享实现)
│ └── types/ # 类型定义
├── providers/ # Provider实现
│ └── adspower/ # AdsPower实现
│ ├── AdsPowerProvider.ts
│ ├── actions/ # AdsPower专用Actions
│ └── core/ # AdsPower专用Core
└── factory/ # 工厂类
```
## 下一步
1. 实现ActionsTODO
2. 实现WorkflowEngineTODO
3. 添加Playwright Provider
4. 完整测试

View File

@ -0,0 +1,10 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts'
]
};

View File

@ -0,0 +1,49 @@
{
"name": "browser-automation-ts",
"version": "2.0.0",
"description": "Enterprise Browser Automation Framework with TypeScript",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"test": "jest",
"test:watch": "jest --watch",
"run": "ts-node cli/run.ts",
"lint": "eslint src/**/*.ts",
"validate-provider": "ts-node scripts/validate-provider.ts"
},
"keywords": [
"browser",
"automation",
"puppeteer",
"playwright",
"typescript",
"oop"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/imap": "^0.8.42",
"@types/js-yaml": "^4.0.5",
"@types/mailparser": "^3.4.6",
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.0.0",
"jest": "^29.0.0",
"ts-jest": "^29.0.0",
"ts-node": "^10.0.0",
"typescript": "^5.0.0"
},
"dependencies": {
"axios": "^1.6.0",
"imap": "^0.8.19",
"js-yaml": "^4.1.0",
"mailparser": "^3.9.0",
"mysql2": "^3.15.3",
"puppeteer": "^21.0.0",
"reflect-metadata": "^0.2.2",
"typeorm": "^0.3.27"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,218 @@
/**
* Adapter -
*/
import { ISiteAdapter } from './ISiteAdapter';
import { ITool } from '../tools/ITool';
/**
* Adapter抽象基类
*/
export abstract class BaseAdapter implements ISiteAdapter {
abstract readonly name: string;
protected context: any;
private tools = new Map<string, ITool>();
private toolConfigs = new Map<string, any>();
/**
* -
*/
async initialize(context: any): Promise<void> {
this.context = context;
console.log(`🔧 Initializing ${this.name} adapter...`);
// 1. 子类注册工具
this.registerTools();
// 2. 验证必需工具
this.validateRequiredTools();
// 3. 初始化所有工具
await this.initializeTools();
console.log(`${this.name} adapter initialized\n`);
}
/**
* -
*/
protected abstract registerTools(): void;
/**
* -
*/
protected abstract getRequiredTools(): string[];
/**
* custom action handlers
*/
abstract getHandlers(): Record<string, (...args: any[]) => Promise<any>>;
/**
*
*/
protected registerTool(tool: ITool, config?: any): void {
if (this.tools.has(tool.name)) {
throw new Error(`Tool already registered: ${tool.name}`);
}
this.tools.set(tool.name, tool);
if (config) {
this.toolConfigs.set(tool.name, config);
}
console.log(` ✓ Registered: ${tool.name}`);
}
/**
*
*/
protected getTool<T extends ITool>(name: string): T {
const tool = this.tools.get(name);
if (!tool) {
throw new Error(
`Tool not found: ${name}. Available tools: ${Array.from(this.tools.keys()).join(', ')}`
);
}
return tool as T;
}
/**
*
*/
protected hasTool(name: string): boolean {
return this.tools.has(name);
}
/**
*
*/
private validateRequiredTools(): void {
const required = this.getRequiredTools();
const missing: string[] = [];
for (const toolName of required) {
if (!this.tools.has(toolName)) {
missing.push(toolName);
}
}
if (missing.length > 0) {
throw new Error(
`Missing required tools: ${missing.join(', ')}. Please register them in registerTools().`
);
}
}
/**
*
*/
private async initializeTools(): Promise<void> {
console.log(`\n Initializing ${this.tools.size} tools...`);
for (const [name, tool] of this.tools) {
try {
await tool.initialize(this.getToolConfig(name));
} catch (error: any) {
console.error(` ❌ Tool ${name} initialization error:`, error);
throw new Error(`Failed to initialize tool ${name}: ${error.message}\n${error.stack}`);
}
}
}
/**
*
*/
protected getToolConfig(toolName: string): any {
return this.toolConfigs.get(toolName);
}
/**
* Workflow执行前
*/
async beforeWorkflow?(context: any): Promise<void>;
/**
* Workflow执行后
*/
async afterWorkflow?(context: any, result: any): Promise<void>;
/**
*
*/
protected async executeRetryStrategy(
strategy: 'refresh' | 'restart' | 'wait',
retryCount: number,
options: any = {}
): Promise<void> {
const page = this.context?.page;
if (!page) {
console.warn('⚠️ No page available for retry strategy');
return;
}
switch (strategy) {
case 'refresh':
console.log('策略: 刷新当前页面');
await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
await new Promise(resolve => setTimeout(resolve, 3000));
break;
case 'restart':
// 重新开始流程(适用于刷新后回到初始状态的网站)
console.warn('策略: 刷新会重置,执行自定义恢复');
await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
await new Promise(resolve => setTimeout(resolve, 3000));
// 调用站点特定的恢复方法
if (this.onRestart && typeof this.onRestart === 'function') {
const restartSteps = await this.onRestart(options);
// 如果返回步骤名称数组,则重新执行这些步骤
if (Array.isArray(restartSteps) && restartSteps.length > 0) {
console.log(`需要重新执行 ${restartSteps.length} 个步骤`);
// TODO: 实现 rerunSteps 或通过引擎重新执行
}
} else {
console.warn('未定义 onRestart 方法,跳过恢复步骤');
}
break;
case 'wait':
const waitTime = options.waitTime || 10000;
console.log(`策略: 延长等待 ${waitTime}ms${retryCount} 次)`);
await new Promise(resolve => setTimeout(resolve, waitTime));
break;
default:
console.warn(`未知重试策略: ${strategy},使用默认等待`);
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
/**
*
* @returns void
*/
protected async onRestart?(options?: any): Promise<string[] | void>;
/**
*
*/
async cleanup(): Promise<void> {
console.log(`\n🧹 Cleaning up ${this.name} adapter...`);
for (const [name, tool] of this.tools) {
if (tool.cleanup) {
try {
await tool.cleanup();
} catch (error: any) {
console.error(` ⚠️ Failed to cleanup ${name}: ${error.message}`);
}
}
}
console.log(`${this.name} adapter cleaned up`);
}
}

View File

@ -0,0 +1,53 @@
/**
*
* adapter来集成所需的工具和业务逻辑
*/
export interface ISiteAdapter {
/**
*
*/
readonly name: string;
/**
*
*/
initialize(context: any): Promise<void>;
/**
* workflow执行前调用
*/
beforeWorkflow?(context: any): Promise<void>;
/**
* workflow执行后调用
*/
afterWorkflow?(context: any, result: any): Promise<void>;
/**
* custom action handlers
* key是handler名称value是处理函数
*/
getHandlers(): Record<string, (...args: any[]) => Promise<any>>;
/**
*
*/
cleanup?(): Promise<void>;
}
/**
*
*
*/
export class EmptyAdapter implements ISiteAdapter {
readonly name = 'empty';
async initialize(context: any): Promise<void> {
// 空实现
}
getHandlers(): Record<string, (...args: any[]) => Promise<any>> {
return {};
}
}

View File

@ -0,0 +1,68 @@
/**
* Action抽象基类
*/
import { IAction, IActionContext } from '../interfaces/IAction';
import { IActionConfig, IActionResult } from '../types';
export abstract class BaseAction<TConfig extends IActionConfig = IActionConfig>
implements IAction {
protected config: TConfig;
protected context: IActionContext;
private startTime: number = 0;
constructor(config: TConfig, context: IActionContext) {
this.config = config;
this.context = context;
}
// 抽象方法 - 子类必须实现
abstract execute(): Promise<IActionResult>;
// 通用方法
async validate(): Promise<boolean> {
return true;
}
log(level: string, message: string): void {
if (this.context.logger) {
this.context.logger.log(level, message);
} else {
console.log(`[${level}] ${message}`);
}
}
protected startTimer(): void {
this.startTime = Date.now();
}
protected getDuration(): number {
return Date.now() - this.startTime;
}
protected async delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
protected async retry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
delayMs: number = 1000
): Promise<T> {
let lastError: Error | undefined;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (i < maxRetries - 1) {
await this.delay(delayMs * (i + 1));
}
}
}
throw lastError;
}
}

View File

@ -0,0 +1,70 @@
/**
*
*
*/
import { IBrowserProvider } from '../interfaces/IBrowserProvider';
import {
IBrowserCapabilities,
ILaunchOptions,
ILaunchResult,
IProviderMetadata
} from '../types';
export abstract class BaseBrowserProvider implements IBrowserProvider {
protected config: any;
protected browser: any = null;
protected page: any = null;
constructor(config: any = {}) {
this.config = config;
}
// ========== 抽象方法 - 子类必须实现 ==========
abstract getName(): string;
abstract getVersion(): string;
abstract isFree(): boolean;
abstract getCapabilities(): IBrowserCapabilities;
abstract launch(options?: ILaunchOptions): Promise<ILaunchResult>;
abstract close(): Promise<void>;
abstract getActionFactory(): any;
// ========== 通用实现 ==========
getMetadata(): IProviderMetadata {
return {
name: this.getName(),
version: this.getVersion(),
free: this.isFree(),
capabilities: this.getCapabilities()
};
}
getBrowser(): any {
if (!this.browser) {
throw new Error('Browser not initialized. Call launch() first.');
}
return this.browser;
}
getPage(): any {
if (!this.page) {
throw new Error('Page not initialized. Call launch() first.');
}
return this.page;
}
async clearCache(): Promise<void> {
// 默认实现,子类可覆盖
console.warn('clearCache() not implemented for this provider');
}
async validateConfig(): Promise<boolean> {
// 默认通过,子类可覆盖
return true;
}
protected getConfig(): any {
return this.config;
}
}

View File

@ -0,0 +1,89 @@
/**
*
*/
export class AutomationError extends Error {
public context: any;
public timestamp: Date;
constructor(message: string, context: any = {}) {
super(message);
this.name = 'AutomationError';
this.context = context;
this.timestamp = new Date();
// 维持正确的原型链
Object.setPrototypeOf(this, AutomationError.prototype);
}
toJSON(): any {
return {
name: this.name,
message: this.message,
context: this.context,
timestamp: this.timestamp,
stack: this.stack
};
}
}
export class ElementNotFoundError extends AutomationError {
public selector: any;
constructor(selector: any, context: any = {}) {
super(`元素未找到: ${JSON.stringify(selector)}`, context);
this.name = 'ElementNotFoundError';
this.selector = selector;
Object.setPrototypeOf(this, ElementNotFoundError.prototype);
}
}
export class TimeoutError extends AutomationError {
public operation: string;
public timeout: number;
constructor(operation: string, timeout: number, context: any = {}) {
super(`操作超时: ${operation} (${timeout}ms)`, context);
this.name = 'TimeoutError';
this.operation = operation;
this.timeout = timeout;
Object.setPrototypeOf(this, TimeoutError.prototype);
}
}
export class ValidationError extends AutomationError {
public expected: any;
public actual: any;
constructor(message: string, expected: any, actual: any, context: any = {}) {
super(message, context);
this.name = 'ValidationError';
this.expected = expected;
this.actual = actual;
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
export class ConfigurationError extends AutomationError {
public configPath: string;
constructor(message: string, configPath: string, context: any = {}) {
super(message, context);
this.name = 'ConfigurationError';
this.configPath = configPath;
Object.setPrototypeOf(this, ConfigurationError.prototype);
}
}
export class RetryExhaustedError extends AutomationError {
public operation: string;
public attempts: number;
constructor(operation: string, attempts: number, context: any = {}) {
super(`重试次数用尽: ${operation} (${attempts} 次尝试)`, context);
this.name = 'RetryExhaustedError';
this.operation = operation;
this.attempts = attempts;
Object.setPrototypeOf(this, RetryExhaustedError.prototype);
}
}

View File

@ -0,0 +1,24 @@
/**
* Action接口
*/
import { IActionConfig, IActionResult } from '../types';
export interface IActionContext {
page: any;
browser: any;
logger: any;
data: Record<string, any>;
[key: string]: any;
}
export interface IAction {
execute(): Promise<IActionResult>;
validate(): Promise<boolean>;
log(level: string, message: string): void;
}
export interface IActionFactory {
getAction(actionName: string): new (context: IActionContext, config: IActionConfig) => IAction;
hasAction(actionName: string): boolean;
}

View File

@ -0,0 +1,37 @@
/**
*
* Provider必须实现此接口
*/
import {
IBrowserCapabilities,
ILaunchOptions,
ILaunchResult,
IProviderMetadata
} from '../types';
export interface IBrowserProvider {
// 元数据
getName(): string;
getVersion(): string;
isFree(): boolean;
getCapabilities(): IBrowserCapabilities;
getMetadata(): IProviderMetadata;
// 生命周期
launch(options?: ILaunchOptions): Promise<ILaunchResult>;
close(): Promise<void>;
// 浏览器访问
getBrowser(): any;
getPage(): any;
// 数据管理
clearCache(): Promise<void>;
// Actions (Provider特定)
getActionFactory(): any;
// 配置验证
validateConfig(): Promise<boolean>;
}

View File

@ -0,0 +1,33 @@
/**
* SmartSelector接口
* Provider实现
*/
export interface ISelectorConfig {
css?: string;
xpath?: string;
text?: string;
id?: string;
selector?: string | string[];
options?: {
exact?: boolean;
caseInsensitive?: boolean;
};
}
export interface ISmartSelector {
/**
*
* @param timeout
* @returns
*/
find(timeout?: number): Promise<any>;
}
/**
* SmartSelector静态工厂方法接口
*/
export interface ISmartSelectorConstructor {
new (config: ISelectorConfig | ISelectorConfig[], page: any): ISmartSelector;
fromConfig(config: ISelectorConfig | ISelectorConfig[], page: any): ISmartSelector;
}

View File

@ -0,0 +1,272 @@
/**
* -
* TypeScript版本Puppeteer
*/
import { Page, ElementHandle } from 'puppeteer';
interface TextOptions {
exact?: boolean;
caseInsensitive?: boolean;
selector?: string;
filterDisabled?: boolean;
filterHidden?: boolean;
}
interface Strategy {
type: string;
find: () => Promise<ElementHandle | null>;
}
export class SmartSelector {
private page: Page;
private strategies: Strategy[] = [];
constructor(page: Page) {
this.page = page;
}
/**
*
*/
static fromConfig(config: any, page: Page): SmartSelector {
const selector = new SmartSelector(page);
if (typeof config === 'string') {
// 简单 CSS 选择器
selector.css(config);
} else if (Array.isArray(config)) {
// 多策略
config.forEach((strategy: any) => {
if (strategy.css) selector.css(strategy.css);
if (strategy.xpath) selector.xpath(strategy.xpath);
if (strategy.text) {
const textOptions: TextOptions = {
exact: strategy.exact,
caseInsensitive: strategy.caseInsensitive,
selector: strategy.selector,
filterDisabled: strategy.filterDisabled,
filterHidden: strategy.filterHidden
};
selector.text(strategy.text, textOptions);
}
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) {
const textOptions: TextOptions = {
exact: config.exact,
caseInsensitive: config.caseInsensitive,
selector: config.selector,
filterDisabled: config.filterDisabled,
filterHidden: config.filterHidden
};
selector.text(config.text, textOptions);
}
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: string): this {
this.strategies.push({
type: 'css',
find: async () => await this.page.$(selector)
});
return this;
}
xpath(xpath: string): this {
this.strategies.push({
type: 'xpath',
find: async () => {
const elements = await this.page.$x(xpath);
return elements[0] || null;
}
});
return this;
}
/**
*
*/
text(text: string, options: TextOptions = {}): this {
const {
exact = true,
caseInsensitive = true,
selector = '*',
filterDisabled = false,
filterHidden = true
} = options;
this.strategies.push({
type: 'text',
find: async () => {
return await this._findByText(text, selector, { exact, caseInsensitive, filterDisabled, filterHidden });
}
});
return this;
}
/**
*
*/
private async _findByText(
searchText: string,
cssSelector: string,
options: Required<Omit<TextOptions, 'selector'>>
): Promise<ElementHandle | null> {
const { exact, caseInsensitive, filterDisabled, filterHidden } = options;
const element = await this.page.evaluateHandle(
({ searchText, exact, caseInsensitive, cssSelector, filterDisabled, filterHidden }) => {
const elements = Array.from(document.querySelectorAll(cssSelector));
for (const el of elements) {
// 过滤 disabled
if (filterDisabled && (el.tagName === 'BUTTON' || el.tagName === 'INPUT') && (el as any).disabled) {
continue;
}
// 过滤隐藏元素
if (filterHidden && (el as any).offsetParent === null) continue;
// 获取文本
const text = (el.textContent || '').trim();
if (!text) continue;
// 标准化空格
const normalizedText = text.replace(/\s+/g, ' ');
const normalizedSearch = searchText.replace(/\s+/g, ' ');
// 匹配
let matches = false;
if (exact) {
matches = caseInsensitive
? normalizedText.toLowerCase() === normalizedSearch.toLowerCase()
: normalizedText === normalizedSearch;
} else {
matches = caseInsensitive
? normalizedText.toLowerCase().includes(normalizedSearch.toLowerCase())
: normalizedText.includes(normalizedSearch);
}
if (matches) return el;
}
return null;
},
{ searchText, exact, caseInsensitive, cssSelector, filterDisabled, filterHidden }
);
if (element) {
const elementHandle = element.asElement();
if (elementHandle) return elementHandle;
}
return null;
}
placeholder(placeholder: string): this {
this.strategies.push({
type: 'placeholder',
find: async () => await this.page.$(`[placeholder="${placeholder}"]`)
});
return this;
}
label(labelText: string): this {
this.strategies.push({
type: 'label',
find: async () => {
const handle = await this.page.evaluateHandle((text: string) => {
const labels = Array.from(document.querySelectorAll('label'));
const label = labels.find(l => l.textContent?.trim() === text.trim());
if (label && (label as HTMLLabelElement).htmlFor) {
return document.getElementById((label as HTMLLabelElement).htmlFor);
}
return null;
}, labelText);
return handle.asElement();
}
});
return this;
}
type(inputType: string): this {
this.strategies.push({
type: 'type',
find: async () => await this.page.$(`input[type="${inputType}"]`)
});
return this;
}
role(role: string): this {
this.strategies.push({
type: 'role',
find: async () => await this.page.$(`[role="${role}"]`)
});
return this;
}
testid(testid: string): this {
this.strategies.push({
type: 'testid',
find: async () => await this.page.$(`[data-testid="${testid}"]`)
});
return this;
}
name(name: string): this {
this.strategies.push({
type: 'name',
find: async () => await this.page.$(`[name="${name}"]`)
});
return this;
}
/**
*
*/
async find(timeout: number = 10000): Promise<ElementHandle | null> {
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;
}
}
export default SmartSelector;

View File

@ -0,0 +1,56 @@
/**
*
*/
export enum BrowserProviderType {
ADSPOWER = 'adspower',
PLAYWRIGHT_STEALTH = 'playwright-stealth',
PUPPETEER_STEALTH = 'puppeteer-stealth'
}
export interface IBrowserCapabilities {
stealth: boolean;
fingerprint: boolean;
proxy: boolean;
incognito: boolean;
profiles: boolean;
cloudflareBypass: boolean;
stripeCompatible: boolean;
}
export interface ILaunchOptions {
headless?: boolean;
viewport?: {
width: number;
height: number;
};
userAgent?: string;
}
export interface IProviderMetadata {
name: string;
version: string;
free: boolean;
capabilities: IBrowserCapabilities;
}
export interface ILaunchResult {
browser: any;
page: any;
wsEndpoint?: string;
}
export interface IActionConfig {
name?: string;
timeout?: number;
optional?: boolean;
retryCount?: number;
[key: string]: any;
}
export interface IActionResult {
success: boolean;
data?: any;
error?: Error;
duration?: number;
}

View File

@ -0,0 +1,64 @@
/**
*
* 使
*/
import { IBrowserProvider } from '../core/interfaces/IBrowserProvider';
import { BrowserProviderType } from '../core/types';
type ProviderConstructor = new (config: any) => IBrowserProvider;
export class BrowserFactory {
private static providers = new Map<string, ProviderConstructor>();
/**
* ProviderTypeScript类型检查
*/
static register<T extends IBrowserProvider>(
type: BrowserProviderType,
ProviderClass: new (config: any) => T
): void {
this.providers.set(type, ProviderClass);
console.log(`✅ Provider "${type}" registered`);
}
/**
* Provider实例
*/
static create<T extends IBrowserProvider = IBrowserProvider>(
type: BrowserProviderType,
config: any = {}
): T {
const ProviderClass = this.providers.get(type);
if (!ProviderClass) {
const available = Array.from(this.providers.keys()).join(', ');
throw new Error(
`Unknown provider: "${type}"\nAvailable: ${available}`
);
}
return new ProviderClass(config) as T;
}
/**
* Provider
*/
static getAvailableProviders(): BrowserProviderType[] {
return Array.from(this.providers.keys()) as BrowserProviderType[];
}
/**
* Provider是否已注册
*/
static has(type: BrowserProviderType): boolean {
return this.providers.has(type);
}
/**
* Provider
*/
static unregister(type: BrowserProviderType): void {
this.providers.delete(type);
}
}

View File

@ -0,0 +1,30 @@
/**
* Browser Automation Framework - Main Entry
*/
// Core exports
export * from './core/types';
export * from './core/interfaces/IBrowserProvider';
export * from './core/interfaces/IAction';
export * from './core/interfaces/ISmartSelector';
export * from './core/base/BaseBrowserProvider';
export * from './core/base/BaseAction';
// Workflow (通用组件)
export * from './workflow/WorkflowEngine';
// Factory
export * from './factory/BrowserFactory';
// Providers
export * from './providers/adspower/AdsPowerProvider';
// Register providers
import { BrowserFactory } from './factory/BrowserFactory';
import { AdsPowerProvider } from './providers/adspower/AdsPowerProvider';
import { BrowserProviderType } from './core/types';
// Auto-register AdsPower
BrowserFactory.register(BrowserProviderType.ADSPOWER, AdsPowerProvider);
console.log('✅ Browser Automation Framework (TypeScript) initialized');

View File

@ -0,0 +1,214 @@
/**
* AdsPower Provider实现
*/
import { BaseBrowserProvider } from '../../core/base/BaseBrowserProvider';
import { IBrowserCapabilities, ILaunchOptions, ILaunchResult } from '../../core/types';
import { AdsPowerActionFactory } from './core/ActionFactory';
import axios from 'axios';
const puppeteer = require('puppeteer');
export interface IAdsPowerConfig {
profileId?: string;
apiBase?: string;
apiKey?: string;
siteName?: string;
incognitoMode?: boolean;
}
export class AdsPowerProvider extends BaseBrowserProvider {
private profileId: string;
private apiBase: string;
private apiKey?: string;
private siteName: string;
private incognitoMode: boolean;
constructor(config: IAdsPowerConfig = {}) {
super(config);
this.profileId = config.profileId || process.env.ADSPOWER_USER_ID || '';
this.apiBase = config.apiBase || process.env.ADSPOWER_API || 'http://127.0.0.1:50325';
this.apiKey = config.apiKey || process.env.ADSPOWER_API_KEY || '35de43696f6241f3df895f2f48777a99';
this.siteName = config.siteName || 'AdsPower';
this.incognitoMode = config.incognitoMode !== false;
}
getName(): string {
return 'AdsPower';
}
getVersion(): string {
return '1.0.0';
}
isFree(): boolean {
return false;
}
getCapabilities(): IBrowserCapabilities {
return {
stealth: true,
fingerprint: true,
proxy: true,
incognito: true,
profiles: true,
cloudflareBypass: true,
stripeCompatible: true
};
}
async validateConfig(): Promise<boolean> {
if (!this.profileId) {
throw new Error('AdsPower Profile ID is required (ADSPOWER_USER_ID)');
}
return true;
}
async launch(options?: ILaunchOptions): Promise<ILaunchResult> {
console.log(`[${this.siteName}] Launching AdsPower browser...`);
await this.validateConfig();
const startUrl = this.buildStartUrl();
const headers = this.buildHeaders();
try {
const response = await axios.get(startUrl, { headers });
const data = response.data;
if (data.code !== 0) {
throw new Error(`AdsPower API error: ${data.msg || JSON.stringify(data)}`);
}
const wsEndpoint = this.extractWsEndpoint(data);
console.log(`[${this.siteName}] WebSocket: ${wsEndpoint}`);
this.browser = await puppeteer.connect({
browserWSEndpoint: wsEndpoint,
defaultViewport: options?.viewport || null
});
await this.setupPage();
console.log(`[${this.siteName}] ✅ AdsPower browser connected`);
return {
browser: this.browser,
page: this.page,
wsEndpoint
};
} catch (error: any) {
console.error(`[${this.siteName}] ❌ Failed to launch: ${error.message}`);
throw error;
}
}
async close(): Promise<void> {
if (!this.browser) {
console.warn(`[${this.siteName}] Browser not initialized`);
return;
}
try {
await this.closeAllPages();
await this.browser.disconnect();
await this.stopBrowserProcess();
this.browser = null;
this.page = null;
console.log(`[${this.siteName}] ✅ Browser closed`);
} catch (error: any) {
console.error(`[${this.siteName}] Error closing browser: ${error.message}`);
throw error;
}
}
getActionFactory(): AdsPowerActionFactory {
return new AdsPowerActionFactory();
}
// ========== Private Methods ==========
private buildStartUrl(): string {
const params = new URLSearchParams({
user_id: this.profileId
});
if (this.incognitoMode) {
params.append('clear_cache_after_closing', '1');
}
return `${this.apiBase}/api/v1/browser/start?${params.toString()}`;
}
private buildHeaders(): Record<string, string> {
const headers: Record<string, string> = {};
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
}
return headers;
}
private extractWsEndpoint(data: any): string {
const wsEndpoint = data.data.ws?.puppeteer ||
data.data.ws?.selenium ||
data.data.ws?.ws ||
data.data.ws;
if (!wsEndpoint) {
throw new Error('AdsPower did not return WebSocket endpoint');
}
return wsEndpoint;
}
private async setupPage(): Promise<void> {
const pages = await this.browser.pages();
this.page = pages[0] || await this.browser.newPage();
if (pages.length > 1) {
for (let i = 1; i < pages.length; i++) {
try {
await pages[i].close();
} catch (e) {
// Ignore
}
}
}
}
private async closeAllPages(): Promise<void> {
try {
const pages = await this.browser.pages();
for (const page of pages) {
try {
await page.close();
} catch (e) {
// Ignore
}
}
} catch (e) {
// Ignore
}
}
private async stopBrowserProcess(): Promise<void> {
if (!this.profileId) return;
const stopUrl = `${this.apiBase}/api/v1/browser/stop?user_id=${encodeURIComponent(this.profileId)}`;
const headers = this.buildHeaders();
try {
const response = await axios.get(stopUrl, { headers });
const data = response.data;
if (data.code === 0) {
console.log(`[${this.siteName}] ✅ Browser process stopped`);
}
} catch (e: any) {
console.warn(`[${this.siteName}] Failed to stop browser process: ${e.message}`);
}
}
}

View File

@ -0,0 +1,402 @@
import BaseAction from '../core/BaseAction';
import SmartSelector from '../../../core/selectors/SmartSelector';
import { ConfigurationError, ElementNotFoundError, ValidationError } from '../../../core/errors/CustomErrors';
import { Page, ElementHandle } from 'puppeteer';
/**
*
*/
interface IClickActionConfig {
selector?: any;
find?: any;
timeout?: number;
waitForEnabled?: boolean;
humanLike?: boolean;
verifyAfter?: any;
waitForPageChange?: boolean;
checkSelector?: any;
waitAfter?: number;
}
/**
*
*/
class ClickAction extends BaseAction {
async execute(): Promise<any> {
const selector = this.config.selector || this.config.find;
if (!selector) {
throw new ConfigurationError(
'缺少选择器配置',
'selector',
{ action: 'click', config: this.config }
);
}
this.log('info', '执行点击');
// 查找元素
const smartSelector = SmartSelector.fromConfig(selector, this.page);
const element = await smartSelector.find(this.config.timeout || 10000);
if (!element) {
throw new ElementNotFoundError(selector, {
action: 'click',
timeout: this.config.timeout || 10000
});
}
// 等待元素变为可点击状态(参考旧框架)
const waitForEnabled = this.config.waitForEnabled !== false; // 默认等待
if (waitForEnabled) {
// 传入 selector 配置,在循环中重新查找元素
await this.waitForClickable(element, this.config.timeout || 30000, selector);
}
// 记录找到的元素信息(总是显示,便于调试)
try {
const info = await element.evaluate((el: any) => {
const tag = el.tagName;
const id = el.id ? `#${el.id}` : '';
const cls = el.className ? `.${el.className.split(' ')[0]}` : '';
const text = (el.textContent || '').trim().substring(0, 30);
const disabled = el.disabled ? ' [DISABLED]' : '';
return `${tag}${id}${cls} "${text}"${disabled}`;
});
this.log('info', `→ 找到元素: ${info}`);
} catch (e: any) {
this.log('warn', `无法获取元素信息: ${e.message}`);
}
// 滚动到可视区域(带容错)
try {
await element.evaluate((el: any) => {
if (el && typeof el.scrollIntoView === 'function') {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
await new Promise(resolve => setTimeout(resolve, 300));
} catch (error: any) {
this.log('warn', `滚动失败,继续尝试点击: ${error.message}`);
}
// 模拟人类"准备点击"的短暂停顿
await this.pauseDelay();
// 点击(支持人类行为模拟)
const humanLike = this.config.humanLike !== false; // 默认使用人类行为
if (humanLike) {
// 重新查找元素并点击(参考旧框架,避免元素失效)
await this.humanClickWithSelector(selector);
} else {
await element.click();
}
this.log('info', '✓ 点击完成');
// 点击后的自然延迟(等待页面反应)
await this.pauseDelay();
// 验证点击后的变化(新元素出现 / 旧元素消失)
if (this.config.verifyAfter) {
await this.verifyAfterClick(this.config.verifyAfter);
}
// 等待页面变化(如果配置了)
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 };
}
/**
*
* @param {ElementHandle} element -
* @param {number} timeout -
* @param {Object} selectorConfig -
*/
async waitForClickable(element: ElementHandle | null, timeout: number, selectorConfig: any = null): Promise<boolean> {
this.log('info', '→ 等待元素可点击...');
const startTime = Date.now();
let lastLogTime = 0;
while (Date.now() - startTime < timeout) {
try {
// 如果提供了 selectorConfig每次重新查找元素参考旧框架
let currentElement = element;
if (selectorConfig) {
const smartSelector = SmartSelector.fromConfig(selectorConfig, this.page);
currentElement = await smartSelector.find(1000);
if (!currentElement) {
// 元素不存在,继续等待
await new Promise(resolve => setTimeout(resolve, 500));
continue;
}
}
// 确保元素存在
if (!currentElement) {
await new Promise(resolve => setTimeout(resolve, 500));
continue;
}
const isClickable = await currentElement.evaluate((el: any) => {
// 更严格的检查:
// 1. 必须可见
if (el.offsetParent === null) return false;
// 2. 如果是 button/input检查 disabled 属性
if (el.tagName === 'BUTTON' || el.tagName === 'INPUT') {
if (el.disabled) return false;
}
// 3. 检查是否被遮挡(可选)
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return false;
return true;
});
if (isClickable) {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
this.log('info', `✓ 元素已可点击 (耗时: ${elapsed}秒)`);
return true;
}
} catch (error: any) {
// 元素可能被重新渲染,继续等待
this.log('debug', `元素检查失败: ${error.message}`);
}
// 每5秒输出一次进度
const elapsed = Date.now() - startTime;
if (elapsed - lastLogTime >= 5000) {
this.log('info', `→ 等待元素可点击中... 已用时 ${(elapsed/1000).toFixed(0)}`);
lastLogTime = elapsed;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
this.log('warn', '⚠️ 等待元素可点击超时,将尝试点击');
return false;
}
/**
* - 使
*/
async humanClickWithSelector(selectorConfig: any): Promise<void> {
this.log('info', '→ 使用人类行为模拟点击...');
try {
// 重新查找元素(避免元素失效)
const smartSelector = SmartSelector.fromConfig(selectorConfig, this.page);
const element = await smartSelector.find(5000);
if (!element) {
throw new ElementNotFoundError(selectorConfig, {
action: 'click',
operation: 'humanClick',
reason: '重新定位失败'
});
}
this.log('debug', '✓ 已重新定位元素');
// 获取元素的边界框
const box = await element.boundingBox();
if (!box) {
this.log('warn', '⚠️ 无法获取元素边界框,使用直接点击');
await element.click();
return;
}
this.log('debug', `元素位置: x=${box.x.toFixed(0)}, y=${box.y.toFixed(0)}, w=${box.width.toFixed(0)}, h=${box.height.toFixed(0)}`);
// 计算点击位置(直接点击中心,避免随机偏移导致点击失败)
const targetX = box.x + box.width / 2;
const targetY = box.y + box.height / 2;
// 第一段移动:先移动到附近(模拟人眼定位)- 更慢
const nearX = targetX + this.randomInt(-50, 50);
const nearY = targetY + this.randomInt(-50, 50);
const steps1 = this.randomInt(15, 30); // 增加步数,移动更慢
this.log('debug', `移动鼠标到附近: (${nearX.toFixed(0)}, ${nearY.toFixed(0)})`);
await this.page.mouse.move(nearX, nearY, { steps: steps1 });
await this.randomDelay(150, 400); // 增加延迟
// 第二段移动:移动到目标位置 - 更慢
this.log('debug', `移动鼠标到目标: (${targetX.toFixed(0)}, ${targetY.toFixed(0)})`);
await this.page.mouse.move(targetX, targetY, { steps: this.randomInt(10, 20) });
// 短暂停顿(模拟人类反应和确认)- 增加延迟
await this.randomDelay(200, 500);
// 点击(使用 down + up而不是 click
this.log('debug', '执行点击 (mouse down + up)...');
await this.page.mouse.down();
await this.randomDelay(80, 180); // 增加按压时间
await this.page.mouse.up();
// 点击后延迟(等待页面响应)- 增加延迟
await this.randomDelay(1200, 2500);
this.log('info', '✓ 人类行为点击完成');
} catch (error: any) {
this.log('error', `⚠️ 人类行为点击失败: ${error.message}`);
throw error;
}
}
/**
*
*/
randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
*
*/
async randomDelay(min: number, max: number): Promise<void> {
const delay = this.randomInt(min, max);
await new Promise(resolve => setTimeout(resolve, delay));
}
/**
*
*/
async verifyAfterClick(config: any): Promise<void> {
const { appears, disappears, checked, timeout = 10000 } = config;
// 验证新元素出现
if (appears) {
this.log('debug', '验证新元素出现...');
for (const selector of (Array.isArray(appears) ? appears : [appears])) {
try {
await this.page.waitForSelector(selector, { timeout, visible: true });
this.log('debug', `✓ 新元素已出现: ${selector}`);
} catch (error: any) {
throw new ValidationError(
`点击后验证失败: 元素未出现`,
'元素出现',
'元素未找到',
{ selector, timeout }
);
}
}
}
// 验证旧元素消失
if (disappears) {
this.log('debug', '验证旧元素消失...');
for (const selector of (Array.isArray(disappears) ? disappears : [disappears])) {
try {
await this.page.waitForSelector(selector, { timeout, hidden: true });
this.log('debug', `✓ 旧元素已消失: ${selector}`);
} catch (error: any) {
throw new ValidationError(
`点击后验证失败: 元素未消失`,
'元素消失',
'元素仍存在',
{ selector, timeout }
);
}
}
}
// 验证 checked 状态(用于 radio/checkbox
if (checked !== undefined) {
this.log('debug', `验证 checked 状态: ${checked}...`);
await new Promise(resolve => setTimeout(resolve, 500));
// 获取 CSS 选择器
const selectorConfig = this.config.selector;
let cssSelector = null;
if (typeof selectorConfig === 'string') {
cssSelector = selectorConfig;
} else if (Array.isArray(selectorConfig)) {
// 取第一个 css 选择器
for (const sel of selectorConfig) {
if (typeof sel === 'string') {
cssSelector = sel;
break;
} else if (sel.css) {
cssSelector = sel.css;
break;
}
}
} else if (selectorConfig.css) {
cssSelector = selectorConfig.css;
}
if (!cssSelector) {
throw new ConfigurationError(
'无法从选择器配置中提取 CSS 选择器',
'selector',
{ config, selectorConfig }
);
}
const isChecked = await this.page.evaluate((sel: string) => {
const element = document.querySelector(sel) as any;
return element && element.checked === true;
}, cssSelector);
const expectedState = checked === true;
if (isChecked !== expectedState) {
throw new ValidationError(
`点击后验证失败: checked 状态不符`,
expectedState,
isChecked,
{ cssSelector }
);
}
this.log('debug', `✓ checked 状态验证通过: ${isChecked}`);
}
}
/**
*
*/
async waitForPageChange(checkSelector: any, timeout: number = 15000): Promise<boolean> {
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;
}
}
export default ClickAction;

View File

@ -0,0 +1,61 @@
import BaseAction from '../core/BaseAction';
import { ConfigurationError, TimeoutError } from '../../../core/errors/CustomErrors';
/**
* -
*
*/
class CustomAction extends BaseAction {
async execute(): Promise<any> {
const handler = this.config.handler;
const params = this.config.params || {};
const timeout = this.config.timeout || 300000; // 默认5分钟超时
if (!handler) {
throw new ConfigurationError('缺少处理函数名称', 'handler', {
action: 'custom',
config: this.config
});
}
this.log('info', `执行自定义函数: ${handler}`);
// 检查适配器中是否存在该函数
if (typeof this.context.adapter[handler] !== 'function') {
throw new ConfigurationError(
`自定义处理函数不存在: ${handler}`,
`adapter.${handler}`,
{ availableHandlers: Object.keys(this.context.adapter) }
);
}
// 使用 Promise.race 实现超时保护
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new TimeoutError(`自定义函数: ${handler}`, timeout, {
handler,
params
}));
}, timeout);
});
try {
// 竞速执行:函数完成 vs 超时
const result = await Promise.race([
this.context.adapter[handler](params),
timeoutPromise
]);
this.log('debug', '✓ 自定义函数执行完成');
return result;
} catch (error: any) {
if (error.message.includes('执行超时')) {
this.log('error', `⚠️ ${error.message}`);
}
throw error;
}
}
}
export default CustomAction;

View File

@ -0,0 +1,147 @@
import BaseAction from '../core/BaseAction';
/**
* -
*
* Example:
* - action: extract
* name: Extract quota info
* selector: p.caption1
* extractType: text
* regex: (\\d+)\\s*\\/\\s*(\\d+)
* saveTo:
* used: $1
* total: $2
* contextKey: quotaInfo
*/
class ExtractAction extends BaseAction {
async execute(): Promise<any> {
const {
selector,
extractType = 'text',
regex,
saveTo,
contextKey,
filter,
multiple = false,
required = true
} = this.config;
if (!selector) {
throw new Error('Extract action 需要 selector 参数');
}
this.log('debug', `提取数据: ${selector}`);
try {
// 在页面中查找并提取数据
const extractedData = await this.page.evaluate((config: any) => {
const { selector, extractType, filter, multiple } = config;
// 查找元素
let elements = Array.from(document.querySelectorAll(selector));
// 过滤元素
if (filter) {
if (filter.contains) {
elements = elements.filter(el =>
el.textContent.includes(filter.contains)
);
}
if (filter.notContains) {
elements = elements.filter(el =>
!el.textContent.includes(filter.notContains)
);
}
}
if (elements.length === 0) {
return null;
}
// 提取数据
const extractFrom = (element: any) => {
switch (extractType) {
case 'text':
return element.textContent.trim();
case 'html':
return element.innerHTML;
case 'attribute':
return element.getAttribute(config.attribute);
case 'value':
return element.value;
default:
return element.textContent.trim();
}
};
if (multiple) {
return elements.map(extractFrom);
} else {
return extractFrom(elements[0]);
}
}, { selector, extractType, filter, multiple, attribute: this.config.attribute });
if (extractedData === null) {
if (required) {
throw new Error(`未找到匹配的元素: ${selector}`);
} else {
this.log('warn', `未找到元素: ${selector},跳过提取`);
return { success: true, data: null };
}
}
this.log('debug', `提取到原始数据: ${JSON.stringify(extractedData)}`);
// 应用正则表达式
let processedData = extractedData;
if (regex && typeof extractedData === 'string') {
const regexObj = new RegExp(regex);
const match = extractedData.match(regexObj);
if (match) {
// 如果有 saveTo 配置,使用捕获组
if (saveTo && typeof saveTo === 'object') {
processedData = {};
for (const [key, value] of Object.entries(saveTo)) {
// $1, $2 等替换为捕获组
if (typeof value === 'string' && value.startsWith('$')) {
const groupIndex = parseInt(value.substring(1));
processedData[key] = match[groupIndex] || null;
} else {
processedData[key] = value;
}
}
} else {
// 返回第一个捕获组或整个匹配
processedData = match[1] || match[0];
}
} else if (required) {
throw new Error(`正则表达式不匹配: ${regex}`);
} else {
this.log('warn', `正则表达式不匹配: ${regex}`);
processedData = null;
}
}
// 保存到上下文
if (contextKey && processedData !== null) {
if (!this.context.data) {
this.context.data = {};
}
this.context.data[contextKey] = processedData;
this.log('info', `✓ 数据已保存到 context.${contextKey}`);
}
this.log('debug', `处理后的数据: ${JSON.stringify(processedData)}`);
return { success: true, data: processedData };
} catch (error: any) {
this.log('error', `数据提取失败: ${error.message}`);
throw error;
}
}
}
export default ExtractAction;

View File

@ -0,0 +1,210 @@
import BaseAction from '../core/BaseAction';
import SmartSelector from '../../../core/selectors/SmartSelector';
import { ConfigurationError, ElementNotFoundError, TimeoutError } from '../../../core/errors/CustomErrors';
/**
*
*/
class FillFormAction extends BaseAction {
async execute(): Promise<any> {
const fields = this.config.fields;
const humanLike = this.config.humanLike !== false; // 默认使用人类行为
if (!fields || typeof fields !== 'object') {
throw new ConfigurationError(
'表单字段配置无效',
'fields',
{ provided: typeof fields, config: this.config }
);
}
this.log('info', `填写表单,共 ${Object.keys(fields).length} 个字段`);
// 填写每个字段
const fieldEntries = Object.entries(fields);
for (let i = 0; i < fieldEntries.length; i++) {
const [key, fieldConfig] = fieldEntries[i];
await this.fillField(key, fieldConfig, humanLike);
// 字段间的停顿(不是最后一个字段时)
if (i < fieldEntries.length - 1) {
await this.pauseDelay();
}
}
this.log('info', '✓ 表单填写完成');
// 模拟人类填写后的思考时间
await this.thinkDelay();
return { success: true };
}
/**
*
*/
async fillField(key: string, fieldConfig: any, humanLike: boolean): Promise<void> {
let selector, value, fieldType;
// 支持三种配置格式
if (typeof fieldConfig === 'object' && fieldConfig.find) {
// 完整配置: { find: [...], value: "...", type: "..." }
selector = fieldConfig.find;
value = this.replaceVariables(fieldConfig.value);
fieldType = fieldConfig.type;
} else if (typeof fieldConfig === 'string') {
// 超简化配置: { fieldName: "value" }
// 自动推断选择器
selector = [
{ css: `#${key}` },
{ name: key },
{ css: `input[name="${key}"]` },
{ css: `select[name="${key}"]` },
{ css: `textarea[name="${key}"]` }
];
value = this.replaceVariables(fieldConfig);
fieldType = 'input';
} else {
// 简化配置: { selector: value }(已有的逻辑)
selector = key;
value = this.replaceVariables(fieldConfig);
fieldType = 'input';
}
// 查找元素(自动等待出现)
const smartSelector = SmartSelector.fromConfig(selector, this.page);
const element = await smartSelector.find(10000);
if (!element) {
throw new ElementNotFoundError(selector, {
action: 'fillForm',
field: key,
value
});
}
this.log('debug', ` → 填写字段: ${key}`);
// 检查字段类型(已在上面定义)
if (!fieldType) {
fieldType = fieldConfig.type || 'input';
}
if (fieldType === 'select') {
// 下拉框选择(需要 CSS 选择器)
const cssSelector = selector.css || selector[0]?.css;
if (!cssSelector) {
throw new ConfigurationError(
`select 类型字段需要 css 选择器`,
'selector',
{ field: key, selector, fieldType }
);
}
await this.page.select(cssSelector, value);
this.log('debug', ` → 已选择: ${value}`);
return;
}
// 普通输入框
// 清空字段(增强清空逻辑,支持 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: any) => {
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}, element);
}
/**
*
*/
async typeHumanLike(element: any, text: string): Promise<void> {
for (let i = 0; i < text.length; i++) {
const char = text[i];
// 每个字符延迟 100-250ms更慢
await element.type(char, {
delay: Math.random() * 150 + 100
});
// 每输入3-5个字符随机停顿一下模拟思考或调整手指
if (i > 0 && i % (Math.floor(Math.random() * 3) + 3) === 0) {
await new Promise(resolve => setTimeout(resolve, Math.random() * 800 + 300));
}
}
// 输入完成后,短暂停顿(模拟检查输入)
await new Promise(resolve => setTimeout(resolve, Math.random() * 500 + 300));
}
/**
*
*/
async submitForm(submitConfig: any): Promise<void> {
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 ElementNotFoundError(selector, {
action: 'fillForm',
operation: 'submitForm'
});
}
// 等待按钮可点击
await this.waitForButtonEnabled(button);
// 点击
await button.click();
// 等待提交后的延迟
if (submitConfig.waitAfter) {
await new Promise(resolve => setTimeout(resolve, submitConfig.waitAfter));
}
}
/**
*
*/
async waitForButtonEnabled(button: any, timeout: number = 30000): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const isEnabled = await this.page.evaluate((btn: any) => {
return !btn.disabled;
}, button);
if (isEnabled) {
return;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
throw new TimeoutError('等待按钮启用', timeout, {
action: 'fillForm'
});
}
}
export default FillFormAction;

View File

@ -0,0 +1,107 @@
import BaseAction from '../core/BaseAction';
import { ValidationError, ElementNotFoundError } from '../../../core/errors/CustomErrors';
/**
* -
*/
class NavigateAction extends BaseAction {
async execute(): Promise<any> {
const url = this.replaceVariables(this.config.url);
const options = this.config.options || {
waitUntil: 'networkidle2',
timeout: 30000
};
// 重试配置
const maxRetries = this.config.maxRetries || 5;
const retryDelay = this.config.retryDelay || 3000;
const totalTimeout = this.config.totalTimeout || 180000; // 默认3分钟
const startTime = Date.now();
let lastError: any = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
// 检查总超时
if (Date.now() - startTime > totalTimeout) {
this.log('error', `总超时 ${totalTimeout}ms停止重试`);
break;
}
try {
if (attempt > 0) {
this.log('info', `${attempt + 1} 次尝试导航...`);
} else {
this.log('info', `导航到: ${url}`);
}
// 尝试导航
await this.page.goto(url, options);
// 验证页面URL是否正确避免重定向到会员中心等
const currentUrl = this.page.url();
if (this.config.verifyUrl && !currentUrl.includes(this.config.verifyUrl)) {
throw new ValidationError(
`页面跳转异常`,
`URL包含: ${this.config.verifyUrl}`,
`实际URL: ${currentUrl}`,
{ expectedUrl: this.config.verifyUrl, actualUrl: currentUrl }
);
}
// 验证关键元素存在(确保页面加载正确)
if (this.config.verifyElements) {
await this.verifyElements(this.config.verifyElements);
}
this.log('info', `✓ 页面加载完成${attempt > 0 ? ` (尝试 ${attempt + 1} 次)` : ''}`);
// 模拟人类阅读页面1-3秒
await this.readPageDelay();
// 可选的额外等待时间
if (this.config.waitAfter) {
await new Promise(resolve => setTimeout(resolve, this.config.waitAfter));
}
return { success: true, url: currentUrl };
} catch (error: any) {
lastError = error;
this.log('warn', `导航失败 (尝试 ${attempt + 1}/${maxRetries}): ${error.message}`);
// 如果不是最后一次尝试,等待后重试
if (attempt < maxRetries - 1) {
this.log('debug', `等待 ${retryDelay}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
}
// 所有重试都失败
this.log('error', `导航失败: ${lastError.message}`);
throw lastError;
}
/**
*
*/
async verifyElements(selectors: string[]): Promise<void> {
this.log('debug', '验证页面元素...');
for (const selector of selectors) {
try {
await this.page.waitForSelector(selector, { timeout: 10000 });
} catch (error: any) {
throw new ElementNotFoundError(selector, {
action: 'navigate',
operation: 'verifyElements',
url: this.page.url()
});
}
}
this.log('debug', `✓ 已验证 ${selectors.length} 个关键元素`);
}
}
export default NavigateAction;

View File

@ -0,0 +1,178 @@
import BaseAction from '../core/BaseAction';
import { ConfigurationError, TimeoutError, RetryExhaustedError } from '../../../core/errors/CustomErrors';
/**
* -
*
*
*
* - action: retryBlock
* name: "支付流程"
* maxRetries: 5
* retryDelay: 2000
* totalTimeout: 300000 # 5
* onRetryBefore:
* - action: custom
* handler: "regenerateCard"
* steps:
* - action: fillForm
* fields: {...}
* - action: click
* selector: {...}
*/
class RetryBlockAction extends BaseAction {
async execute(): Promise<any> {
const {
steps = [],
maxRetries = 3,
retryDelay = 1000,
totalTimeout = 600000, // 默认10分钟整体超时
onRetryBefore = [],
onRetryAfter = []
} = this.config;
const blockName = this.config.name || 'RetryBlock';
if (!steps || steps.length === 0) {
throw new ConfigurationError(
'RetryBlock 必须包含至少一个步骤',
'steps',
{ blockName, config: this.config }
);
}
let lastError: any = null;
const startTime = Date.now();
for (let attempt = 0; attempt <= maxRetries; attempt++) {
// 检查整体超时
const elapsed = Date.now() - startTime;
if (elapsed > totalTimeout) {
throw new TimeoutError(
`${blockName} (整体)`,
totalTimeout,
{
attempts: attempt,
elapsed,
lastError: lastError?.message
}
);
}
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: any) {
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 RetryExhaustedError(
blockName,
maxRetries + 1,
{
lastError: lastError?.message,
stack: lastError?.stack,
totalTime: Date.now() - startTime
}
);
}
/**
*
*/
async executeHooks(hooks: any[]): Promise<void> {
for (const hookConfig of hooks) {
await this.executeStep(hookConfig);
}
}
/**
*
*/
async executeSteps(steps: any[]): Promise<void> {
for (const stepConfig of steps) {
await this.executeStep(stepConfig);
}
}
/**
*
*/
async executeStep(stepConfig: any): Promise<any> {
const actionType = stepConfig.action;
// 动态加载对应的 Action
const ActionClass = this.getActionClass(actionType);
// 修复BaseAction 构造函数签名是 (context, config)
const action = new ActionClass(
this.context, // 第一个参数context
stepConfig // 第二个参数config
);
return await action.execute();
}
/**
* action Action
*/
getActionClass(actionType: string): any {
const actionMap: any = {
navigate: require('./NavigateAction').default,
fillForm: require('./FillFormAction').default,
click: require('./ClickAction').default,
wait: require('./WaitAction').default,
custom: require('./CustomAction').default,
scroll: require('./ScrollAction').default,
verify: require('./VerifyAction').default
};
const ActionClass = actionMap[actionType];
if (!ActionClass) {
throw new Error(`未知的 action 类型: ${actionType}`);
}
return ActionClass;
}
}
export default RetryBlockAction;

View File

@ -0,0 +1,131 @@
import BaseAction from '../core/BaseAction';
/**
* -
*
*
* 1.
* 2.
* 3.
* 4.
*
* Example:
* - action: scroll
* type: bottom
*
* - action: scroll
* type: element
* selector: '#submit-button'
*
* - action: scroll
* type: distance
* x: 0
* y: 500
*/
class ScrollAction extends BaseAction {
async execute(): Promise<any> {
const {
type = 'bottom',
selector,
x = 0,
y = 0,
behavior = 'smooth'
} = this.config;
this.log('debug', `执行滚动: ${type}`);
switch (type) {
case 'bottom':
await this.scrollToBottom(behavior);
break;
case 'top':
await this.scrollToTop(behavior);
break;
case 'element':
if (!selector) {
throw new Error('滚动到元素需要提供 selector');
}
await this.scrollToElement(selector, behavior);
break;
case 'distance':
await this.scrollByDistance(x, y, behavior);
break;
default:
throw new Error(`不支持的滚动类型: ${type}`);
}
// 等待滚动动画完成
await new Promise(resolve => setTimeout(resolve, 500));
this.log('debug', '✓ 滚动完成');
// 模拟人类滚动后查看内容的停顿
await this.pauseDelay();
return { success: true };
}
/**
*
*/
async scrollToBottom(behavior: string): Promise<void> {
await this.page.evaluate((b: any) => {
window.scrollTo({
top: document.body.scrollHeight,
left: 0,
behavior: b
});
}, behavior);
}
/**
*
*/
async scrollToTop(behavior: string): Promise<void> {
await this.page.evaluate((b: any) => {
window.scrollTo({
top: 0,
left: 0,
behavior: b
});
}, behavior);
}
/**
*
*/
async scrollToElement(selector: string, behavior: string): Promise<void> {
const element = await this.page.$(selector);
if (!element) {
throw new Error(`元素不存在: ${selector}`);
}
await element.evaluate((el: any, b: any) => {
el.scrollIntoView({
behavior: b,
block: 'center',
inline: 'nearest'
});
}, behavior);
}
/**
*
*/
async scrollByDistance(x: number, y: number, behavior: string): Promise<void> {
await this.page.evaluate((dx: number, dy: number, b: any) => {
window.scrollBy({
top: dy,
left: dx,
behavior: b
});
}, x, y, behavior);
}
}
export default ScrollAction;

View File

@ -0,0 +1,251 @@
import BaseAction from '../core/BaseAction';
import SmartSelector from '../../../core/selectors/SmartSelector';
import { ConfigurationError, ValidationError } from '../../../core/errors/CustomErrors';
/**
* -
*
* /
*
* Example:
* - action: verify
* name: Verify payment result
* conditions:
* success:
* - urlNotContains: stripe.com
* - elementExists: .payment-success
* failure:
* - elementExists: .error-message
* - textContains: declined
* timeout: 10000
* pollInterval: 500
* onFailure: throw
*/
class VerifyAction extends BaseAction {
async execute(): Promise<any> {
const {
conditions,
timeout = 10000,
pollInterval = 500,
onSuccess = 'continue',
onFailure = 'throw',
onTimeout = 'throw'
} = this.config;
if (!conditions) {
throw new ConfigurationError(
'Verify action 需要 conditions 参数',
'conditions',
{ action: 'verify', config: this.config }
);
}
this.log('debug', '开始验证...');
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
// 检查成功条件
if (conditions.success) {
const successResult = await this.checkConditions(conditions.success);
if (successResult.matched) {
this.log('success', `✓ 验证成功: ${successResult.reason}`);
return this.handleResult('success', onSuccess);
}
}
// 检查失败条件
if (conditions.failure) {
const failureResult = await this.checkConditions(conditions.failure);
if (failureResult.matched) {
this.log('error', `✗ 验证失败: ${failureResult.reason}`);
return this.handleResult('failure', onFailure, failureResult.reason);
}
}
// 等待后继续轮询
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
// 超时
this.log('warn', `⚠ 验证超时(${timeout}ms`);
return this.handleResult('timeout', onTimeout, '验证超时');
}
/**
*
*/
async checkConditions(conditionList: any): Promise<{matched: boolean; reason?: string | null}> {
if (!Array.isArray(conditionList)) {
conditionList = [conditionList];
}
for (const condition of conditionList) {
const result = await this.checkSingleCondition(condition);
if (result.matched) {
return result;
}
}
return { matched: false };
}
/**
*
*/
async checkSingleCondition(condition: any): Promise<{matched: boolean; reason?: string | null}> {
// 条件类型1: urlContains / urlNotContains
if (condition.urlContains !== undefined) {
const currentUrl = this.page.url();
const matched = currentUrl.includes(condition.urlContains);
return {
matched,
reason: matched ? `URL 包含 "${condition.urlContains}"` : null
};
}
if (condition.urlNotContains !== undefined) {
const currentUrl = this.page.url();
const matched = !currentUrl.includes(condition.urlNotContains);
return {
matched,
reason: matched ? `URL 不包含 "${condition.urlNotContains}"` : null
};
}
// 条件类型2: urlEquals
if (condition.urlEquals !== undefined) {
const currentUrl = this.page.url();
const matched = currentUrl === condition.urlEquals;
return {
matched,
reason: matched ? `URL 等于 "${condition.urlEquals}"` : null
};
}
// 条件类型3: elementExists / elementNotExists
if (condition.elementExists !== undefined) {
const element = await this.page.$(condition.elementExists);
const matched = !!element;
return {
matched,
reason: matched ? `元素存在: ${condition.elementExists}` : null
};
}
if (condition.elementNotExists !== undefined) {
const element = await this.page.$(condition.elementNotExists);
const matched = !element;
return {
matched,
reason: matched ? `元素不存在: ${condition.elementNotExists}` : null
};
}
// 条件类型4: elementVisible / elementHidden
if (condition.elementVisible !== undefined) {
const visible = await this.page.evaluate((selector: string) => {
const el = document.querySelector(selector);
if (!el) return false;
const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
}, condition.elementVisible);
return {
matched: visible,
reason: visible ? `元素可见: ${condition.elementVisible}` : null
};
}
if (condition.elementHidden !== undefined) {
const hidden = await this.page.evaluate((selector: string) => {
const el = document.querySelector(selector);
if (!el) return true;
const style = window.getComputedStyle(el);
return style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0';
}, condition.elementHidden);
return {
matched: hidden,
reason: hidden ? `元素隐藏: ${condition.elementHidden}` : null
};
}
// 条件类型5: textContains / textNotContains
if (condition.textContains !== undefined) {
const hasText = await this.page.evaluate((text: string) => {
return document.body.textContent.includes(text);
}, condition.textContains);
return {
matched: hasText,
reason: hasText ? `页面包含文本: "${condition.textContains}"` : null
};
}
if (condition.textNotContains !== undefined) {
const hasText = await this.page.evaluate((text: string) => {
return document.body.textContent.includes(text);
}, condition.textNotContains);
return {
matched: !hasText,
reason: !hasText ? `页面不包含文本: "${condition.textNotContains}"` : null
};
}
// 条件类型6: elementTextContains
if (condition.elementTextContains !== undefined) {
const { selector, text } = condition.elementTextContains;
const hasText = await this.page.evaluate((sel: string, txt: string) => {
const el = document.querySelector(sel);
return el && el.textContent.includes(txt);
}, selector, text);
return {
matched: !!hasText,
reason: hasText ? `元素 ${selector} 包含文本 "${text}"` : null
};
}
// 条件类型7: custom - 自定义 JS 函数
if (condition.custom !== undefined) {
const matched = await this.page.evaluate(condition.custom);
return {
matched: !!matched,
reason: matched ? '自定义条件满足' : null
};
}
return { matched: false };
}
/**
*
*/
handleResult(resultType: string, action: string, reason: string | null = ''): any {
switch (action) {
case 'continue':
// 继续执行,不做任何事
return { success: true, result: resultType };
case 'throw':
// 抛出异常,触发重试或错误处理
throw new ValidationError(
`验证${resultType}`,
resultType === '成功' ? '满足成功条件' : '满足失败条件',
resultType,
{ reason, action: 'verify' }
);
case 'return':
// 返回结果,由调用者处理
return { success: resultType === 'success', result: resultType, reason };
default:
return { success: true, result: resultType };
}
}
}
export default VerifyAction;

View File

@ -0,0 +1,202 @@
import BaseAction from '../core/BaseAction';
import SmartSelector from '../../../core/selectors/SmartSelector';
import { ConfigurationError, ElementNotFoundError, TimeoutError } from '../../../core/errors/CustomErrors';
/**
*
*/
class WaitAction extends BaseAction {
async execute(): Promise<any> {
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();
case 'url':
return await this.waitForUrl();
default:
throw new ConfigurationError(
`未知的等待类型: ${type}`,
'type',
{ supportedTypes: ['delay', 'element', 'navigation', 'condition', 'url'] }
);
}
}
/**
*
*/
async waitDelay(): Promise<{success: boolean}> {
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(): Promise<{success: boolean}> {
const selector = this.config.selector || this.config.find;
const timeout = this.config.timeout || 10000;
if (!selector) {
throw new ConfigurationError(
'缺少选择器配置',
'find',
{ action: 'wait', type: 'element' }
);
}
this.log('debug', '等待元素出现');
const smartSelector = SmartSelector.fromConfig(selector, this.page);
const element = await smartSelector.find(timeout);
if (!element) {
throw new ElementNotFoundError(selector, {
action: 'wait',
type: 'element',
timeout
});
}
this.log('debug', '✓ 元素已出现');
return { success: true };
}
/**
*
*/
async waitForNavigation(): Promise<{success: boolean}> {
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(): Promise<{success: boolean}> {
const handler = this.config.handler;
const timeout = this.config.timeout || 10000;
if (!handler) {
throw new ConfigurationError(
'缺少条件处理函数',
'handler',
{ action: 'wait', type: 'condition' }
);
}
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 TimeoutError(
`waitForCondition: ${handler}`,
timeout,
{ handler, elapsed: Date.now() - startTime }
);
}
/**
* URL
*/
async waitForUrl(): Promise<{success: boolean}> {
const timeout = this.config.timeout || 20000;
const urlContains = this.config.urlContains;
const urlNotContains = this.config.urlNotContains;
const urlEquals = this.config.urlEquals;
if (!urlContains && !urlNotContains && !urlEquals) {
throw new ConfigurationError(
'需要指定 urlContains、urlNotContains 或 urlEquals',
'urlContains/urlNotContains/urlEquals',
{ action: 'wait', type: 'url' }
);
}
this.log('debug', '等待 URL 变化');
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const currentUrl = this.page.url();
let matched = false;
if (urlContains) {
matched = currentUrl.includes(urlContains);
if (matched) {
this.log('debug', `✓ URL 包含 "${urlContains}": ${currentUrl}`);
return { success: true };
}
}
if (urlNotContains) {
matched = !currentUrl.includes(urlNotContains);
if (matched) {
this.log('debug', `✓ URL 不包含 "${urlNotContains}": ${currentUrl}`);
return { success: true };
}
}
if (urlEquals) {
matched = currentUrl === urlEquals;
if (matched) {
this.log('debug', `✓ URL 等于 "${urlEquals}"`);
return { success: true };
}
}
await new Promise(resolve => setTimeout(resolve, 500));
}
const finalUrl = this.page.url();
throw new TimeoutError(
'waitForUrl',
timeout,
{
urlContains,
urlNotContains,
urlEquals,
actualUrl: finalUrl
}
);
}
}
export default WaitAction;

View File

@ -0,0 +1,58 @@
/**
* AdsPower ActionFactory
* Puppeteer的Actions
*/
import { IActionFactory } from '../../../core/interfaces/IAction';
import ClickAction from '../actions/ClickAction';
import WaitAction from '../actions/WaitAction';
import NavigateAction from '../actions/NavigateAction';
import CustomAction from '../actions/CustomAction';
import VerifyAction from '../actions/VerifyAction';
import FillFormAction from '../actions/FillFormAction';
import ScrollAction from '../actions/ScrollAction';
import ExtractAction from '../actions/ExtractAction';
import RetryBlockAction from '../actions/RetryBlockAction';
export class AdsPowerActionFactory implements IActionFactory {
private actions: Map<string, any>;
constructor() {
this.actions = new Map();
this.registerDefaultActions();
}
private registerDefaultActions(): void {
// 注册所有AdsPower支持的Actions
this.actions.set('click', ClickAction);
this.actions.set('wait', WaitAction);
this.actions.set('navigate', NavigateAction);
this.actions.set('custom', CustomAction);
this.actions.set('verify', VerifyAction);
this.actions.set('fillForm', FillFormAction);
this.actions.set('scroll', ScrollAction);
this.actions.set('extract', ExtractAction);
this.actions.set('retryBlock', RetryBlockAction);
}
getAction(actionName: string): any {
const ActionClass = this.actions.get(actionName);
if (!ActionClass) {
throw new Error(`Unknown action: ${actionName} in AdsPower provider`);
}
return ActionClass;
}
hasAction(actionName: string): boolean {
return this.actions.has(actionName);
}
/**
* Action
*/
registerAction(name: string, ActionClass: any): void {
this.actions.set(name, ActionClass);
}
}

View File

@ -0,0 +1,189 @@
/**
* AdsPower Provider的BaseAction
* BaseAction
*/
import { Page } from 'puppeteer';
export interface ActionContext {
page: Page;
logger?: any;
data?: any;
siteConfig?: any;
config?: any;
siteName?: string;
adapter?: any; // 自定义适配器用于CustomAction和WaitAction
}
export abstract class BaseAction {
protected context: ActionContext;
protected config: any;
protected page: Page;
protected logger: any;
constructor(context: ActionContext, config: any) {
this.context = context;
this.config = config;
this.page = context.page;
this.logger = context.logger;
}
/**
*
*/
abstract execute(): Promise<any>;
/**
*
*
*
* - {{account.email}}, {{site.url}}, {{config.timeout}}
* - {{var|default}}, {{user.name|Guest}}
* -
*/
replaceVariables(value: any): any {
if (typeof value !== 'string') return value;
return value.replace(/\{\{(.+?)\}\}/g, (match, expression) => {
// 解析默认值:{{var|default}}
const [path, defaultValue] = expression.split('|').map((s: string) => s.trim());
// 获取变量值
const result = this.resolveVariablePath(path);
// 如果找到值,返回
if (result !== undefined && result !== null) {
return result;
}
// 如果有默认值,使用默认值
if (defaultValue !== undefined) {
this.log('debug', `变量 "${path}" 不存在,使用默认值: "${defaultValue}"`);
return defaultValue;
}
// 变量不存在且无默认值,发出警告
this.log('warn', `⚠️ 变量 "${path}" 不存在,返回原始值: ${match}`);
return match;
});
}
/**
*
*/
resolveVariablePath(path: string): any {
const keys = path.split('.');
const rootKey = keys[0];
// 确定数据源
let dataSource: any;
let startIndex = 1; // 从第二个key开始
switch (rootKey) {
case 'site':
// {{site.url}} -> context.siteConfig.url
dataSource = this.context.siteConfig;
break;
case 'config':
// {{config.timeout}} -> context.config
dataSource = this.context.config;
break;
case 'env':
// {{env.API_KEY}} -> process.env
dataSource = process.env;
break;
default:
// 默认从 context.data 读取
// {{account.email}} -> context.data.account.email
dataSource = this.context.data;
startIndex = 0; // 从第一个key开始
}
if (!dataSource) {
return undefined;
}
// 遍历路径获取值
let result = dataSource;
for (let i = startIndex; i < keys.length; i++) {
if (result && typeof result === 'object') {
result = result[keys[i]];
} else {
return undefined;
}
}
return result;
}
/**
*
*/
log(level: string, message: string): void {
if (this.logger && this.logger[level]) {
this.logger[level](this.context.siteName || 'Automation', message);
} else {
console.log(`[${level.toUpperCase()}] ${message}`);
}
}
/**
*
*/
// 随机延迟
async randomDelay(min: number, max: number): Promise<void> {
const delay = min + Math.random() * (max - min);
await new Promise(resolve => setTimeout(resolve, delay));
}
// 阅读页面延迟2-5秒- 模拟用户查看页面内容
async readPageDelay(): Promise<void> {
await this.randomDelay(2000, 5000);
}
// 思考延迟1-2.5秒)- 模拟填写表单后的思考
async thinkDelay(): Promise<void> {
await this.randomDelay(1000, 2500);
}
// 短暂停顿300-800ms- 模拟操作间的自然停顿
async pauseDelay(): Promise<void> {
await this.randomDelay(300, 800);
}
// 步骤间延迟1.5-3秒- 模拟步骤之间的过渡
async stepDelay(): Promise<void> {
await this.randomDelay(1500, 3000);
}
/**
* Action类
*/
getActionClass(actionType: string): any {
const actionMap: any = {
navigate: require('./NavigateAction').default,
fillForm: require('./FillFormAction').default,
click: require('./ClickAction').default,
wait: require('./WaitAction').default,
custom: require('./CustomAction').default,
scroll: require('./ScrollAction').default,
verify: require('./VerifyAction').default,
extract: require('./ExtractAction').default,
retryBlock: require('./RetryBlockAction').default
};
const ActionClass = actionMap[actionType];
if (!ActionClass) {
throw new Error(`未知的 action 类型: ${actionType}`);
}
return ActionClass;
}
}
export default BaseAction;

View File

@ -0,0 +1,327 @@
/**
* -
* 100%
*/
import { BaseTool } from './ITool';
export interface AccountGeneratorConfig {
email?: {
domain?: string;
pattern?: string;
};
password?: {
strategy?: 'email' | 'random';
length?: number;
includeUppercase?: boolean;
includeLowercase?: boolean;
includeNumbers?: boolean;
includeSpecial?: boolean;
};
name?: {
gender?: 'male' | 'female' | 'neutral';
locale?: 'en' | 'zh-CN';
};
phone?: {
country?: string;
};
includePhone?: boolean;
}
export interface AccountData {
firstName: string;
lastName: string;
fullName: string;
email: string;
username: string;
password: string;
passwordStrategy: string;
timestamp: string;
phone?: string;
}
export class AccountGeneratorTool extends BaseTool<AccountGeneratorConfig> {
readonly name = 'account-generator';
// 邮箱域名(与旧框架一致)
private emailDomains = ['qichen111.asia'];
// 英文名字库(与旧框架一致)
private firstNames = {
male: [
'James', 'John', 'Robert', 'Michael', 'William',
'David', 'Richard', 'Joseph', 'Thomas', 'Charles',
'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald',
'Steven', 'Paul', 'Andrew', 'Joshua', 'Kenneth'
],
female: [
'Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth',
'Barbara', 'Susan', 'Jessica', 'Sarah', 'Karen',
'Nancy', 'Lisa', 'Betty', 'Margaret', 'Sandra',
'Ashley', 'Kimberly', 'Emily', 'Donna', 'Michelle'
],
neutral: [
'Alex', 'Jordan', 'Taylor', 'Casey', 'Riley',
'Morgan', 'Parker', 'Avery', 'Quinn', 'Skyler'
]
};
// 姓氏库(与旧框架一致)
private lastNames = [
'Smith', 'Johnson', 'Williams', 'Brown', 'Jones',
'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez',
'Hernandez', 'Lopez', 'Gonzalez', 'Wilson', 'Anderson',
'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin',
'Lee', 'Perez', 'Thompson', 'White', 'Harris',
'Sanchez', 'Clark', 'Ramirez', 'Lewis', 'Robinson'
];
// 中文名字
private chineseFirstNames = ['伟', '芳', '娜', '秀英', '敏', '静', '丽', '强', '磊', '军'];
private chineseLastNames = ['王', '李', '张', '刘', '陈', '杨', '赵', '黄', '周', '吴'];
// 密码字符集
private lowercase = 'abcdefghijklmnopqrstuvwxyz';
private uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
private numbers = '0123456789';
private special = '!@#$%^&*()_+-=[]{}|;:,.<>?';
protected validateConfig(config: AccountGeneratorConfig): void {
// 所有配置都是可选的
}
protected async doInitialize(): Promise<void> {
// 设置默认配置
this.config = this.config || {};
}
/**
*
*/
async generate(options: AccountGeneratorConfig = {}): Promise<AccountData> {
this.ensureInitialized();
// 合并配置
const opts = { ...this.config, ...options };
// 生成名字
const name = this.generateName(opts.name);
// 生成邮箱
const email = this.generateEmail(opts.email);
// 生成密码(根据策略)
let password: string;
const passwordStrategy = opts.password?.strategy || 'email';
if (passwordStrategy === 'email') {
password = email;
} else {
password = this.generatePassword(opts.password);
}
// 生成用户名
const username = this.generateUsername();
const account: AccountData = {
firstName: name.firstName,
lastName: name.lastName,
fullName: name.fullName,
email,
username,
password,
passwordStrategy,
timestamp: new Date().toISOString()
};
// 可选:电话号码
if (opts.includePhone !== false) {
account.phone = this.generatePhone(opts.phone);
}
return account;
}
/**
*
*/
private generateEmail(options: any = {}): string {
const domain = options?.domain || this.getRandomItem(this.emailDomains);
const prefix = this.generateEmailPrefix(options?.pattern);
return `${prefix}@${domain}`;
}
/**
*
*/
private generateEmailPrefix(pattern?: string): string {
if (pattern) {
return pattern.replace('{random}', this.generateRandomString());
}
// 默认8-12位随机字符串
const length = this.randomInt(8, 12);
return this.generateRandomString(length);
}
/**
*
*/
private generateName(options: any = {}): { firstName: string; lastName: string; fullName: string } {
const locale = options?.locale || 'en';
if (locale === 'zh-CN') {
const firstName = this.getRandomItem(this.chineseFirstNames);
const lastName = this.getRandomItem(this.chineseLastNames);
return {
firstName,
lastName,
fullName: `${lastName}${firstName}`
};
}
// 英文名字
let firstNamePool: string[];
const gender = options?.gender;
if (gender === 'male') {
firstNamePool = this.firstNames.male;
} else if (gender === 'female') {
firstNamePool = this.firstNames.female;
} else {
// 混合所有
firstNamePool = [
...this.firstNames.male,
...this.firstNames.female,
...this.firstNames.neutral
];
}
const firstName = this.getRandomItem(firstNamePool);
const lastName = this.getRandomItem(this.lastNames);
return {
firstName,
lastName,
fullName: `${firstName} ${lastName}`
};
}
/**
*
*/
private generatePassword(options: any = {}): string {
const {
length = 12,
includeUppercase = true,
includeLowercase = true,
includeNumbers = true,
includeSpecial = true,
minUppercase = 1,
minLowercase = 1,
minNumbers = 1,
minSpecial = 1
} = options || {};
let chars = '';
let password = '';
// 构建字符集
if (includeLowercase) chars += this.lowercase;
if (includeUppercase) chars += this.uppercase;
if (includeNumbers) chars += this.numbers;
if (includeSpecial) chars += this.special;
// 确保满足最小要求
if (includeLowercase && minLowercase > 0) {
for (let i = 0; i < minLowercase; i++) {
password += this.lowercase.charAt(this.randomInt(0, this.lowercase.length - 1));
}
}
if (includeUppercase && minUppercase > 0) {
for (let i = 0; i < minUppercase; i++) {
password += this.uppercase.charAt(this.randomInt(0, this.uppercase.length - 1));
}
}
if (includeNumbers && minNumbers > 0) {
for (let i = 0; i < minNumbers; i++) {
password += this.numbers.charAt(this.randomInt(0, this.numbers.length - 1));
}
}
if (includeSpecial && minSpecial > 0) {
for (let i = 0; i < minSpecial; i++) {
password += this.special.charAt(this.randomInt(0, this.special.length - 1));
}
}
// 填充剩余长度
while (password.length < length) {
password += chars.charAt(this.randomInt(0, chars.length - 1));
}
// 打乱顺序
return this.shuffle(password);
}
/**
*
*/
private generateUsername(): string {
const length = this.randomInt(8, 12);
return this.generateRandomString(length);
}
/**
*
*/
private generatePhone(options: any = {}): string {
// 美国格式1 + 10位数字
return `1${Math.floor(Math.random() * 9000000000 + 1000000000)}`;
}
/**
*
*/
private generateRandomString(length: number = 8): string {
return Math.random().toString(36).substring(2, 2 + length);
}
/**
*
*/
private shuffle(str: string): string {
const arr = str.split('');
for (let i = arr.length - 1; i > 0; i--) {
const j = this.randomInt(0, i);
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr.join('');
}
/**
*
*/
private randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
*
*/
private getRandomItem<T>(array: T[]): T {
return array[this.randomInt(0, array.length - 1)];
}
/**
*
*/
async generateBatch(count: number, options: AccountGeneratorConfig = {}): Promise<AccountData[]> {
const accounts: AccountData[] = [];
for (let i = 0; i < count; i++) {
accounts.push(await this.generate(options));
}
return accounts;
}
}

View File

@ -0,0 +1,241 @@
/**
* - TypeORM
* ORM式操作
*/
import { BaseTool } from './ITool';
import { DataSource, EntityManager } from 'typeorm';
import 'reflect-metadata';
export interface DatabaseConfig {
type?: 'mysql' | 'mariadb' | 'postgres' | 'sqlite';
host?: string;
port?: number;
username?: string;
password?: string;
database?: string;
// 可选:表定义
tables?: {
[tableName: string]: {
columns: Record<string, string>; // { columnName: type }
primaryKey?: string;
};
};
}
export class DatabaseTool extends BaseTool<DatabaseConfig> {
readonly name = 'database';
private dataSource!: DataSource;
private manager!: EntityManager;
protected validateConfig(config: DatabaseConfig): void {
if (!config.host) throw new Error('Database host is required');
if (!config.database) throw new Error('Database name is required');
}
protected async doInitialize(): Promise<void> {
const dbType = this.config.type || 'mysql';
// 创建数据源(根据类型使用不同配置)
this.dataSource = new DataSource({
type: dbType as any,
host: this.config.host,
port: this.config.port || 3306,
username: this.config.username || 'root',
password: this.config.password || '',
database: this.config.database,
synchronize: false,
logging: false,
entities: [],
} as any);
// 初始化连接
await this.dataSource.initialize();
this.manager = this.dataSource.manager;
console.log(`✓ Database connected: ${this.config.host}/${this.config.database}`);
// 如果配置了表定义,自动创建表
if (this.config.tables) {
await this.createTablesIfNotExist();
}
}
/**
* 1.
*/
async tableExists(tableName: string): Promise<boolean> {
this.ensureInitialized();
const result = await this.manager.query(
`SELECT COUNT(*) as count FROM information_schema.tables
WHERE table_schema = ? AND table_name = ?`,
[this.config.database, tableName]
);
return result[0].count > 0;
}
/**
* 2.
*/
async exists(tableName: string, where: Record<string, any>): Promise<boolean> {
this.ensureInitialized();
const { whereClause, params } = this.buildWhereClause(where);
const sql = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${whereClause}`;
const result = await this.manager.query(sql, params);
return result[0].count > 0;
}
/**
* 3.
*/
async insert(tableName: string, data: Record<string, any>): Promise<any> {
this.ensureInitialized();
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = columns.map(() => '?').join(', ');
const sql = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`;
const result = await this.manager.query(sql, values);
return { insertId: result.insertId, affectedRows: result.affectedRows };
}
/**
* 4.
*/
async update(
tableName: string,
where: Record<string, any>,
data: Record<string, any>
): Promise<number> {
this.ensureInitialized();
const setClauses = Object.keys(data).map(key => `${key} = ?`).join(', ');
const setValues = Object.values(data);
const { whereClause, params: whereParams } = this.buildWhereClause(where);
const sql = `UPDATE ${tableName} SET ${setClauses} WHERE ${whereClause}`;
const result = await this.manager.query(sql, [...setValues, ...whereParams]);
return result.affectedRows;
}
/**
* 5.
*/
async delete(tableName: string, where: Record<string, any>): Promise<number> {
this.ensureInitialized();
const { whereClause, params } = this.buildWhereClause(where);
const sql = `DELETE FROM ${tableName} WHERE ${whereClause}`;
const result = await this.manager.query(sql, params);
return result.affectedRows;
}
/**
* 6.
*/
async find(tableName: string, where?: Record<string, any>): Promise<any[]> {
this.ensureInitialized();
let sql = `SELECT * FROM ${tableName}`;
let params: any[] = [];
if (where) {
const { whereClause, params: whereParams } = this.buildWhereClause(where);
sql += ` WHERE ${whereClause}`;
params = whereParams;
}
return await this.manager.query(sql, params);
}
/**
* 7.
*/
async findOne(tableName: string, where: Record<string, any>): Promise<any | null> {
const results = await this.find(tableName, where);
return results.length > 0 ? results[0] : null;
}
/**
* 8. SQL
*/
async query(sql: string, params?: any[]): Promise<any> {
this.ensureInitialized();
return await this.manager.query(sql, params);
}
/**
*
*/
async createTable(tableName: string, columns: Record<string, string>, primaryKey?: string): Promise<void> {
this.ensureInitialized();
const exists = await this.tableExists(tableName);
if (exists) {
console.log(` Table ${tableName} already exists`);
return;
}
const columnDefs = Object.entries(columns).map(([name, type]) => {
let def = `${name} ${type}`;
if (name === primaryKey) {
def += ' PRIMARY KEY AUTO_INCREMENT';
}
return def;
});
const sql = `CREATE TABLE ${tableName} (${columnDefs.join(', ')})`;
await this.manager.query(sql);
console.log(` ✓ Table ${tableName} created`);
}
/**
*
*/
private async createTablesIfNotExist(): Promise<void> {
if (!this.config.tables) return;
for (const [tableName, tableDef] of Object.entries(this.config.tables)) {
await this.createTable(tableName, tableDef.columns, tableDef.primaryKey);
}
}
/**
* WHERE子句
*/
private buildWhereClause(where: Record<string, any>): { whereClause: string; params: any[] } {
const conditions: string[] = [];
const params: any[] = [];
for (const [key, value] of Object.entries(where)) {
conditions.push(`${key} = ?`);
params.push(value);
}
return {
whereClause: conditions.join(' AND '),
params
};
}
/**
*
*/
async cleanup(): Promise<void> {
if (this.dataSource?.isInitialized) {
await this.dataSource.destroy();
console.log(' ✓ database cleaned up');
}
}
}

View File

@ -0,0 +1,201 @@
/**
*
* 使
* 100%
*/
import { BaseTool } from './ITool';
import { IEmailProvider, EmailProviderConfig } from './email/IEmailProvider';
import { ImapEmailProvider } from './email/ImapEmailProvider';
import { BaseParser } from './email/parsers/BaseParser';
import { WindsurfParser } from './email/parsers/WindsurfParser';
export interface EmailToolConfig extends EmailProviderConfig {
// 搜索配置
checkInterval?: number; // 检查间隔(秒)
// EmailProviderConfig已经包含了所有IMAP配置user, password, host, port, tls, tlsOptions
}
export class EmailTool extends BaseTool<EmailToolConfig> {
readonly name = 'email';
private provider!: IEmailProvider;
private parsers: BaseParser[] = [];
protected validateConfig(config: EmailToolConfig): void {
if (!config.type) throw new Error('Email provider type is required');
if (config.type === 'imap') {
if (!config.user) throw new Error('Email user is required');
if (!config.password) throw new Error('Email password is required');
if (!config.host) throw new Error('Email host is required');
}
}
protected async doInitialize(): Promise<void> {
// 根据类型创建提供商
switch (this.config.type) {
case 'imap':
this.provider = new ImapEmailProvider(this.config);
break;
// 未来可以添加更多提供商
// case 'pop3':
// this.provider = new Pop3EmailProvider(this.config);
// break;
// case 'api':
// this.provider = new TempMailApiProvider(this.config);
// break;
default:
throw new Error(`Unsupported email provider type: ${this.config.type}`);
}
// 注册解析器
this.parsers = [
new WindsurfParser()
// 未来添加更多解析器
// new GitHubParser(),
// new TwitterParser(),
];
console.log(`✓ Email tool initialized with ${this.config.type} provider`);
}
/**
* 100%
*/
async getVerificationCode(
siteName: string,
recipientEmail: string,
timeout: number = 120
): Promise<string> {
console.log(`[EmailVerification] 开始获取 ${siteName} 的验证码...`);
console.log(`[EmailVerification] 接收邮箱: ${recipientEmail}`);
return new Promise((resolve, reject) => {
const startTime = Date.now();
const maxWaitTime = timeout * 1000;
let checkInterval: NodeJS.Timeout;
let isResolved = false;
const checkMail = async () => {
if (Date.now() - startTime > maxWaitTime) {
clearInterval(checkInterval);
this.provider.disconnect();
if (!isResolved) {
isResolved = true;
reject(new Error('获取验证码超时'));
}
return;
}
try {
// 获取最新邮件
console.log('[EmailVerification] 正在搜索邮件...');
const emails = await this.provider.getLatestEmails(50, 'INBOX');
if (!emails || emails.length === 0) {
console.log('[EmailVerification] 暂无未读邮件');
return;
}
console.log(`[EmailVerification] ✓ 找到 ${emails.length} 封未读邮件`);
// 按日期倒序排序(最新的在前)
emails.sort((a, b) => {
const dateA = a.date ? new Date(a.date).getTime() : 0;
const dateB = b.date ? new Date(b.date).getTime() : 0;
return dateB - dateA;
});
// 打印最近5条邮件信息
const recentEmails = emails.slice(0, 5);
console.log('[EmailVerification] ' + '='.repeat(60));
console.log('[EmailVerification] 最近5条邮件');
recentEmails.forEach((email, index) => {
const dateStr = email.date ? new Date(email.date).toLocaleString('zh-CN') : 'N/A';
console.log(`[EmailVerification] ${index + 1}. 时间: ${dateStr}`);
console.log(`[EmailVerification] 发件人: ${email.from}`);
console.log(`[EmailVerification] 主题: ${email.subject}`);
console.log(`[EmailVerification] 收件人: ${email.to}`);
});
console.log('[EmailVerification] ' + '='.repeat(60));
// 查找匹配的邮件并提取验证码
// 必须检查收件人是否匹配,避免获取到旧邮件的验证码
for (const email of emails) {
if (isResolved) return;
console.log(`[EmailVerification] 检查邮件: 发件人="${email.from}", 主题="${email.subject}", 收件人="${email.to}", 时间="${email.date}"`);
// 提取收件人邮箱地址(可能包含名字,如 "Name <email@example.com>"
const emailToMatch = email.to.match(/<(.+?)>/);
const actualRecipient = emailToMatch ? emailToMatch[1] : email.to;
// 检查收件人是否匹配
if (!actualRecipient.includes(recipientEmail)) {
console.log(`[EmailVerification] ✗ 跳过:收件人不匹配(期望:${recipientEmail},实际:${actualRecipient}`);
continue;
}
console.log('[EmailVerification] ✓ 收件人匹配!');
for (const parser of this.parsers) {
if (parser.canParse(email)) {
console.log(`[EmailVerification] ✓ 找到匹配的邮件: ${email.subject}`);
const code = parser.extractCode(email);
if (code) {
clearInterval(checkInterval);
this.provider.disconnect();
if (!isResolved) {
isResolved = true;
console.log(`[EmailVerification] ✓ 成功提取验证码: ${code}`);
resolve(code);
}
return;
} else {
console.log('[EmailVerification] 邮件匹配但无法提取验证码');
}
}
}
}
console.log('[EmailVerification] 未找到匹配的验证码邮件');
} catch (err: any) {
console.error(`[EmailVerification] 检查邮件失败: ${err.message}`);
}
};
// 连接成功后开始检查
this.provider.connect().then(() => {
console.log('[EmailVerification] IMAP连接成功开始监听验证码邮件...');
checkMail();
const interval = (this.config.checkInterval || 10) * 1000;
checkInterval = setInterval(checkMail, interval);
}).catch((err: Error) => {
if (!isResolved) {
isResolved = true;
reject(new Error(`IMAP连接失败: ${err.message}`));
}
});
});
}
/**
*
*/
addParser(parser: BaseParser): void {
this.parsers.push(parser);
}
/**
*
*/
async cleanup(): Promise<void> {
if (this.provider) {
this.provider.disconnect();
console.log(' ✓ email cleaned up');
}
}
}

View File

@ -0,0 +1,84 @@
/**
* Tool插件系统基础接口
*/
/**
* - Tool必须实现
*/
export interface ITool<TConfig = any> {
/**
*
*/
readonly name: string;
/**
*
* @param config
*/
initialize(config: TConfig): Promise<void>;
/**
*
*/
cleanup?(): Promise<void>;
/**
*
*/
healthCheck?(): Promise<boolean>;
}
/**
* -
*/
export abstract class BaseTool<TConfig = any> implements ITool<TConfig> {
abstract readonly name: string;
protected config!: TConfig;
protected initialized = false;
/**
* -
*/
async initialize(config: TConfig): Promise<void> {
this.validateConfig(config);
this.config = config;
await this.doInitialize();
this.initialized = true;
console.log(`${this.name} initialized`);
}
/**
*
*/
protected abstract validateConfig(config: TConfig): void;
/**
*
*/
protected abstract doInitialize(): Promise<void>;
/**
*
*/
protected ensureInitialized(): void {
if (!this.initialized) {
throw new Error(`${this.name} is not initialized. Call initialize() first.`);
}
}
/**
*
*/
async cleanup(): Promise<void> {
this.initialized = false;
console.log(`${this.name} cleaned up`);
}
/**
*
*/
async healthCheck(): Promise<boolean> {
return this.initialized;
}
}

View File

@ -0,0 +1,439 @@
/**
* Card Generator Tool -
* 100%
*/
import { BaseTool } from '../ITool';
import { randomInt, randomDigits, padZero, generateLuhnNumber } from './utils';
import { CARD_TYPES, EXPIRY_CONFIG, CardTypeConfig, BinInfo } from './config';
import { DatabaseTool } from '../DatabaseTool';
export interface CardInfo {
number: string;
month: string;
year: string;
cvv: string;
type: string;
issuer: string;
country: string;
countryName: string;
}
export interface CardGeneratorConfig {
type?: string;
database?: DatabaseTool;
}
export class CardGeneratorTool extends BaseTool<CardGeneratorConfig> {
readonly name = 'card-generator';
private cardTypes = CARD_TYPES;
private expiryConfig = EXPIRY_CONFIG;
private usedNumbers = new Set<string>();
private database: DatabaseTool | null = null;
private lastBinInfo: any = null;
protected validateConfig(config: CardGeneratorConfig): void {
// 配置是可选的
}
protected async doInitialize(): Promise<void> {
this.database = this.config.database || null;
}
/**
* +
* @param {string} type -
* @returns {Promise<string>}
*/
async generateCardNumber(type: string): Promise<string> {
const config = this.cardTypes[type];
if (!config) {
throw new Error(`Unknown card type: ${type}`);
}
// 尝试生成唯一卡号最多100次
let attempts = 0;
const maxAttempts = 100;
while (attempts < maxAttempts) {
const cardNumber = this._generateCardNumberInternal(type, config);
// 检查1内存去重本次运行
if (this.usedNumbers.has(cardNumber)) {
attempts++;
continue;
}
// 检查2数据库去重历史记录
if (this.database) {
const existsInDb = await this.checkCardNumberInDatabase(cardNumber);
if (existsInDb) {
attempts++;
continue;
}
}
// 通过所有检查,记录并返回
this.usedNumbers.add(cardNumber);
return cardNumber;
}
throw new Error(`无法生成唯一卡号(${maxAttempts}次尝试后仍重复)`);
}
/**
*
* @param {string} cardNumber -
* @returns {Promise<boolean>}
*/
private async checkCardNumberInDatabase(cardNumber: string): Promise<boolean> {
try {
// 检查数据库连接是否已初始化
if (!this.database) {
return false; // 数据库未初始化,跳过数据库查询
}
const rows = await this.database.find('windsurf_accounts', { payment_card_number: cardNumber });
return rows.length > 0;
} catch (error) {
// 如果数据库查询失败,静默降级(不输出,避免乱码)
return false;
}
}
/**
*
* @param {string} type -
* @param {Object} config -
* @returns {string}
*/
private _generateCardNumberInternal(type: string, config: CardTypeConfig): string {
const { prefix, prefixes, length, useLuhn, successfulPatterns, generation } = config;
const prefixInfo = this.selectPrefix(prefix, prefixes);
this.lastBinInfo = prefixInfo; // 保存BIN信息
const selectedPrefix = prefixInfo.fullPrefix;
if (!selectedPrefix) {
throw new Error(`卡类型 ${type} 未配置有效的前缀`);
}
if (selectedPrefix.length >= length) {
throw new Error(`前缀长度(${selectedPrefix.length}) 不得大于或等于卡号总长度(${length})`);
}
const digitsNeeded = length - selectedPrefix.length - 1;
if (digitsNeeded <= 0) {
throw new Error(`前缀配置错误:无法为 ${type} 生成剩余位数`);
}
const patternLength = successfulPatterns && successfulPatterns.length > 0
? successfulPatterns[0].length
: null;
const canUseAdvancedStrategies = Boolean(
generation &&
successfulPatterns &&
successfulPatterns.length > 0 &&
prefixInfo.supportsPatterns &&
patternLength === digitsNeeded + 1
);
if (canUseAdvancedStrategies) {
const rand = Math.random();
if (rand < 0.2) {
return this.generateByWeights(selectedPrefix, digitsNeeded, length);
}
if (rand < 0.8) {
return this.generateByMutation(selectedPrefix, successfulPatterns || [], generation.mutationDigits || [1, 1], digitsNeeded, length);
}
// 其余概率走纯随机策略
}
if (useLuhn) {
return generateLuhnNumber(selectedPrefix, length);
}
const remainingLength = length - selectedPrefix.length;
return selectedPrefix + randomDigits(remainingLength);
}
private selectPrefix(defaultPrefix?: string, prefixes?: BinInfo[]): any {
const options: any[] = [];
if (Array.isArray(prefixes) && prefixes.length > 0) {
prefixes.forEach((item) => {
if (!item) return;
if (typeof item === 'string') {
options.push({
fullPrefix: item,
weight: 1,
supportsPatterns: true
});
return;
}
if (typeof item === 'object') {
const bin = typeof item.bin === 'string' ? item.bin : '';
const extension = typeof item.extension === 'string' ? item.extension : '';
const fullPrefix = typeof item.fullPrefix === 'string' ? item.fullPrefix : `${bin}${extension}`;
if (!fullPrefix) {
return;
}
const weight = Number.isFinite(item.weight) && (item.weight || 0) > 0 ? (item.weight || 1) : 1;
const supportsPatterns = item.allowPatterns !== false;
const issuer = item.issuer || '未知';
const country = item.country || 'CN';
options.push({ fullPrefix, weight, supportsPatterns, issuer, country });
}
});
}
if (options.length === 0 && defaultPrefix) {
return { fullPrefix: defaultPrefix, supportsPatterns: true };
}
if (options.length === 0) {
return { fullPrefix: null, supportsPatterns: false };
}
const totalWeight = options.reduce((sum: number, item: any) => sum + item.weight, 0);
let randomValue = Math.random() * totalWeight;
for (const option of options) {
randomValue -= option.weight;
if (randomValue <= 0) {
return option;
}
}
return options[options.length - 1];
}
/**
*
* @param {string} prefix - BIN前缀
* @param {Array<string>} patterns -
* @param {Array<number>} mutationDigits - [min, max]
* @param {number} digitsNeeded -
* @param {number} totalLength -
* @returns {string}
*/
private generateByMutation(prefix: string, patterns: string[], mutationDigits: [number, number], digitsNeeded: number, totalLength: number): string {
if (!Array.isArray(patterns) || patterns.length === 0) {
return generateLuhnNumber(prefix, totalLength);
}
const basePattern = patterns[randomInt(0, patterns.length - 1)];
if (typeof basePattern !== 'string' || basePattern.length !== digitsNeeded + 1) {
return generateLuhnNumber(prefix, totalLength);
}
const bodyDigits = basePattern.slice(0, digitsNeeded).split('');
let minChanges = 1;
let maxChanges = 1;
if (Array.isArray(mutationDigits) && mutationDigits.length > 0) {
const [minRaw, maxRaw] = mutationDigits;
if (Number.isFinite(minRaw)) {
minChanges = Math.max(0, minRaw);
}
if (Number.isFinite(maxRaw)) {
maxChanges = Math.max(minChanges, maxRaw);
} else {
maxChanges = Math.max(minChanges, minChanges);
}
}
maxChanges = Math.min(maxChanges, digitsNeeded);
minChanges = Math.min(minChanges, maxChanges);
const changeCount = maxChanges > 0 ? randomInt(minChanges, maxChanges) : 0;
const changedPositions = new Set();
for (let i = 0; i < changeCount; i++) {
let pos;
do {
pos = randomInt(0, bodyDigits.length - 1);
} while (changedPositions.has(pos));
changedPositions.add(pos);
let newDigit;
do {
newDigit = randomInt(0, 9).toString();
} while (newDigit === bodyDigits[pos]);
bodyDigits[pos] = newDigit;
}
const partial = prefix + bodyDigits.join('');
const checkDigit = this.calculateLuhnCheckDigit(partial);
return partial + checkDigit;
}
/**
* 60
* @param {string} prefix - BIN前缀
* @param {number} digitsNeeded -
* @param {number} totalLength -
* @returns {string}
*/
private generateByWeights(prefix: string, digitsNeeded: number, totalLength: number): string {
if (digitsNeeded <= 0) {
return generateLuhnNumber(prefix, totalLength);
}
// 基于60个成功案例的位置权重频率分布
const positionWeights = [
[7, 5, 8, 2, 5, 7, 6, 7, 6, 7], // 位置1
[7, 5, 7, 4, 11, 4, 2, 8, 8, 4], // 位置2
[5, 4, 4, 9, 6, 7, 7, 6, 5, 7], // 位置3
[12, 5, 8, 7, 7, 5, 4, 6, 5, 1], // 位置4: 数字0占20%
[9, 6, 7, 3, 5, 6, 3, 9, 7, 5], // 位置5
[10, 4, 5, 9, 7, 8, 4, 5, 6, 2], // 位置6
[7, 3, 7, 2, 9, 6, 4, 6, 9, 7], // 位置7
];
if (digitsNeeded > positionWeights.length) {
return generateLuhnNumber(prefix, totalLength);
}
let pattern = '';
for (let pos = 0; pos < digitsNeeded; pos++) {
const weights = positionWeights[pos];
const digit = this.weightedRandomDigit(weights);
pattern += digit;
}
const partial = prefix + pattern;
const checkDigit = this.calculateLuhnCheckDigit(partial);
return partial + checkDigit;
}
/**
*
* @param {Array} weights - 100-9
* @returns {string}
*/
private weightedRandomDigit(weights: number[]): string {
const total = weights.reduce((sum, w) => sum + w, 0);
let random = Math.random() * total;
for (let i = 0; i < weights.length; i++) {
random -= weights[i];
if (random <= 0) return i.toString();
}
return randomInt(0, 9).toString();
}
/**
* Luhn校验位
* @param {string} partial -
* @returns {string}
*/
private calculateLuhnCheckDigit(partial: string): string {
let sum = 0;
let isEven = true;
for (let i = partial.length - 1; i >= 0; i--) {
let digit = parseInt(partial[i]);
if (isEven) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
isEven = !isEven;
}
return ((10 - (sum % 10)) % 10).toString();
}
/**
*
* @returns {{month: string, year: string}}
*/
private generateExpiry(): { month: string; year: string } {
const month = randomInt(this.expiryConfig.minMonth, this.expiryConfig.maxMonth);
const year = randomInt(this.expiryConfig.minYear, this.expiryConfig.maxYear);
return {
month: padZero(month, 2),
year: padZero(year, 2)
};
}
/**
* CVV安全码
* @param {string} type -
* @returns {string}
*/
private generateCVV(type: string): string {
const config = this.cardTypes[type];
const cvvLength = config.cvvLength;
const maxValue = Math.pow(10, cvvLength) - 1;
const cvv = randomInt(0, maxValue);
return padZero(cvv, cvvLength);
}
/**
* API
*/
async generate(type: string = 'unionpay'): Promise<CardInfo> {
const number = await this.generateCardNumber(type);
const expiry = this.generateExpiry();
const cvv = this.generateCVV(type);
const issuer = this.lastBinInfo?.issuer || '未知';
const country = this.lastBinInfo?.country || 'CN';
const countryName = country === 'MO' ? '澳门' : '中国';
return {
number,
month: expiry.month,
year: expiry.year,
cvv,
type: this.cardTypes[type].name,
issuer,
country,
countryName
};
}
/**
*
*/
async generateBatch(count: number, type: string = 'unionpay'): Promise<CardInfo[]> {
const cards: CardInfo[] = [];
for (let i = 0; i < count; i++) {
cards.push(await this.generate(type));
}
return cards;
}
/**
*
*/
getSupportedTypes() {
return Object.keys(this.cardTypes).map(key => ({
id: key,
name: this.cardTypes[key].name
}));
}
async cleanup(): Promise<void> {
console.log(' ✓ card-generator cleaned up');
}
}
// 导出已在类定义中完成

View File

@ -0,0 +1,169 @@
/**
* Card Generator Configuration
* 100%
*/
export interface BinInfo {
bin?: string;
extension?: string;
fullPrefix?: string;
weight?: number;
issuer?: string;
country?: string;
allowPatterns?: boolean;
}
export interface CardTypeConfig {
name: string;
prefix?: string;
prefixes?: BinInfo[];
length: number;
cvvLength: number;
useLuhn: boolean;
successfulPatterns?: string[];
generation?: {
mutationDigits?: [number, number];
};
}
export const CARD_TYPES: Record<string, CardTypeConfig> = {
unionpay: {
name: '中国银联 (UnionPay)',
// 农业银行622836开头的13位BIN可生成6,500张
prefixes: [
{ bin: '6228367540023', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367540057', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367540385', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367540707', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367540744', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367540810', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367540814', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367541130', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367541210', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367541299', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367541450', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367541880', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367542443', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367542464', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367542487', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367542602', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367542653', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367542738', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367542797', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367542940', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367543564', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367543770', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367543917', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367544252', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367544322', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367544445', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367544742', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367544873', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367545022', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367545055', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367545237', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367545452', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367545657', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367545800', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367545864', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367545956', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367545976', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367546042', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367546223', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367546361', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367546496', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367546781', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367546998', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367547093', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367547237', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367547238', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367547300', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367547416', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367547542', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367547562', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367547863', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367548160', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367548400', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367548435', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367548491', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367548575', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367548774', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367549031', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367549130', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367549131', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367549198', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367549574', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367549888', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367549976', weight: 1, issuer: '中国农业银行', country: 'MO' },
{ bin: '6228367586381', weight: 1, issuer: '中国农业银行', country: 'MO' }
],
length: 16,
cvvLength: 3,
useLuhn: true
},
visa: {
name: 'Visa',
prefix: '4',
length: 16,
cvvLength: 3,
useLuhn: true
},
mastercard: {
name: 'MasterCard',
prefix: '5',
length: 16,
cvvLength: 3,
useLuhn: true
},
amex: {
name: 'American Express',
prefix: '34',
length: 15,
cvvLength: 4,
useLuhn: true
},
discover: {
name: 'Discover',
prefix: '6011',
length: 16,
cvvLength: 3,
useLuhn: true
}
};
/**
*
*/
export const EXPIRY_CONFIG = {
minYear: 26, // 2026
maxYear: 30, // 2030
minMonth: 1,
maxMonth: 12
};
/**
*
*/
export const OUTPUT_FORMATS = {
pipe: {
name: 'Pipe分隔 (|)',
formatter: (card: any) => `${card.number}|${card.month}|${card.year}|${card.cvv}`
},
json: {
name: 'JSON格式',
formatter: (card: any) => JSON.stringify(card, null, 2)
},
csv: {
name: 'CSV格式',
formatter: (card: any) => `${card.number},${card.month},${card.year},${card.cvv}`
},
pretty: {
name: '美化格式',
formatter: (card: any) => `
Card Number: ${card.number}
Expiry Date: ${card.month}/${card.year}
CVV: ${card.cvv}
Type: ${card.type}
`.trim()
}
};

View File

@ -0,0 +1,48 @@
/**
* Formatter -
*/
const { OUTPUT_FORMATS } = require('./config');
class Formatter {
constructor() {
this.formats = OUTPUT_FORMATS;
}
/**
*
* @param {Object} card -
* @param {string} format -
* @returns {string}
*/
format(card, format = 'pipe') {
const formatter = this.formats[format];
if (!formatter) {
throw new Error(`Unknown format: ${format}`);
}
return formatter.formatter(card);
}
/**
*
* @param {Array} cards -
* @param {string} format -
* @returns {string}
*/
formatBatch(cards, format = 'pipe') {
return cards.map(card => this.format(card, format)).join('\n');
}
/**
*
* @returns {Array}
*/
getSupportedFormats() {
return Object.keys(this.formats).map(key => ({
id: key,
name: this.formats[key].name
}));
}
}
module.exports = Formatter;

View File

@ -0,0 +1,81 @@
/**
* Card Generator Utils -
* 100%
*/
/**
*
*/
export function randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
*
*/
export function randomDigits(length: number): string {
let result = '';
for (let i = 0; i < length; i++) {
result += randomInt(0, 9);
}
return result;
}
/**
*
*/
export function padZero(num: number, length: number = 2): string {
return String(num).padStart(length, '0');
}
/**
* Luhn算法校验
*/
export function luhnCheck(cardNumber: string): boolean {
let sum = 0;
let isEven = false;
for (let i = cardNumber.length - 1; i >= 0; i--) {
let digit = parseInt(cardNumber[i]);
if (isEven) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0;
}
/**
* Luhn算法的卡号
*/
export function generateLuhnNumber(prefix: string, totalLength: number): string {
const remaining = totalLength - prefix.length - 1;
let cardNumber = prefix + randomDigits(remaining);
let sum = 0;
let isEven = true;
for (let i = cardNumber.length - 1; i >= 0; i--) {
let digit = parseInt(cardNumber[i]);
if (isEven) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
isEven = !isEven;
}
const checkDigit = (10 - (sum % 10)) % 10;
return cardNumber + checkDigit;
}

View File

@ -0,0 +1,63 @@
/**
*
* IMAPPOP3API等多种邮箱接入方式
*/
export interface EmailMessage {
uid?: number;
from: string;
to: string;
subject: string;
date: Date | string;
text: string;
html: string;
headers?: any;
}
export interface IEmailProvider {
/**
*
*/
connect(): Promise<void>;
/**
*
*/
disconnect(): void;
/**
*
* @param count
* @param folder 'INBOX'
*/
getLatestEmails(count: number, folder?: string): Promise<EmailMessage[]>;
/**
*
* @param subject
* @param sinceDays
*/
searchBySubject(subject: string, sinceDays?: number): Promise<EmailMessage[]>;
/**
*
* @param uid UID
*/
markAsRead?(uid: number): Promise<void>;
}
export interface EmailProviderConfig {
type: 'imap' | 'pop3' | 'api';
// IMAP/POP3配置
user?: string;
password?: string;
host?: string;
port?: number;
tls?: boolean;
tlsOptions?: any;
// API配置如临时邮箱
apiKey?: string;
apiUrl?: string;
}

View File

@ -0,0 +1,225 @@
/**
* IMAP邮箱提供商
* 100%
*/
import Imap = require('imap');
import { simpleParser } from 'mailparser';
import { IEmailProvider, EmailMessage, EmailProviderConfig } from './IEmailProvider';
export class ImapEmailProvider implements IEmailProvider {
private config: EmailProviderConfig;
private imap: any = null;
private connected: boolean = false;
constructor(config: EmailProviderConfig) {
this.config = config;
}
/**
*
*/
async connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.imap = new Imap({
user: this.config.user!,
password: this.config.password!,
host: this.config.host!,
port: this.config.port!,
tls: this.config.tls!,
tlsOptions: this.config.tlsOptions || { rejectUnauthorized: false }
});
this.imap.once('ready', () => {
this.connected = true;
console.log(`[IMAP] 已连接到邮箱: ${this.config.user}`);
resolve();
});
this.imap.once('error', (err: Error) => {
console.error(`[IMAP] 连接失败: ${err.message}`);
reject(err);
});
this.imap.once('end', () => {
this.connected = false;
console.log('[IMAP] 连接已关闭');
});
this.imap.connect();
});
}
/**
*
*/
disconnect(): void {
if (this.imap && this.connected) {
this.imap.end();
}
}
/**
*
*/
async getLatestEmails(count: number = 50, folder: string = 'INBOX'): Promise<EmailMessage[]> {
if (!this.connected) {
await this.connect();
}
return new Promise((resolve, reject) => {
this.imap.openBox(folder, false, (err: Error, box: any) => {
if (err) {
reject(err);
return;
}
// 搜索条件:只搜索未读邮件
this.imap.search(['UNSEEN'], (err: Error, results: number[]) => {
if (err) {
reject(err);
return;
}
if (!results || results.length === 0) {
resolve([]);
return;
}
console.log(`[IMAP] 搜索到 ${results.length} 封未读邮件`);
// 只取最新的N封
const uids = results.slice(-count);
const emails: EmailMessage[] = [];
let processedCount = 0;
const totalCount = uids.length;
const fetch = this.imap.fetch(uids, {
bodies: '',
markSeen: true
});
fetch.on('message', (msg: any) => {
msg.on('body', (stream: any) => {
simpleParser(stream, (err: Error | null, parsed: any) => {
if (err) {
console.warn(`[IMAP] 解析邮件失败: ${err.message}`);
} else {
emails.push({
uid: msg.uid,
from: parsed.from?.text || '',
to: parsed.to?.text || '',
subject: parsed.subject || '',
date: parsed.date,
text: parsed.text || '',
html: parsed.html || '',
headers: parsed.headers
});
}
processedCount++;
// 所有邮件都处理完后才resolve
if (processedCount === totalCount) {
console.log(`[IMAP] 成功解析 ${emails.length} 封邮件`);
resolve(emails);
}
});
});
});
fetch.once('error', (err: Error) => {
reject(err);
});
});
});
});
}
/**
*
*/
async searchBySubject(subject: string, sinceDays: number = 1): Promise<EmailMessage[]> {
if (!this.connected) {
await this.connect();
}
return new Promise((resolve, reject) => {
this.imap.openBox('INBOX', false, (err: Error, box: any) => {
if (err) {
reject(err);
return;
}
const sinceDate = new Date();
sinceDate.setDate(sinceDate.getDate() - sinceDays);
this.imap.search([['SINCE', sinceDate], ['SUBJECT', subject]], (err: Error, results: number[]) => {
if (err) {
reject(err);
return;
}
if (!results || results.length === 0) {
resolve([]);
return;
}
const emails: EmailMessage[] = [];
const fetch = this.imap.fetch(results, {
bodies: '',
markSeen: true
});
fetch.on('message', (msg: any) => {
msg.on('body', (stream: any) => {
simpleParser(stream, (err: Error | null, parsed: any) => {
if (err) {
console.warn(`[IMAP] 解析邮件失败: ${err.message}`);
return;
}
emails.push({
uid: msg.uid,
from: parsed.from?.text || '',
to: parsed.to?.text || '',
subject: parsed.subject || '',
date: parsed.date,
text: parsed.text || '',
html: parsed.html || '',
headers: parsed.headers
});
});
});
});
fetch.once('error', (err: Error) => {
reject(err);
});
fetch.once('end', () => {
resolve(emails);
});
});
});
});
}
/**
*
*/
async markAsRead(uid: number): Promise<void> {
if (!this.connected) {
return;
}
return new Promise((resolve, reject) => {
this.imap.addFlags(uid, ['\\Seen'], (err: Error) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
}

View File

@ -0,0 +1,43 @@
/**
*
*
* 100%
*/
import { EmailMessage } from '../IEmailProvider';
export abstract class BaseParser {
protected siteName: string;
constructor(siteName: string) {
this.siteName = siteName;
}
/**
*
*/
abstract canParse(email: EmailMessage): boolean;
/**
*
*/
abstract extractCode(email: EmailMessage): string | null;
/**
*
*/
protected extractByRegex(content: string, pattern: RegExp): string | null {
if (!content) return null;
const match = content.match(pattern);
return match ? match[1] : null;
}
/**
* HTML中提取文本
*/
protected stripHtml(html: string): string {
if (!html) return '';
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
}
}

View File

@ -0,0 +1,128 @@
/**
* Windsurf邮件解析器
* Windsurf发送的验证码邮件
* 100%
*/
import { BaseParser } from './BaseParser';
import { EmailMessage } from '../IEmailProvider';
export class WindsurfParser extends BaseParser {
private senderKeywords: string[];
private subjectKeywords: string[];
private codePatterns: RegExp[];
constructor() {
super('Windsurf');
// Windsurf邮件的特征
this.senderKeywords = ['windsurf', 'codeium', 'exafunction'];
this.subjectKeywords = ['verify', 'verification', 'code', '验证', 'welcome'];
// 验证码的正则表达式(根据实际邮件调整)
this.codePatterns = [
// HTML格式: <h1 class="code_xxx">866172</h1>
/<h1[^>]*class="code[^"]*"[^>]*>(\d{6})<\/h1>/i,
// 常见格式
/6 digit code[^0-9]*(\d{6})/i,
/verification code[^0-9]*(\d{6})/i,
/verify.*code:?\s*(\d{6})/i,
// 纯6位数字最后尝试
/\b(\d{6})\b/
];
}
/**
* Windsurf的验证码邮件
*/
canParse(email: EmailMessage): boolean {
if (!email) return false;
const from = (email.from || '').toLowerCase();
const subject = (email.subject || '').toLowerCase();
// 检查发件人
const hasSender = this.senderKeywords.some(keyword =>
from.includes(keyword)
);
// 检查主题
const hasSubject = this.subjectKeywords.some(keyword =>
subject.includes(keyword)
);
return hasSender || hasSubject;
}
/**
*
*/
extractCode(email: EmailMessage): string | null {
if (!email) return null;
// 优先从HTML提取
let code = this.extractFromHtml(email.html);
if (code) return code;
// 其次从纯文本提取
code = this.extractFromText(email.text);
if (code) return code;
return null;
}
/**
* HTML内容提取验证码
*/
private extractFromHtml(html: string): string | null {
if (!html) return null;
// 先尝试直接从HTML提取保留HTML标签
for (const pattern of this.codePatterns) {
const code = this.extractByRegex(html, pattern);
if (code && this.validateCode(code)) {
return code;
}
}
// 如果HTML提取失败再去除标签后尝试
const text = this.stripHtml(html);
return this.extractFromText(text);
}
/**
*
*/
private extractFromText(text: string): string | null {
if (!text) return null;
// 尝试所有正则表达式
for (const pattern of this.codePatterns) {
const code = this.extractByRegex(text, pattern);
if (code && this.validateCode(code)) {
return code;
}
}
return null;
}
/**
*
*/
private validateCode(code: string): boolean {
if (!code) return false;
// Windsurf验证码是6位数字
if (code.length !== 6) {
return false;
}
// 应该是纯数字
if (!/^\d{6}$/.test(code)) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,105 @@
/**
*
* Provider共享API
*/
import { IActionFactory, IActionContext } from '../core/interfaces/IAction';
export interface IWorkflowStep {
action: string;
name?: string;
[key: string]: any;
}
export interface IWorkflowResult {
success: boolean;
steps: number;
errors: any[];
duration: number;
}
export class WorkflowEngine {
private workflow: IWorkflowStep[];
private context: IActionContext;
private actionFactory: IActionFactory;
private startTime: number = 0;
constructor(
workflow: IWorkflowStep[],
context: IActionContext,
actionFactory: IActionFactory
) {
this.workflow = workflow;
this.context = context;
this.actionFactory = actionFactory;
}
async execute(): Promise<IWorkflowResult> {
this.startTime = Date.now();
const errors: any[] = [];
let completedSteps = 0;
console.log(`\n[WorkflowEngine] Starting workflow with ${this.workflow.length} steps\n`);
for (let i = 0; i < this.workflow.length; i++) {
const step = this.workflow[i];
try {
console.log(`[WorkflowEngine] [${i + 1}/${this.workflow.length}] ${step.name || step.action}`);
await this.executeStep(step);
completedSteps++;
} catch (error: any) {
errors.push({
step: i + 1,
name: step.name || step.action,
error: error.message
});
// 如果步骤不是可选的,停止执行
if (!step.optional) {
console.error(`[WorkflowEngine] ❌ Fatal error at step ${i + 1}, stopping workflow`);
break;
}
console.warn(`[WorkflowEngine] ⚠️ Step ${i + 1} failed but is optional, continuing...`);
}
}
const duration = Date.now() - this.startTime;
const success = errors.length === 0 || errors.every((e: any, i: number) =>
this.workflow[i]?.optional
);
console.log(`\n[WorkflowEngine] Workflow completed: ${completedSteps}/${this.workflow.length} steps`);
console.log(`[WorkflowEngine] Duration: ${(duration / 1000).toFixed(2)}s`);
console.log(`[WorkflowEngine] Errors: ${errors.length}\n`);
return {
success,
steps: completedSteps,
errors,
duration
};
}
private async executeStep(step: IWorkflowStep): Promise<void> {
// 从ActionFactory获取Action类
const ActionClass = this.actionFactory.getAction(step.action);
if (!ActionClass) {
throw new Error(`Unknown action: ${step.action}`);
}
// 创建Action实例参数顺序context, config
const action = new ActionClass(this.context, step);
// 执行(多态!不需要知道具体实现)
const result = await action.execute();
if (!result.success) {
throw new Error(result.error?.message || 'Action failed');
}
}
}

View File

@ -0,0 +1,67 @@
/**
*
*/
import { BrowserFactory } from '../src/factory/BrowserFactory';
import { BrowserProviderType } from '../src/core/types';
import { AdsPowerProvider } from '../src/providers/adspower/AdsPowerProvider';
describe('Browser Automation Framework', () => {
describe('Factory', () => {
test('should have AdsPower registered', () => {
const providers = BrowserFactory.getAvailableProviders();
expect(providers).toContain(BrowserProviderType.ADSPOWER);
});
test('should create AdsPower provider', () => {
const provider = BrowserFactory.create(BrowserProviderType.ADSPOWER, {
profileId: 'test-profile'
});
expect(provider).toBeInstanceOf(AdsPowerProvider);
expect(provider.getName()).toBe('AdsPower');
});
test('should throw error for unknown provider', () => {
expect(() => {
BrowserFactory.create('unknown' as BrowserProviderType);
}).toThrow();
});
});
describe('AdsPower Provider', () => {
let provider: AdsPowerProvider;
beforeEach(() => {
provider = new AdsPowerProvider({
profileId: 'test-profile',
siteName: 'Test'
});
});
test('should have correct metadata', () => {
expect(provider.getName()).toBe('AdsPower');
expect(provider.getVersion()).toBe('1.0.0');
expect(provider.isFree()).toBe(false);
});
test('should have correct capabilities', () => {
const caps = provider.getCapabilities();
expect(caps.stealth).toBe(true);
expect(caps.fingerprint).toBe(true);
expect(caps.cloudflareBypass).toBe(true);
expect(caps.stripeCompatible).toBe(true);
});
test('should validate config', async () => {
await expect(provider.validateConfig()).resolves.toBe(true);
});
test('should fail validation without profileId', async () => {
const invalidProvider = new AdsPowerProvider({});
await expect(invalidProvider.validateConfig()).rejects.toThrow();
});
});
});

View File

@ -0,0 +1,124 @@
/**
* Windsurf Workflow
* 使TypeScript架构测试旧的YAML workflow
*/
import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'js-yaml';
import { AdsPowerProvider } from '../src/providers/adspower/AdsPowerProvider';
import { WorkflowEngine } from '../src/workflow/WorkflowEngine';
interface WindsurfConfig {
site: string;
workflow: any[];
errorHandling?: any;
}
async function runWindsurfWorkflow() {
console.log('🚀 Starting Windsurf Workflow Test...\n');
// 1. 读取YAML配置
const configPath = path.join(__dirname, '../configs/sites/windsurf.yaml');
console.log(`📄 Loading config from: ${configPath}`);
if (!fs.existsSync(configPath)) {
console.error('❌ Config file not found!');
console.log('Please copy windsurf.yaml to: browser-automation-ts/configs/sites/windsurf.yaml');
process.exit(1);
}
const configContent = fs.readFileSync(configPath, 'utf8');
const config = yaml.load(configContent) as WindsurfConfig;
console.log(`✅ Loaded workflow with ${config.workflow.length} steps\n`);
// 2. 初始化AdsPower Provider
console.log('🌐 Initializing AdsPower Provider...');
const provider = new AdsPowerProvider({
profileId: process.env.ADSPOWER_USER_ID,
siteName: 'Windsurf'
});
try {
// 3. 启动浏览器
const result = await provider.launch();
console.log('✅ Browser launched successfully\n');
// 4. 准备Context
const context = {
page: result.page,
browser: result.browser,
logger: console,
data: {
// 可以从环境变量或其他地方加载账号数据
account: {
email: process.env.WINDSURF_EMAIL || 'test@example.com',
password: process.env.WINDSURF_PASSWORD || 'password123'
}
},
siteConfig: {
url: 'https://codeium.com',
name: 'Windsurf'
},
config: config,
siteName: 'Windsurf'
};
// 5. 创建WorkflowEngine
const engine = new WorkflowEngine(
config.workflow,
context,
provider.getActionFactory()
);
// 6. 执行Workflow
console.log('▶️ Starting workflow execution...\n');
const workflowResult = await engine.execute();
// 7. 输出结果
console.log('\n' + '='.repeat(60));
console.log('📊 Workflow Execution Summary');
console.log('='.repeat(60));
console.log(`Status: ${workflowResult.success ? '✅ SUCCESS' : '❌ FAILED'}`);
console.log(`Steps Completed: ${workflowResult.steps}/${config.workflow.length}`);
console.log(`Duration: ${(workflowResult.duration / 1000).toFixed(2)}s`);
console.log(`Errors: ${workflowResult.errors.length}`);
if (workflowResult.errors.length > 0) {
console.log('\n❌ Errors:');
workflowResult.errors.forEach((err: any, i: number) => {
console.log(` ${i + 1}. Step ${err.step} (${err.name}): ${err.error}`);
});
}
console.log('='.repeat(60) + '\n');
// 8. 等待查看结果
console.log('⏸️ Waiting 5 seconds before closing...');
await new Promise(resolve => setTimeout(resolve, 5000));
} catch (error: any) {
console.error('\n❌ Fatal error:', error.message);
console.error(error.stack);
} finally {
// 9. 关闭浏览器
try {
console.log('\n🔒 Closing browser...');
await provider.close();
console.log('✅ Browser closed successfully');
} catch (e: any) {
console.error('⚠️ Error closing browser:', e.message);
}
}
}
// 运行测试
runWindsurfWorkflow()
.then(() => {
console.log('\n✅ Test completed!');
process.exit(0);
})
.catch((error) => {
console.error('\n❌ Test failed:', error);
process.exit(1);
});

View File

@ -0,0 +1,51 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"allowJs": false,
"checkJs": false,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": ".",
"paths": {
"@core/*": ["src/core/*"],
"@providers/*": ["src/providers/*"],
"@factory/*": ["src/factory/*"]
},
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"noEmitOnError": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"tests"
]
}