Compare commits

..

No commits in common. "develop3" and "master" have entirely different histories.

267 changed files with 207 additions and 60605 deletions

21
.env
View File

@ -1,21 +0,0 @@
# AdsPower 指纹浏览器配置
ADSPOWER_USER_ID=k1728p8l
ADSPOWER_API_KEY=35de43696f6241f3df895f2f48777a99
# 使用 127.0.0.1 而不是 local.adspower.net避免 TUN VPN 路由冲突
ADSPOWER_API=http://127.0.0.1:50325
# MySQL 数据库配置
MYSQL_HOST=172.22.222.111
MYSQL_PORT=3306
MYSQL_USER=windsurf-auto-register
MYSQL_PASSWORD=Qichen5210523
MYSQL_DATABASE=windsurf-auto-register
# CapSolver 验证码识别
CAPSOLVER_API_KEY=CAP-0FCDDA4906E87D9F4FF68EAECD34E320876FBA70E4F30EA1ADCD264EDB15E4BF
# 2Captcha 验证码识别 (支持 Stripe hCaptcha)
TWOCAPTCHA_API_KEY=4e6ac0ee29459018fd5e0c454163cd4e
# YesCaptcha 验证码识别 (优先使用)
YESCAPTCHA_API_KEY=a8a04f2e8ceab43cdf3793e2b72bf4d76f4f4a6b81789

2
.gitignore vendored
View File

@ -9,6 +9,8 @@ logs
npm-debug.log* npm-debug.log*
# Environment # Environment
.env
.env.local
# IDE # IDE
.vscode/ .vscode/

View File

@ -1,241 +0,0 @@
# CapSolver 验证失败问题分析
## 问题现象
使用 CapSolver API Key 配置后,无法自动绕过 Cloudflare Turnstile 验证。
## 系统架构分析
### 当前配置状态
- ✅ API Key 已配置: `CAP-0FCDDA4906E87D9F4FF68EAECD34E320876FBA70E4F30EA1ADCD264EDB15E4BF`
- ✅ 扩展路径存在: `extensions/capsolver/`
- ✅ 配置文件正确: `extensions/capsolver/assets/config.js`
- ✅ 扩展关键文件完整:
- manifest.json ✓
- config.js ✓
- cloudflare-content.js ✓
- inject-turnstile.js ✓
- background.js ✓
### 扩展工作机制
#### 1. 扩展注入流程
```
manifest.json (content_scripts)
cloudflare-content.js (run_at: document_start)
inject-turnstile.js (注入到页面)
拦截 window.turnstile.render()
发送消息给 background.js
调用 CapSolver API
返回 token 并触发回调
```
#### 2. inject-turnstile.js 的关键逻辑
- 创建 `window.turnstile` Proxy
- 拦截 `render()` 方法
- 提取 sitekey 和 callback
- 发送 `registerTurnstile` 消息
- 监听 `turnstileSolved` 消息
- 调用原始回调函数
#### 3. windsurf.js 的等待逻辑
```javascript
// 步骤2: 507-542行
if (this.capsolverKey) {
// 等待按钮激活(检测扩展是否完成)
while (elapsed < 60000) {
const buttonEnabled = await this.page.evaluate(() => {
const button = document.querySelector('button');
return button && !button.disabled && button.textContent.trim() === 'Continue';
});
if (buttonEnabled) {
// 成功
break;
}
await new Promise(resolve => setTimeout(resolve, 2000));
elapsed = Date.now() - startTime;
}
}
```
## 可能的问题原因
### 问题1: 扩展未正确加载 ⭐最可能⭐
**症状**:
- 扩展文件存在但未被浏览器激活
- 控制台没有扩展日志
- inject-turnstile.js 未注入到页面
**原因**:
1. Windows 路径问题已修复第241-246行
2. Manifest V3 的 service worker 限制
3. Chrome 扩展权限问题
**验证方法**:
```javascript
// 在页面执行
console.log(window.turnstile); // 应该是Proxy对象
console.log(window.registerTurnstileData); // 应该有sitekey
```
### 问题2: Turnstile API未加载完成
**症状**:
- 第495行报错: "Turnstile API 未加载"
- scripts数组为空
**原因**:
1. Cloudflare CDN 被墙(中国大陆网络)
2. 扩展阻止了 Turnstile 脚本加载
3. 页面导航过快,脚本未加载完
**当前处理**:
- 第502-503行: 等待30秒重试
### 问题3: 扩展脚本执行时机问题
**症状**:
- inject-turnstile.js 注入太晚
- window.turnstile 已经被原始脚本创建
- Proxy 拦截失败
**原因**:
- cloudflare-content.js 的 run_at: "document_start" 可能不够早
- React 应用的 Turnstile 组件可能动态加载
### 问题4: API Key 或余额问题
**症状**:
- 扩展调用 API 失败
- 60秒超时
**验证**:
- 检查 CapSolver 控制台余额
- 检查 API Key 是否有效
- 检查网络能否访问 api.capsolver.com
### 问题5: Content Script与页面隔离
**症状**:
- Content script 无法访问页面的 window 对象
- inject-turnstile.js 需要通过 injected script 运行
**可能问题**:
- 扩展的 web_accessible_resources 配置
- script 注入方式不对
## 调试建议
### 步骤1: 验证扩展是否加载
```bash
# 运行注册流程
node src/cli.js register -s windsurf --keep-browser-open
# 在浏览器控制台检查:
chrome://extensions/
# 查看 "Captcha Solver" 是否已启用
# 查看是否有错误
```
### 步骤2: 检查页面注入
在 Turnstile 页面打开控制台:
```javascript
// 检查扩展是否注入
console.log(window.turnstile);
// 应该输出: Proxy {render: ƒ, reset: ƒ, ...}
// 检查注册数据
console.log(window.registerTurnstileData);
// 应该输出: {sitekey: "0x4AAA..."}
// 检查回调
console.log(window.turnstileCallback);
// 应该是一个函数
```
### 步骤3: 检查扩展日志
```javascript
// 监听消息
window.addEventListener('message', (event) => {
if (event.data?.type === 'registerTurnstile') {
console.log('扩展已注册Turnstile:', event.data);
}
if (event.data?.type === 'turnstileSolved') {
console.log('扩展已解决:', event.data);
}
});
```
### 步骤4: 网络检查
```bash
# 检查能否访问 Cloudflare
curl https://challenges.cloudflare.com/turnstile/v0/api.js
# 检查能否访问 CapSolver
curl https://api.capsolver.com/
# 如果被墙,需要代理
```
## 推荐修复方案
### 方案A: 添加详细日志 ⭐推荐⭐
在 windsurf.js 中添加调试输出:
```javascript
// 步骤2: 设置密码后
// 检查扩展注入状态
const extensionStatus = await this.page.evaluate(() => {
return {
hasTurnstileProxy: window.turnstile && window.turnstile.toString().includes('Proxy'),
hasRegisterData: !!window.registerTurnstileData,
hasCallback: !!window.turnstileCallback,
sitekey: window.registerTurnstileData?.sitekey
};
});
logger.info(this.siteName, '扩展状态:', JSON.stringify(extensionStatus, null, 2));
```
### 方案B: 强制等待 Turnstile API
```javascript
// 在步骤2中点击Continue之前
await this.page.waitForFunction(
() => typeof window.turnstile !== 'undefined',
{ timeout: 30000 }
);
```
### 方案C: 手动注入 inject-turnstile.js
如果扩展未正确加载,可以手动注入:
```javascript
const fs = require('fs');
const injectScript = fs.readFileSync(
path.join(projectRoot, 'extensions/capsolver/assets/inject/inject-turnstile.js'),
'utf8'
);
await this.page.evaluateOnNewDocument(injectScript);
```
### 方案D: 使用 Chrome DevTools Protocol
```javascript
const client = await this.page.target().createCDPSession();
await client.send('Page.addScriptToEvaluateOnNewDocument', {
source: injectScript
});
```
## 下一步行动
1. **立即执行**: 添加详细调试日志方案A
2. **验证扩展**: 运行测试并检查浏览器扩展状态
3. **收集信息**:
- 扩展是否加载?
- window.turnstile 是否是 Proxy
- 控制台有什么错误?
4. **根据结果**: 选择对应的修复方案

View File

@ -1,199 +0,0 @@
# CapSolver API 注入修复实施记录
## 修改完成时间
2025-11-17 14:47
## 实施的修改
### 文件: `src/tools/account-register/sites/windsurf.js`
### 方法: `solveWithCapSolver()` (第767-962行)
### 修改内容
#### 1. 改进 Token 注入第773-783行
**修改前**: 仅简单设置 input.value
```javascript
await this.page.evaluate((solution) => {
document.querySelector('input[name="cf-turnstile-response"]').value = solution;
}, token);
```
**修改后**: 注入 token 并触发事件
```javascript
await this.page.evaluate((token) => {
const input = document.querySelector('input[name="cf-turnstile-response"]');
if (input) {
input.value = token;
// 触发各种事件以通知页面
['input', 'change', 'blur'].forEach(eventType => {
input.dispatchEvent(new Event(eventType, { bubbles: true }));
});
}
}, token);
```
**改进**: 触发 DOM 事件,让监听器检测到 token 变化
#### 2. 添加回调函数触发第787-862行
**新增功能**: 多种方法查找并触发 Turnstile 回调
- **方法A**: 查找 React 组件回调 `window.cf__reactTurnstileOnLoad`
- **方法B**: 从 DOM 元素 `[data-callback]` 属性获取回调名
- **方法C**: 使用 `window.turnstile.reset()``window.turnstile.execute()` API
- **方法D**: 遍历 window 对象查找包含 'turnstile' 的函数
**日志输出**: 显示每个方法的触发结果(成功 ✓ 或失败 ✗)
#### 3. 添加按钮强制激活第876-906行
**新增功能**: 后备方案,确保按钮可点击
```javascript
const buttonActivated = await this.page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button'));
buttons.forEach(btn => {
if (btn.textContent.trim() === 'Continue' ||
btn.textContent.trim() === 'Submit') {
if (btn.disabled) {
btn.disabled = false;
btn.removeAttribute('disabled');
btn.classList.remove('disabled');
}
}
});
});
```
**改进**: 即使回调失败,也能确保按钮可点击
#### 4. 改进验证完成检测第908-962行
**修改前**:
- 检查 iframe 状态(可能无法访问)
- 超时时间 20 秒
- 失败时等待 30 秒手动操作
**修改后**:
- **检查1**: 按钮必须激活
- **检查2**: hidden input 必须有值
- **检查3**: iframe 状态(可选)
- 超时时间 10 秒(缩短)
- 轮询间隔 500ms更频繁
- 失败时输出详细调试信息
- 如果按钮已激活且有 token认为成功
- 失败时等待 10 秒(缩短)
**调试信息输出**:
```javascript
{
buttonDisabled: false,
buttonText: "Continue",
hasToken: true,
tokenLength: 2000
}
```
## 关键改进点
### 1. 完整的验证流程模拟
- ✅ Token 注入
- ✅ 事件触发
- ✅ 回调调用
- ✅ 按钮激活
- ✅ 状态验证
### 2. 多重保障机制
- 4 种回调触发方法
- 强制按钮激活(后备)
- 灵活的状态检测
### 3. 详细的调试信息
- Token 长度显示
- 每个回调的触发结果
- 按钮激活状态
- 最终状态快照
### 4. 更合理的超时处理
- 缩短等待时间10秒而非30秒
- 提供详细的失败原因
- 智能判断是否继续
## 测试步骤
1. **运行注册流程**:
```bash
node src/cli.js register -s windsurf --keep-browser-open
```
2. **观察日志输出**:
- `[CapSolver] 注入 token 到页面 (长度: XXX)...`
- `[CapSolver] 回调触发结果:`
- 查看哪些方法成功(✓)
- 查看哪些方法失败(✗)
- `[CapSolver] 已强制激活按钮``按钮已经是激活状态`
- `[CapSolver] ✓ 验证完成确认通过`
3. **检查浏览器**:
- Turnstile checkbox 是否勾选?
- Continue 按钮是否可点击?
- 是否自动进入下一步?
4. **如果失败**:
- 查看 `[CapSolver] 当前状态:` 输出
- 判断是回调问题、按钮问题还是 iframe 问题
- 根据具体问题选择下一步方案
## 预期结果
### 成功场景
```
[Windsurf] [CapSolver] ✓ 获取到token: 0.kDqN3tMdKWreLsL90...
[Windsurf] [CapSolver] 注入 token 到页面 (长度: 2048)...
[Windsurf] [CapSolver] ✓ Token 已注入到 hidden input
[Windsurf] [CapSolver] 尝试触发 Turnstile 回调...
[Windsurf] [CapSolver] 回调触发结果:
[Windsurf] ✓ cf__reactTurnstileOnLoad
[Windsurf] ✓ window.turnstileCallback
[Windsurf] [CapSolver] 按钮已经是激活状态
[Windsurf] [CapSolver] ✓ 验证完成确认通过
[Windsurf] [CapSolver] ✓ 已点击 Continue 按钮
[Windsurf] 步骤 2 完成
```
### 部分成功场景
```
[Windsurf] [CapSolver] 回调触发结果:
[Windsurf] ✗ cf__reactTurnstileOnLoad: not a function
[Windsurf] ✓ data-callback: onTurnstileComplete
[Windsurf] [CapSolver] ✓ 已强制激活按钮
[Windsurf] [CapSolver] ✓ 验证完成确认通过
```
→ 至少一个回调成功即可
### 失败场景
```
[Windsurf] [CapSolver] ⚠️ 未找到任何回调函数
[Windsurf] [CapSolver] ✓ 已强制激活按钮
[Windsurf] [CapSolver] 验证完成检测超时
[Windsurf] [CapSolver] 当前状态: {"buttonDisabled":false,"buttonText":"Continue","hasToken":true,"tokenLength":2048}
[Windsurf] [CapSolver] ✓ 状态检查通过,继续执行
```
→ 按钮已激活且有 token继续执行
## 后续方案
如果当前修改仍然失败,下一步考虑:
### 方案3: 提前拦截 Turnstile 初始化
`initBrowser()` 中使用 `evaluateOnNewDocument()` 提前注入拦截代码
### 方案2: 使用 CDP 操作 iframe
通过 Chrome DevTools Protocol 直接访问 Turnstile iframe
### 方案4: 混合方案
不用完整扩展,只注入 `inject-turnstile.js` 脚本
## 备注
- 此修改不影响扩展加载方式第214-300行
- 废弃的 API 方法仍保留但已标记 `@deprecated`
- 步骤2密码设置中的扩展等待逻辑第507-542行保持不变

View File

@ -1,155 +0,0 @@
# 快速开始指南
## 🚀 5分钟上手
### 1. 基本使用
生成一张银联卡(默认):
```bash
node src/cli.js card
```
输出:
```
6228367546245545|08|29|783
```
### 2. 常用命令
```bash
# 生成Visa卡
node src/cli.js card -t visa
# 批量生成10张银联卡
node src/cli.js card -n 10
# JSON格式输出
node src/cli.js card -f json
# 美化格式
node src/cli.js card -f pretty
# 查看支持的卡类型
node src/cli.js card list-types
# 查看帮助
node src/cli.js --help
```
### 3. 便捷脚本
也可以使用 npm scripts
```bash
# 生成一张卡
npm run card
# 使用自定义参数(需要加 --
npm run card -- -t visa -n 5
```
### 4. 批量生成并保存
```bash
# 生成100张银联卡并保存
node src/cli.js card -n 100 > cards.txt
# 生成CSV格式
node src/cli.js card -n 50 -f csv > cards.csv
# 生成JSON格式每行一个JSON对象
node src/cli.js card -n 20 -f json > cards.json
```
### 5. 编程使用
创建脚本 `test.js`
```javascript
const CardGenerator = require('./src/tools/card-generator/generator');
const generator = new CardGenerator();
// 生成单张卡
const card = generator.generate('unionpay');
console.log(card);
// 批量生成
const cards = generator.generateBatch(10, 'visa');
console.log(cards);
```
运行:
```bash
node test.js
```
## 📊 输出格式对比
| 格式 | 命令 | 输出示例 |
|------|------|---------|
| **Pipe** | `node src/cli.js card` | `6228367546245545\|08\|29\|783` |
| **JSON** | `node src/cli.js card -f json` | `{"number":"6228367546245545","month":"08","year":"29","cvv":"783","type":"中国银联 (UnionPay)"}` |
| **CSV** | `node src/cli.js card -f csv` | `6228367546245545,08,29,783` |
| **Pretty** | `node src/cli.js card -f pretty` | 多行美化格式 |
## 🎯 常见使用场景
### 测试场景1: 支付系统测试
```bash
# 生成各种卡类型用于测试
node src/cli.js card -t visa -n 5
node src/cli.js card -t mastercard -n 5
node src/cli.js card -t amex -n 5
```
### 测试场景2: 前端表单测试
```bash
# 生成JSON格式用于mock数据
node src/cli.js card -n 20 -f json > mock-cards.json
```
### 测试场景3: 数据库填充
```bash
# 生成CSV格式导入数据库
node src/cli.js card -t unionpay -n 1000 -f csv > import.csv
```
## 🔧 运行示例代码
查看完整示例:
```bash
node examples/basic-usage.js
```
## ⚡ 性能提示
- 单次生成推荐不超过 10000 张卡
- 大批量生成建议分批进行
- 使用 pipe 格式性能最优
## ❓ 遇到问题?
1. 查看完整文档:`README.md`
2. 查看工具文档:`docs/tools/card-generator.md`
3. 运行示例:`node examples/basic-usage.js`
## 📝 输出格式说明
**Pipe格式** (默认)`卡号|月|年|CVV`
- 适合:文本处理、批量导入
- 分隔符:`|`
**JSON格式**完整JSON对象
- 适合API测试、前端mock
- 包含卡号、有效期、CVV、卡类型
**CSV格式**:逗号分隔
- 适合Excel、数据库导入
- 分隔符:`,`
**Pretty格式**:美化显示
- 适合:人工阅读、展示
- 多行格式化输出

View File

@ -11,7 +11,6 @@
- 🔧 **高度可配置** - 支持多种输出格式和自定义选项 - 🔧 **高度可配置** - 支持多种输出格式和自定义选项
- 📦 **轻量级** - 最小依赖,快速安装 - 📦 **轻量级** - 最小依赖,快速安装
- 🌍 **跨平台** - 支持 macOS、Linux、Windows - 🌍 **跨平台** - 支持 macOS、Linux、Windows
- 🛡️ **反检测技术** - 使用最新的 rebrowser-puppeteer降低被识别风险
## 📦 安装 ## 📦 安装
@ -239,15 +238,12 @@ node src/index.js card -n 5
## 📋 待办事项 ## 📋 待办事项
- [x] 信用卡生成器支持Luhn算法 - [ ] 添加更多工具(邮箱生成器、用户名生成器等)
- [x] 账号注册工具(支持步骤化流程)
- [x] 反检测技术rebrowser-puppeteer
- [x] 人类行为模拟
- [ ] 添加更多网站注册脚本
- [ ] 添加单元测试 - [ ] 添加单元测试
- [ ] 验证码识别集成 - [ ] 添加配置文件支持
- [ ] 代理池管理
- [ ] 发布到 npm - [ ] 发布到 npm
- [ ] 添加交互式模式
- [ ] 支持自定义卡号规则
## 🤝 贡献 ## 🤝 贡献

View File

@ -1,44 +0,0 @@
# CapSolver 扩展配置(官方标准方法)
## 1. 下载扩展
访问https://github.com/capsolver/capsolver-browser-extension/releases
下载最新版本的 `capsolver-extension.zip`
## 2. 解压到项目
```bash
# 解压到 extensions 目录
unzip capsolver-extension.zip -d extensions/capsolver
```
## 3. 配置 API Key
编辑文件:`extensions/capsolver/assets/config.js`
找到并修改:
```javascript
apiKey: 'CAP-0FCDDA4906E87D9F4FF68EAECD34E320876FBA70E4F30EA1ADCD264EDB15E4BF'
```
## 4. 运行测试
```bash
node src/cli.js register -s windsurf --keep-browser-open
```
## 扩展会自动工作
- ✅ 自动检测 Cloudflare Turnstile
- ✅ 自动调用 CapSolver API
- ✅ **自动勾选 checkbox**
- ✅ 按钮激活后自动继续
## 代码已集成
代码已修改为自动加载扩展:
- 检查 `extensions/capsolver` 是否存在
- 启动时自动加载扩展
- 等待扩展自动处理验证码
- 无需手动操作

View File

@ -1,107 +0,0 @@
# 任务:删除 CAPSOLVER 相关代码
## Analysis
### CAPSOLVER 代码位置分析
通过完整阅读 `windsurf.js` 文件,发现 CAPSOLVER 相关代码分布在以下位置:
#### 1. 构造函数中的初始化 (第34-40行)
- 初始化 `this.capsolverKey` 变量
- 输出 CapSolver 启用/未配置的日志
#### 2. 浏览器初始化中的提示 (第319-327行)
- 在 `initBrowser()` 方法中提示用户安装 CapSolver 扩展
#### 3. 步骤2中的自动验证处理 (第543-580行)
- 在 `step2_setPassword()` 中使用 CapSolver 扩展自动处理 Turnstile
- 包含等待验证完成的逻辑
#### 4. 废弃的 API 方法 (第597-1021行)
- `solveWithCapSolver()` 方法
- 已标记为 @deprecated
- 使用 CapSolver API 方式(不是扩展方式)
#### 5. Cloudflare 验证处理方法 (第1023-1176行)
- `handleCloudflareVerification()` 方法
- 前半部分使用 CapSolver 扩展自动处理
- 后半部分回退到手动模式(使用 CloudflareHandler
### 核心依赖关系
- **步骤2 (`step2_setPassword()`)**: 依赖 CapSolver 进行 Turnstile 验证
- **手动验证回退**: 已有 `CloudflareHandler` 作为手动验证方案
- **环境变量**: `CAPSOLVER_API_KEY``.env` 文件中配置
### 删除策略
1. **保留手动验证逻辑**: CloudflareHandler 必须保留
2. **删除 CapSolver 扩展相关代码**: 包括初始化、提示、自动处理逻辑
3. **删除废弃的 API 方法**: 完整移除 `solveWithCapSolver()` 方法
4. **简化验证流程**: 直接使用手动验证,不再尝试自动验证
## Proposed Solution
### 方案选择:渐进式删除
采用安全的渐进式删除策略,确保每一步修改后代码仍可正常运行:
1. **删除构造函数中的 CapSolver 初始化**
- 移除 `this.capsolverKey` 变量
- 移除相关日志输出
2. **删除浏览器初始化中的 CapSolver 提示**
- 移除扩展安装提示
3. **简化 step2_setPassword() 方法**
- 移除 CapSolver 扩展自动处理部分第543-580行
- 保留按钮点击和页面跳转逻辑
4. **删除 solveWithCapSolver() 方法**
- 完整移除整个方法第597-1021行
5. **简化 handleCloudflareVerification() 方法**
- 移除 CapSolver 扩展自动处理部分第1027-1074行
- 直接使用 CloudflareHandler 手动验证
### 优势
- 代码更简洁,移除未使用的自动化依赖
- 保留完整的手动验证流程
- 不影响现有功能逻辑
## Plan
### 步骤1: 删除构造函数中的 CapSolver 初始化
- 文件: `windsurf.js`
- 行数: 34-40
- 操作: 删除 `this.capsolverKey` 及相关日志
### 步骤2: 删除浏览器初始化中的提示
- 文件: `windsurf.js`
- 行数: 319-327
- 操作: 删除 CapSolver 扩展提示
### 步骤3: 简化 step2_setPassword() 方法
- 文件: `windsurf.js`
- 行数: 543-580
- 操作: 删除 CapSolver 自动验证逻辑
### 步骤4: 删除 solveWithCapSolver() 方法
- 文件: `windsurf.js`
- 行数: 597-1021
- 操作: 完整删除整个方法及其注释
### 步骤5: 简化 handleCloudflareVerification() 方法
- 文件: `windsurf.js`
- 行数: 1027-1074
- 操作: 删除 CapSolver 扩展处理部分,直接使用手动验证
## Progress
- [ ] 步骤1: 删除构造函数中的 CapSolver 初始化
- [ ] 步骤2: 删除浏览器初始化中的提示
- [ ] 步骤3: 简化 step2_setPassword()
- [ ] 步骤4: 删除 solveWithCapSolver() 方法
- [ ] 步骤5: 简化 handleCloudflareVerification()
- [ ] 验证: 检查代码逻辑完整性

303
TODO.md
View File

@ -1,303 +0,0 @@
# 🎯 Windsurf 注册自动化 - 当前状态与问题
## ✅ 已完成
1. **CapSolver 集成** - 完成
- API Key: `CAP-028D3BE0F462CE98724F8568BBC87F692646994A91999FB986A2A5E4BE6BF65A`
- 已保存到 `.env` 文件
- 代码集成完成(`solveWithCapSolver` 方法)
- 价格: $1.20/1000次
- 成功率: 99%+
2. **浏览器方案确定** - rebrowser-puppeteer
- 使用: `"puppeteer": "npm:rebrowser-puppeteer@^23.9.0"`
- 内置反检测,不需要额外配置
- 已改回使用 `require('puppeteer')` 而不是 `puppeteer-real-browser`
---
## ⚠️ 当前问题
### 核心问题:浏览器选择混乱
**历史记录**
1. ✅ 最初使用 rebrowser-puppeteer - **可以绕过 Cloudflare**
2. ❌ 切换到 puppeteer-real-browser (turnstile: false) - 需要手动点击
3. ❌ 尝试 puppeteer-real-browser (turnstile: true) - **在付款页面出现循环点击问题**
4. ❌ 尝试 Playwright - Cloudflare 检测失败
5. ✅ **应该回到 rebrowser-puppeteer** - 原本就可以用
**问题根源**
- rebrowser-puppeteer **本身就能绕过 Cloudflare**
- 不需要 puppeteer-real-browser 的 turnstile 功能
- puppeteer-real-browser 在付款页面有干扰
---
## 🔧 当前测试方案
### ✅ 方案rebrowser-puppeteer + CapSolver已按官方文档修复
**CapSolver API 调用(已验证正确)**
- ✅ 使用 `AntiTurnstileTaskProxyLess`
- ✅ 1 秒轮询(官方推荐)
- ✅ 120 秒超时
- ✅ 错误检测和处理
- ✅ Token 获取正确
**当前问题2025-11-17 11:00 - 已修复)**
- ✅ API Key 正确配置
- ✅ Sitekey 动态获取改进(增加调试日志)
- ✅ Token 注入方式完全重写
**核心问题(已识别)**
- ❌ **Token 注入方式不正确**:之前只是简单设置 input.value
- ❌ **没有使用 window.turnstile API**
- ❌ **没有正确触发 Turnstile 回调机制**
**修复方案2025-11-17 11:00 已实施)**
1. ✅ 改进 sitekey 获取逻辑(增加来源追踪和 Widget ID 获取)
2. ✅ 使用 5 种方法注入 token
- 方法 1: `window.turnstile.reset()` + `window.turnstile.render()` 重新渲染
- 方法 2: 直接设置 hidden input 值并触发事件
- 方法 3: 调用页面回调函数
- 方法 4: 使用 `window.turnstile.ready()`
- 方法 5: 操作 iframe如果可访问
3. ✅ 改进验证完成检测检查按钮、token、iframe状态
4. ✅ 增加详细调试日志
**修复关键代码**
- Line 403-454: 改进 sitekey 获取,返回 {sitekey, method, widgetId}
- Line 545-684: 完全重写 token 注入逻辑,使用 window.turnstile API
- Line 660-684: 改进验证完成检测
---
## 🚨 最新发现的关键问题2025-11-17 13:18
### 问题CapSolver 扩展干扰 Stripe 支付页面
**现象**
- 步骤1-5全部成功完成
- 到达Stripe支付页面后填写银行卡信息
- 点击"订阅"按钮后,出现错误:"我们未验证该卡的付款方式"
- 或触发额外的 hCaptcha 验证
- 银行卡信息完全正确(已验证)
**验证测试**
- ✅ 复制Stripe支付链接到**真实浏览器**无CapSolver扩展
- ✅ 使用**完全相同的银行卡信息**
- ✅ **支付成功!**
**根本原因**
1. Stripe 有严格的反欺诈检测机制
2. 检测到 CapSolver 浏览器扩展的异常行为:
- 扩展修改了页面 DOM
- 扩展注入了脚本
- 浏览器指纹异常
3. Stripe 触发安全机制,拒绝处理支付
**问题分析**
```
步骤2: Cloudflare验证 → 需要CapSolver ✅
步骤6: Stripe支付 → CapSolver干扰支付 ❌
```
**矛盾点**
- 必须用CapSolver通过Cloudflare步骤2
- 但CapSolver会导致Stripe拒绝支付步骤6
---
### 解决方案(待选择实现)
#### 方案1: 两阶段浏览器切换 ⭐推荐⭐
**思路**只在需要时使用CapSolver支付时切换到干净浏览器
**实现步骤**
1. **步骤1-2**使用带CapSolver扩展的浏览器
2. **步骤2完成后**
- 保存登录状态cookies + localStorage
- 获取当前URL
- 关闭带扩展的浏览器
3. **重新启动干净浏览器**
- 不加载任何扩展
- 导航到保存的URL
- 恢复cookies和localStorage
4. **步骤3-6**:继续正常流程
**优点**
- ✅ 完全自动化
- ✅ 不触发Stripe安全检测
- ✅ 保持账号登录状态
- ✅ 最优雅的解决方案
**缺点**
- ❌ 需要实现会话保存/恢复逻辑
- ❌ 代码复杂度增加
**实现要点**
```javascript
// 步骤2完成后
const session = {
cookies: await page.cookies(),
localStorage: await page.evaluate(() => JSON.stringify(localStorage)),
url: page.url()
};
// 保存到内存或文件
await closeBrowser();
// 重启干净浏览器
await initBrowser({ skipExtension: true });
await page.goto(session.url);
await page.setCookie(...session.cookies);
await page.evaluate((ls) => Object.assign(localStorage, JSON.parse(ls)), session.localStorage);
```
---
#### 方案2: 手动支付模式(最简单)
**思路**:自动化到支付页面,然后提示用户手动完成
**实现步骤**
1. 步骤1-5自动完成
2. 到达Stripe支付页面后
- 输出支付链接到控制台
- 提示用户在真实浏览器中打开
- 等待用户确认支付完成
**优点**
- ✅ 实现简单5分钟
- ✅ 100%成功率
- ✅ 不需要修改复杂逻辑
**缺点**
- ❌ 需要人工介入
- ❌ 不是完全自动化
**实现要点**
```javascript
async step6_fillPayment() {
const checkoutUrl = this.page.url();
logger.info(this.siteName, '================================');
logger.info(this.siteName, '⚠️ 需要手动完成支付');
logger.info(this.siteName, '================================');
logger.info(this.siteName, '原因CapSolver扩展会干扰Stripe支付');
logger.info(this.siteName, '');
logger.info(this.siteName, '📋 支付链接:');
logger.info(this.siteName, checkoutUrl);
logger.info(this.siteName, '');
logger.info(this.siteName, '请复制上述链接到真实浏览器中完成支付');
logger.info(this.siteName, '================================');
// 等待用户确认
await this.waitForUserConfirmation();
}
```
---
#### 方案3: 动态卸载扩展(理论方案)
**思路**步骤2后卸载CapSolver扩展
**问题**
- ❌ Puppeteer 不支持运行时卸载扩展
- ❌ 需要重启浏览器
**结论**不可行等同于方案1
---
### 推荐决策
**短期**(立即可用):
→ **方案2手动支付模式**
- 快速实现
- 稳定可靠
- 只需5分钟
**长期**(最佳方案):
→ **方案1两阶段浏览器切换**
- 完全自动化
- 用户体验最佳
- 需要1-2小时开发
---
## 📋 测试计划
### 测试 1: rebrowser + CapSolver
```bash
node src/cli.js register -s windsurf --keep-browser-open
```
**预期结果**
1. 步骤 1-2: 正常填写
2. 步骤 3: Cloudflare 出现
- 如果 rebrowser 能绕过 → 直接通过
- 如果绕不过 → CapSolver 自动解决
3. 步骤 4-6: 正常完成(**重点测试付款页面**
**关键检查点**
- [ ] 步骤 3: Cloudflare 是否自动通过?
- [ ] 步骤 6: 付款页面是否有循环点击?
- [ ] 步骤 6: 银行卡输入是否成功?
---
## 🐛 已知 Bug
### Bug 1: puppeteer-real-browser (turnstile: true) 在付款页面循环点击
- **现象**: 一直点击 Stripe Link 的"保存信息"checkbox
- **原因**: turnstile 功能误判 checkbox 为验证码
- **解决**: 不使用 puppeteer-real-browser
### Bug 2: Playwright 被 Cloudflare 检测
- **现象**: 即使用了 stealth 插件也无法通过
- **原因**: Playwright 的特征容易被检测
- **解决**: 不使用 Playwright
---
## 📝 代码状态
### 当前代码使用:
- ✅ rebrowser-puppeteer已修改
- ✅ CapSolver API 集成(已完成)
- ✅ 自动回退到手动模式(已实现)
### 需要验证:
- [ ] rebrowser 能否自动绕过 Cloudflare
- [ ] 如果不能CapSolver 能否接管?
- [ ] 付款页面是否正常?
---
## 🎯 下一步行动
1. **立即测试**:运行注册流程,观察 Cloudflare 和付款页面
2. **记录结果**
- Cloudflare 是否自动通过?
- CapSolver 是否成功?
- 付款页面是否正常?
3. **根据结果调整**
- 如果成功 → 完成 ✅
- 如果失败 → 分析原因并调整
---
## 💡 重要提醒
**不要再切换浏览器方案!**
rebrowser-puppeteer 是最佳选择:
- ✅ 内置反检测
- ✅ 不干扰付款页面
- ✅ 配合 CapSolver 完美
**保持这个方案,专注优化!**

48
aaa.js
View File

@ -1,48 +0,0 @@
/**
* 验证生成的卡号是否符合规律
*/
const CardGenerator = require('./src/tools/card-generator/generator');
const Formatter = require('./src/tools/card-generator/formatter');
const { luhnCheck } = require('./src/shared/utils');
const generator = new CardGenerator();
const formatter = new Formatter();
console.log('=== 生成卡号验证 ===\n');
// 生成10张银联卡
console.log('生成10张银联卡进行验证:\n');
const cards = generator.generateBatch(100, 'unionpay');
cards.forEach((card, index) => {
const isValid = luhnCheck(card.number);
const status = isValid ? '✓' : '✗';
const formatted = formatter.format(card, 'pipe');
console.log(`${index + 1}. ${formatted} ${status}`);
});
console.log('\n=== 验证结果 ===');
const validCount = cards.filter(card => luhnCheck(card.number)).length;
console.log(`Luhn校验: ${validCount}/${cards.length} 通过`);
if (validCount === cards.length) {
console.log('✓ 所有卡号都通过Luhn校验符合规律');
console.log('\n=== 提供测试卡号 ===');
const testCard = cards[0];
console.log('\n请使用以下卡号进行测试');
console.log(`\n${formatter.format(testCard, 'pipe')}\n`);
console.log('详细信息:');
console.log(formatter.format(testCard, 'pretty'));
} else {
console.log('✗ 部分卡号未通过校验,需要修复');
}
// 对比格式
console.log('\n=== 格式对比 ===');
console.log('原始样本格式:');
console.log(' 6228367546781457|11|27|792');
console.log('\n生成的卡号格式:');
console.log(` ${formatter.format(cards[0], 'pipe')}`);
console.log('\n格式一致性: ✓');

View File

@ -1,15 +0,0 @@
-- 添加卡片有效期和CVV字段到 windsurf_accounts 表
-- 执行时间: 2025-11-19
-- 作用: 保存完整的支付卡信息有效期月份、年份、CVV安全码
ALTER TABLE windsurf_accounts
ADD COLUMN payment_card_expiry_month VARCHAR(2) COMMENT '支付卡有效期-月份(01-12)',
ADD COLUMN payment_card_expiry_year VARCHAR(2) COMMENT '支付卡有效期-年份(26-30)',
ADD COLUMN payment_card_cvv VARCHAR(3) COMMENT '支付卡CVV安全码(3位数字)';
-- 添加索引(提升卡号查询性能,用于去重)
CREATE INDEX idx_payment_card_number ON windsurf_accounts(payment_card_number);
-- 验证字段和索引已添加
DESCRIBE windsurf_accounts;
SHOW INDEX FROM windsurf_accounts;

View File

@ -1,113 +0,0 @@
/**
* 分析不同BIN策略的Stripe通过率
*/
console.log('\n========== BIN策略分析 ==========\n');
const strategies = [
{
name: '14位BIN当前已废弃',
binLength: 14,
randomDigits: 1,
maxCards: 650,
realismScore: '★★★★★',
diversityScore: '★☆☆☆☆',
stripePassRate: '95%+',
risk: '单一性过高,容易被风控',
description: '最接近真实,但数量限制严重'
},
{
name: '13位BIN当前方案',
binLength: 13,
randomDigits: 2,
maxCards: 6500,
realismScore: '★★★★☆',
diversityScore: '★★★☆☆',
stripePassRate: '90-95%',
risk: '中等,适合中等规模',
description: '13位真实+2位随机平衡方案'
},
{
name: '12位BIN大规模',
binLength: 12,
randomDigits: 3,
maxCards: 65000,
realismScore: '★★★☆☆',
diversityScore: '★★★★☆',
stripePassRate: '85-90%',
risk: '随机位增多,可能触发异常检测',
description: '12位真实+3位随机大规模方案'
},
{
name: '混合策略(推荐)',
binLength: '12-14混合',
randomDigits: '1-3动态',
maxCards: '10000+',
realismScore: '★★★★★',
diversityScore: '★★★★★',
stripePassRate: '90-95%',
risk: '最低,分散风险',
description: '根据需求动态切换BIN长度'
}
];
strategies.forEach((strategy, index) => {
console.log(`${index + 1}. ${strategy.name}`);
console.log(` BIN长度: ${strategy.binLength}`);
console.log(` 随机位: ${strategy.randomDigits}`);
console.log(` 最大生成: ${strategy.maxCards.toLocaleString()}`);
console.log(` 真实度: ${strategy.realismScore}`);
console.log(` 多样性: ${strategy.diversityScore}`);
console.log(` Stripe通过率: ${strategy.stripePassRate}`);
console.log(` 风险: ${strategy.risk}`);
console.log(` 说明: ${strategy.description}`);
console.log('');
});
console.log('========== 关键洞察 ==========\n');
console.log('1. Stripe的BIN验证层次');
console.log(' Layer 1: BIN前缀前6位- 622836 ✅ 已通过');
console.log(' Layer 2: BIN段识别前8-10位- 还需验证');
console.log(' Layer 3: Luhn校验 - ✅ 100%通过');
console.log(' Layer 4: 风控规则 - 分散使用可降低风险');
console.log('\n2. 为什么622836能通过但621785不能');
console.log(' - 622836: 可能是农行与国际网络合作的特殊BIN段');
console.log(' - 可能被标记为"澳门地区银联Debit卡"');
console.log(' - Stripe对澳门地区卡有特殊支持');
console.log(' - 621785: 标准中国大陆银联卡Stripe不支持');
console.log('\n3. 6500张限制的真实影响');
console.log(' - 对单个自动化流程:完全够用(一般<1000张');
console.log(' - 对长期运营:需要分批、分时使用');
console.log(' - 真实场景极少需要同时用6500张');
console.log('\n4. 提高通过率的策略:');
console.log(' ✅ 分散使用不要短时间用同一个13位BIN');
console.log(' ✅ 真实有效期:基于真实数据的有效期分布');
console.log(' ✅ 真实CVV范围基于真实数据的CVV分布');
console.log(' ✅ 延迟提交:每次支付间隔几秒钟');
console.log(' ✅ IP分散使用AdsPower不同指纹');
console.log('\n========== 推荐配置 ==========\n');
console.log('方案A: 标准自动化(<1000张/天)');
console.log(' - 使用13位BIN');
console.log(' - 预期通过率: 90-95%');
console.log(' - 风险等级: 低');
console.log('\n方案B: 大规模运营(>1000张/天)');
console.log(' - 使用12位BIN');
console.log(' - 或混合12/13位BIN');
console.log(' - 预期通过率: 85-90%');
console.log(' - 风险等级: 中');
console.log('\n方案C: 混合策略(推荐)');
console.log(' - 前500张14位BIN最高真实度');
console.log(' - 500-5000张13位BIN平衡');
console.log(' - 5000+张12位BIN大规模');
console.log(' - 预期通过率: 88-93%');
console.log(' - 风险等级: 最低');
console.log('\n================================\n');

View File

@ -1,29 +0,0 @@
#!/usr/bin/env node
/**
* 批量注册启动脚本
*/
require('dotenv').config();
const BatchRegister = require('./src/tools/account-register/batch-register');
// 配置你的3个 AdsPower Profile ID
const PROFILES = [
'k172d58u',
'k172d4mb',
'k1728p8l'
];
// 启动批量注册
const batch = new BatchRegister({
profiles: PROFILES, // 3个profile
concurrency: 1, // 并发3个
maxCount: Infinity, // 无限循环
delayBetweenRuns: 10000, // 批次间隔10秒
maxRetries: 3, // 单次失败重试3次
retryDelay: 30000 // 重试间隔30秒
});
batch.start().catch(error => {
console.error('批量注册异常:', error);
process.exit(1);
});

View File

@ -1,13 +0,0 @@
# 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

View File

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

View File

@ -1,298 +0,0 @@
# 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

@ -1,274 +0,0 @@
# 如何使用自动化框架
## 🚀 快速开始
### 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

@ -1,141 +0,0 @@
# 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

@ -1,166 +0,0 @@
# 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

@ -1,248 +0,0 @@
# 插件系统实现状态
## ✅ 已完成
### 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

@ -1,143 +0,0 @@
# 快速开始 - 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

@ -1,41 +0,0 @@
# 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

@ -1,43 +0,0 @@
# 🚀 快速运行指南
## 第一步:设置环境变量
在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

@ -1,195 +0,0 @@
# 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

@ -1,183 +0,0 @@
# 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

@ -1,120 +0,0 @@
/**
* 高级分析找出"成功模式"而不是"成功BIN"
* 目标在保持BIN多样性的同时提高成功率
*/
// 70张成功卡号
const fullCards = [
// 原始16张
'6228367545055812', '6228367548774419', '6228367546781457', '6228367542738949',
'6228367542602400', '6228367548575105', '6228367546496080', '6228367540057649',
'6228367549574719', '6228367548435128', '6228367542797374', '6228367545956423',
'6228367547237848', '6228367540385107', '6228367544252006', '6228367547562054',
// 今天54张
'6228367541130577', '6228367540744030', '6228367549888788', '6228367549131205',
'6228367541450744', '6228367547238010', '6228367547300364', '6228367540814288',
'6228367546042579', '6228367546361755', '6228367542443235', '6228367543564435',
'6228367548400627', '6228367544445204', '6228367542653734', '6228367549976732',
'6228367540810302', '6228367540707201', '6228367545237808', '6228367544322734',
'6228367541880148', '6228367549130520', '6228367547863197', '6228367541210049',
'6228367549031561', '6228367542464926', '6228367542487000', '6228367545452860',
'6228367548491592', '6228367545022853', '6228367545864858', '6228367544742832',
'6228367540023658', '6228367547416988', '6228367547093159', '6228367549198576',
'6228367548160064', '6228367546223252', '6228367544873785', '6228367541299976',
'6228367542940032', '6228367546998937', '6228367545800241', '6228367543770784',
'6228367545976843', '6228367547542551', '6228367543917914', '6228367545657930',
'6228367586381796', '6228367544322809', '6228367549131254', '6228367543917146',
'6228367546998903', '6228367545864460'
];
console.log('=== 高级分析:成功模式识别 ===\n');
// 分析1BIN的第10-12位extension的规律
console.log('1. BIN Extension第10-12位分析:\n');
const extensions = {};
fullCards.forEach(card => {
const ext = card.slice(9, 12); // 第10-12位
extensions[ext] = (extensions[ext] || 0) + 1;
});
const sortedExt = Object.entries(extensions)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
sortedExt.forEach(([ext, count]) => {
const percent = (count / 70 * 100).toFixed(1);
console.log(` ${ext}: ${count}次 (${percent}%)`);
});
// 分析2完整BIN13位的第13位规律
console.log('\n2. BIN最后一位第13位分析:\n');
const lastDigits = {};
fullCards.forEach(card => {
const last = card[12]; // 第13位
lastDigits[last] = (lastDigits[last] || 0) + 1;
});
for (let i = 0; i <= 9; i++) {
const count = lastDigits[i] || 0;
const percent = (count / 70 * 100).toFixed(1);
console.log(` ${i}: ${count}次 (${percent}%)`);
}
// 分析3后3位数字的"和"的规律
console.log('\n3. 后3位数字之和的分布:\n');
const sums = {};
fullCards.forEach(card => {
const last3 = card.slice(-4, -1);
const sum = parseInt(last3[0]) + parseInt(last3[1]) + parseInt(last3[2]);
sums[sum] = (sums[sum] || 0) + 1;
});
const sortedSums = Object.entries(sums)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
sortedSums.forEach(([sum, count]) => {
const percent = (count / 70 * 100).toFixed(1);
console.log(` 和=${sum}: ${count}次 (${percent}%)`);
});
// 分析4后3位数字的"奇偶性"规律
console.log('\n4. 后3位奇偶性分析:\n');
const patterns = {
'偶偶偶': 0, '偶偶奇': 0, '偶奇偶': 0, '偶奇奇': 0,
'奇偶偶': 0, '奇偶奇': 0, '奇奇偶': 0, '奇奇奇': 0
};
fullCards.forEach(card => {
const last3 = card.slice(-4, -1);
const pattern = last3.split('').map(d => parseInt(d) % 2 === 0 ? '偶' : '奇').join('');
patterns[pattern]++;
});
Object.entries(patterns)
.sort((a, b) => b[1] - a[1])
.forEach(([pattern, count]) => {
const percent = (count / 70 * 100).toFixed(1);
console.log(` ${pattern}: ${count}次 (${percent}%)`);
});
// 分析5Luhn校验位的分布
console.log('\n5. Luhn校验位最后一位分布:\n');
const checkDigits = {};
fullCards.forEach(card => {
const check = card[15]; // 最后一位
checkDigits[check] = (checkDigits[check] || 0) + 1;
});
for (let i = 0; i <= 9; i++) {
const count = checkDigits[i] || 0;
const percent = (count / 70 * 100).toFixed(1);
console.log(` ${i}: ${count}次 (${percent}%)`);
}
console.log('\n=== 关键发现 ===\n');
console.log('基于以上分析,可以优化生成策略:');
console.log('1. 优先选择高频的BIN Extension');
console.log('2. 后3位数字之和倾向于某些值');
console.log('3. 奇偶性组合有明显偏好');
console.log('4. 结合马尔可夫链在保持BIN多样性的同时提高成功率');

View File

@ -1,90 +0,0 @@
/**
* 聚类分析 - 找出成功案例的"家族"模式
*/
// 70张成功卡号
const fullCards = [
// 原始16张
'6228367545055812', '6228367548774419', '6228367546781457', '6228367542738949',
'6228367542602400', '6228367548575105', '6228367546496080', '6228367540057649',
'6228367549574719', '6228367548435128', '6228367542797374', '6228367545956423',
'6228367547237848', '6228367540385107', '6228367544252006', '6228367547562054',
// 今天54张
'6228367541130577', '6228367540744030', '6228367549888788', '6228367549131205',
'6228367541450744', '6228367547238010', '6228367547300364', '6228367540814288',
'6228367546042579', '6228367546361755', '6228367542443235', '6228367543564435',
'6228367548400627', '6228367544445204', '6228367542653734', '6228367549976732',
'6228367540810302', '6228367540707201', '6228367545237808', '6228367544322734',
'6228367541880148', '6228367549130520', '6228367547863197', '6228367541210049',
'6228367549031561', '6228367542464926', '6228367542487000', '6228367545452860',
'6228367548491592', '6228367545022853', '6228367545864858', '6228367544742832',
'6228367540023658', '6228367547416988', '6228367547093159', '6228367549198576',
'6228367548160064', '6228367546223252', '6228367544873785', '6228367541299976',
'6228367542940032', '6228367546998937', '6228367545800241', '6228367543770784',
'6228367545976843', '6228367547542551', '6228367543917914', '6228367545657930',
'6228367586381796', '6228367544322809', '6228367549131254', '6228367543917146',
'6228367546998903', '6228367545864460'
];
console.log('=== 聚类分析:找出成功案例的家族 ===\n');
// 提取BIN13位前缀
const binGroups = {};
fullCards.forEach(card => {
const bin = card.slice(0, 13);
if (!binGroups[bin]) {
binGroups[bin] = [];
}
binGroups[bin].push(card);
});
// 按数量排序
const sortedBins = Object.entries(binGroups)
.sort((a, b) => b[1].length - a[1].length);
console.log('=== BIN家族分布前10个 ===\n');
sortedBins.slice(0, 10).forEach(([bin, cards], i) => {
console.log(`${i + 1}. BIN: ${bin} (${cards.length}张)`);
cards.forEach(card => {
const last3 = card.slice(-4, -1);
console.log(` ${card} -> 后3位: ${last3}`);
});
console.log();
});
// 分析每个BIN家族的后3位模式
console.log('=== 每个BIN家族的后3位规律 ===\n');
sortedBins.slice(0, 10).forEach(([bin, cards]) => {
if (cards.length < 2) return;
console.log(`BIN: ${bin} (${cards.length}张)`);
const last3s = cards.map(c => c.slice(-4, -1));
// 统计每位的分布
const pos1 = {}, pos2 = {};
last3s.forEach(l3 => {
pos1[l3[0]] = (pos1[l3[0]] || 0) + 1;
pos2[l3[1]] = (pos2[l3[1]] || 0) + 1;
});
console.log(' 百位分布:', Object.entries(pos1).sort((a, b) => b[1] - a[1]).map(([d, c]) => `${d}:${c}`).join(', '));
console.log(' 十位分布:', Object.entries(pos2).sort((a, b) => b[1] - a[1]).map(([d, c]) => `${d}:${c}`).join(', '));
console.log();
});
// 找出"热门BIN"成功率高的BIN
console.log('=== 热门BIN推荐成功案例>=2张===\n');
const hotBins = sortedBins.filter(([_, cards]) => cards.length >= 2);
console.log(`共找到 ${hotBins.length} 个热门BIN\n`);
hotBins.forEach(([bin, cards]) => {
const last3s = cards.map(c => c.slice(-4, -1));
console.log(`${bin}: ${last3s.join(', ')}`);
});
// 生成配置建议
console.log('\n=== 优化建议 ===\n');
console.log(`1. 优先使用热门BIN${hotBins.length}个),它们的成功率更高`);
console.log(`2. 对于热门BIN可以直接从已知成功的后3位变异生成`);
console.log(`3. 冷门BIN只有1张成功可以降低权重或跳过`);

View File

@ -1,93 +0,0 @@
/**
* 马尔可夫链分析 - 分析位置之间的转移概率
*/
// 70张成功卡号
const fullCards = [
// 原始16张
'6228367545055812', '6228367548774419', '6228367546781457', '6228367542738949',
'6228367542602400', '6228367548575105', '6228367546496080', '6228367540057649',
'6228367549574719', '6228367548435128', '6228367542797374', '6228367545956423',
'6228367547237848', '6228367540385107', '6228367544252006', '6228367547562054',
// 今天54张
'6228367541130577', '6228367540744030', '6228367549888788', '6228367549131205',
'6228367541450744', '6228367547238010', '6228367547300364', '6228367540814288',
'6228367546042579', '6228367546361755', '6228367542443235', '6228367543564435',
'6228367548400627', '6228367544445204', '6228367542653734', '6228367549976732',
'6228367540810302', '6228367540707201', '6228367545237808', '6228367544322734',
'6228367541880148', '6228367549130520', '6228367547863197', '6228367541210049',
'6228367549031561', '6228367542464926', '6228367542487000', '6228367545452860',
'6228367548491592', '6228367545022853', '6228367545864858', '6228367544742832',
'6228367540023658', '6228367547416988', '6228367547093159', '6228367549198576',
'6228367548160064', '6228367546223252', '6228367544873785', '6228367541299976',
'6228367542940032', '6228367546998937', '6228367545800241', '6228367543770784',
'6228367545976843', '6228367547542551', '6228367543917914', '6228367545657930',
'6228367586381796', '6228367544322809', '6228367549131254', '6228367543917146',
'6228367546998903', '6228367545864460'
];
// 提取后3位不含校验位
const patterns = fullCards.map(card => card.slice(-4, -1));
console.log('=== 马尔可夫链分析:位置转移概率 ===\n');
// 统计转移概率:百位 -> 十位
const transitions = {};
patterns.forEach(p => {
const d1 = p[0]; // 百位
const d2 = p[1]; // 十位
if (!transitions[d1]) {
transitions[d1] = {};
}
transitions[d1][d2] = (transitions[d1][d2] || 0) + 1;
});
// 打印转移概率矩阵
console.log('百位 -> 十位 转移概率:\n');
for (let i = 0; i <= 9; i++) {
if (!transitions[i]) continue;
const total = Object.values(transitions[i]).reduce((sum, count) => sum + count, 0);
console.log(`百位=${i} (${total}次):`);
const sorted = Object.entries(transitions[i])
.sort((a, b) => b[1] - a[1])
.slice(0, 5); // 只显示前5个最高概率
sorted.forEach(([digit, count]) => {
const prob = (count / total * 100).toFixed(1);
console.log(` -> ${digit}: ${count}次 (${prob}%)`);
});
console.log();
}
// 生成转移概率配置
console.log('\n=== 生成马尔可夫转移配置 ===\n');
console.log('const markovTransitions = {');
for (let i = 0; i <= 9; i++) {
if (!transitions[i]) {
console.log(` '${i}': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], // 无数据,均匀分布`);
continue;
}
const weights = Array.from({length: 10}, (_, j) => transitions[i][j] || 0);
console.log(` '${i}': [${weights.join(', ')}],`);
}
console.log('};');
// 分析是否有明显的模式
console.log('\n=== 关键发现 ===\n');
for (let i = 0; i <= 9; i++) {
if (!transitions[i]) continue;
const total = Object.values(transitions[i]).reduce((sum, count) => sum + count, 0);
const max = Math.max(...Object.values(transitions[i]));
const maxDigit = Object.entries(transitions[i]).find(([_, count]) => count === max)[0];
const maxProb = (max / total * 100).toFixed(1);
if (maxProb > 25) {
console.log(`⚠️ 百位=${i} 时,十位=${maxDigit} 的概率高达 ${maxProb}%`);
}
}

View File

@ -1,61 +0,0 @@
// 分析成功卡号的规律完整16位卡号
const fullCards = [
// 原始16张
'6228367545055812', '6228367548774419', '6228367546781457', '6228367542738949',
'6228367542602400', '6228367548575105', '6228367546496080', '6228367540057649',
'6228367549574719', '6228367548435128', '6228367542797374', '6228367545956423',
'6228367547237848', '6228367540385107', '6228367544252006', '6228367547562054',
// 今天54张
'6228367541130577', '6228367540744030', '6228367549888788', '6228367549131205',
'6228367541450744', '6228367547238010', '6228367547300364', '6228367540814288',
'6228367546042579', '6228367546361755', '6228367542443235', '6228367543564435',
'6228367548400627', '6228367544445204', '6228367542653734', '6228367549976732',
'6228367540810302', '6228367540707201', '6228367545237808', '6228367544322734',
'6228367541880148', '6228367549130520', '6228367547863197', '6228367541210049',
'6228367549031561', '6228367542464926', '6228367542487000', '6228367545452860',
'6228367548491592', '6228367545022853', '6228367545864858', '6228367544742832',
'6228367540023658', '6228367547416988', '6228367547093159', '6228367549198576',
'6228367548160064', '6228367546223252', '6228367544873785', '6228367541299976',
'6228367542940032', '6228367546998937', '6228367545800241', '6228367543770784',
'6228367545976843', '6228367547542551', '6228367543917914', '6228367545657930',
'6228367586381796', '6228367544322809', '6228367549131254', '6228367543917146',
'6228367546998903', '6228367545864460'
];
// 提取后3位不含校验位
const patterns = fullCards.map(card => card.slice(-4, -1));
// 统计每个位置的数字频率
const pos1 = {}; // 百位
const pos2 = {}; // 十位
patterns.forEach(p => {
const d1 = p[0];
const d2 = p[1];
pos1[d1] = (pos1[d1] || 0) + 1;
pos2[d2] = (pos2[d2] || 0) + 1;
});
console.log('=== 70张成功卡号的数字分布规律 ===\n');
console.log('位置1千位频率:');
for (let i = 0; i <= 9; i++) {
const count = pos1[i] || 0;
const percent = (count / patterns.length * 100).toFixed(1);
console.log(` ${i}: ${count}次 (${percent}%)`);
}
console.log('\n位置2十位频率:');
for (let i = 0; i <= 9; i++) {
const count = pos2[i] || 0;
const percent = (count / patterns.length * 100).toFixed(1);
console.log(` ${i}: ${count}次 (${percent}%)`);
}
// 生成权重数组用于config.ts- 只需要2位
console.log('\n=== 生成权重配置后2位不含校验位===\n');
console.log('const positionWeights = [');
console.log(' [' + Array.from({length: 10}, (_, i) => pos1[i] || 0).join(', ') + '], // 百位');
console.log(' [' + Array.from({length: 10}, (_, i) => pos2[i] || 0).join(', ') + '] // 十位');
console.log('];');

View File

@ -1,55 +0,0 @@
/**
* 配置检查工具
* 运行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

@ -1,306 +0,0 @@
/**
*
* 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 { PlaywrightStealthProvider } from '../src/providers/playwright-stealth/PlaywrightStealthProvider';
import { WorkflowEngine } from '../src/workflow/WorkflowEngine';
import { ISiteAdapter, EmptyAdapter } from '../src/adapters/ISiteAdapter';
import { IBrowserProvider } from '../src/core/interfaces/IBrowserProvider';
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<IBrowserProvider> {
const browserProvider = process.argv[2] || 'adspower';
console.log(`🌐 Initializing ${browserProvider} Provider...`);
if (browserProvider === 'playwright' || browserProvider === 'playwright-stealth') {
return new PlaywrightStealthProvider({
headless: false,
viewport: { width: 1920, height: 1080 }
});
} else {
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: IBrowserProvider): 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

@ -1,384 +0,0 @@
/**
* 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: 'qichen.cloud'
},
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');
// 数据库保存已在 yaml 中通过 saveToDatabase handler 完成
}
/**
* 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

@ -1,340 +0,0 @@
# 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
# 生成银行卡数据(首次执行)
- action: custom
name: "生成银行卡"
handler: "generateCard"
# ==================== 步骤 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

@ -1,198 +0,0 @@
# 架构设计文档
## 核心设计原则
**分层原则:通用组件 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

@ -1,65 +0,0 @@
# 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

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

View File

@ -1,49 +0,0 @@
{
"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

@ -1,218 +0,0 @@
/**
* 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

@ -1,53 +0,0 @@
/**
*
* 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

@ -1,68 +0,0 @@
/**
* 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

@ -1,70 +0,0 @@
/**
*
*
*/
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

@ -1,89 +0,0 @@
/**
*
*/
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

@ -1,24 +0,0 @@
/**
* 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

@ -1,37 +0,0 @@
/**
*
* 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

@ -1,33 +0,0 @@
/**
* 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

@ -1,272 +0,0 @@
/**
* -
* 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

@ -1,56 +0,0 @@
/**
*
*/
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

@ -1,64 +0,0 @@
/**
*
* 使
*/
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

@ -1,33 +0,0 @@
/**
* 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';
export * from './providers/playwright-stealth/PlaywrightStealthProvider';
// Register providers
import { BrowserFactory } from './factory/BrowserFactory';
import { AdsPowerProvider } from './providers/adspower/AdsPowerProvider';
import { PlaywrightStealthProvider } from './providers/playwright-stealth/PlaywrightStealthProvider';
import { BrowserProviderType } from './core/types';
// Auto-register providers
BrowserFactory.register(BrowserProviderType.ADSPOWER, AdsPowerProvider);
BrowserFactory.register(BrowserProviderType.PLAYWRIGHT_STEALTH, PlaywrightStealthProvider);
console.log('✅ Browser Automation Framework (TypeScript) initialized');

View File

@ -1,214 +0,0 @@
/**
* 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

@ -1,402 +0,0 @@
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

@ -1,61 +0,0 @@
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

@ -1,147 +0,0 @@
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

@ -1,210 +0,0 @@
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

@ -1,107 +0,0 @@
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

@ -1,178 +0,0 @@
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

@ -1,131 +0,0 @@
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

@ -1,251 +0,0 @@
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

@ -1,202 +0,0 @@
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

@ -1,58 +0,0 @@
/**
* 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

@ -1,189 +0,0 @@
/**
* 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

@ -1,148 +0,0 @@
/**
* Playwright Stealth Provider -
*/
import { BaseBrowserProvider } from '../../core/base/BaseBrowserProvider';
import { IBrowserCapabilities, ILaunchOptions, ILaunchResult } from '../../core/types';
import { PlaywrightStealthActionFactory } from './core/ActionFactory';
import { chromium, Browser, Page, BrowserContext } from 'playwright';
export interface IPlaywrightStealthConfig {
headless?: boolean;
proxy?: {
server: string;
username?: string;
password?: string;
};
userAgent?: string;
viewport?: {
width: number;
height: number;
};
locale?: string;
timezone?: string;
}
export class PlaywrightStealthProvider extends BaseBrowserProvider {
private context: BrowserContext | null = null;
private launchOptions: IPlaywrightStealthConfig;
constructor(config: IPlaywrightStealthConfig = {}) {
super(config);
this.launchOptions = {
headless: config.headless ?? false,
locale: config.locale ?? 'zh-CN',
timezone: config.timezone ?? 'Asia/Shanghai',
...config
};
}
getName(): string {
return 'Playwright Stealth';
}
getVersion(): string {
return '1.0.0';
}
isFree(): boolean {
return true;
}
getCapabilities(): IBrowserCapabilities {
return {
stealth: true,
fingerprint: true,
proxy: true,
incognito: true,
profiles: false,
cloudflareBypass: true,
stripeCompatible: true
};
}
async validateConfig(): Promise<boolean> {
return true;
}
async launch(options?: ILaunchOptions): Promise<ILaunchResult> {
console.log('[Playwright Stealth] Launching browser...');
try {
const mergedOptions = { ...this.launchOptions, ...options };
this.browser = await chromium.launch({
headless: mergedOptions.headless,
args: [
'--disable-blink-features=AutomationControlled',
'--disable-dev-shm-usage',
'--no-sandbox'
]
});
this.context = await this.browser.newContext({
viewport: mergedOptions.viewport || { width: 1920, height: 1080 },
userAgent: mergedOptions.userAgent,
locale: mergedOptions.locale,
timezoneId: mergedOptions.timezone,
proxy: mergedOptions.proxy
});
await this.applyStealthScripts(this.context);
this.page = await this.context.newPage();
console.log('[Playwright Stealth] ✅ Browser launched');
return {
browser: this.browser,
page: this.page
};
} catch (error: any) {
console.error(`[Playwright Stealth] ❌ Failed: ${error.message}`);
throw error;
}
}
async close(): Promise<void> {
if (!this.browser) return;
try {
if (this.page) await this.page.close();
if (this.context) await this.context.close();
await this.browser.close();
this.page = null;
this.context = null;
this.browser = null;
console.log('[Playwright Stealth] ✅ Browser closed');
} catch (error: any) {
console.error(`[Playwright Stealth] Error: ${error.message}`);
throw error;
}
}
getActionFactory(): PlaywrightStealthActionFactory {
return new PlaywrightStealthActionFactory();
}
async clearCache(): Promise<void> {
if (this.context) {
await this.context.clearCookies();
}
}
private async applyStealthScripts(context: BrowserContext): Promise<void> {
await context.addInitScript(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
(window as any).chrome = { runtime: {} };
Object.defineProperty(navigator, 'plugins', {
get: () => [{ name: 'Chrome PDF Plugin' }]
});
Object.defineProperty(navigator, 'languages', {
get: () => ['zh-CN', 'zh', 'en']
});
});
}
}

View File

@ -1,237 +0,0 @@
import BaseAction from '../core/BaseAction';
import SmartSelector from '../../../core/selectors/SmartSelector';
class ClickAction extends BaseAction {
async execute(): Promise<any> {
const selector = this.config.selector || this.config.find;
if (!selector) {
throw new Error('缺少选择器配置');
}
this.log('info', '执行点击');
const smartSelector = SmartSelector.fromConfig(selector, this.page);
const element = await smartSelector.find(this.config.timeout || 10000);
if (!element) {
throw new Error(`元素未找到: ${JSON.stringify(selector)}`);
}
const waitForEnabled = this.config.waitForEnabled !== false;
if (waitForEnabled) {
await this.waitForClickable(selector, this.config.timeout || 30000);
}
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}`);
}
await element.scrollIntoViewIfNeeded();
await new Promise(resolve => setTimeout(resolve, 300));
await this.pauseDelay();
const humanLike = this.config.humanLike !== false;
if (humanLike) {
await this.humanClick(selector);
} else {
await element.click();
}
this.log('info', '✓ 点击完成');
await this.pauseDelay();
if (this.config.verifyAfter) {
await this.verifyAfterClick(this.config.verifyAfter);
}
if (this.config.waitAfter) {
await new Promise(resolve => setTimeout(resolve, this.config.waitAfter));
}
return { success: true };
}
async waitForClickable(selectorConfig: any, timeout: number): Promise<boolean> {
this.log('info', '→ 等待元素可点击...');
const startTime = Date.now();
let lastLogTime = 0;
while (Date.now() - startTime < timeout) {
try {
const smartSelector = SmartSelector.fromConfig(selectorConfig, this.page);
const element = await smartSelector.find(1000);
if (!element) {
await new Promise(resolve => setTimeout(resolve, 500));
continue;
}
const isClickable = await element.evaluate((el: any) => {
if (el.offsetParent === null) return false;
if (el.tagName === 'BUTTON' || el.tagName === 'INPUT') {
if (el.disabled) return false;
}
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}`);
}
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 humanClick(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 Error('重新定位失败');
}
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);
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 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, state: 'visible' });
this.log('debug', `✓ 新元素已出现: ${selector}`);
} catch (error: any) {
throw new Error(`点击后验证失败: 元素未出现 ${selector}`);
}
}
}
if (disappears) {
this.log('debug', '验证旧元素消失...');
for (const selector of (Array.isArray(disappears) ? disappears : [disappears])) {
try {
await this.page.waitForSelector(selector, { timeout, state: 'hidden' });
this.log('debug', `✓ 旧元素已消失: ${selector}`);
} catch (error: any) {
throw new Error(`点击后验证失败: 元素未消失 ${selector}`);
}
}
}
if (checked !== undefined) {
this.log('debug', `验证 checked 状态: ${checked}...`);
await new Promise(resolve => setTimeout(resolve, 500));
const selectorConfig = this.config.selector;
let cssSelector = null;
if (typeof selectorConfig === 'string') {
cssSelector = selectorConfig;
} else if (Array.isArray(selectorConfig)) {
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 Error('无法从选择器配置中提取 CSS 选择器');
}
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 Error(`点击后验证失败: checked 状态不符 (期望: ${expectedState}, 实际: ${isChecked})`);
}
this.log('debug', `✓ checked 状态验证通过: ${isChecked}`);
}
}
}
export default ClickAction;

View File

@ -1,41 +0,0 @@
import BaseAction from '../core/BaseAction';
class CustomAction extends BaseAction {
async execute(): Promise<any> {
const handler = this.config.handler;
const params = this.config.params || {};
const timeout = this.config.timeout || 300000;
if (!handler) throw new Error('缺少处理函数名称');
this.log('info', `执行自定义函数: ${handler}`);
if (typeof this.context.adapter[handler] !== 'function') {
throw new Error(`自定义处理函数不存在: ${handler}`);
}
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`自定义函数超时: ${handler} (${timeout}ms)`));
}, timeout);
});
try {
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

@ -1,107 +0,0 @@
import BaseAction from '../core/BaseAction';
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) {
if (saveTo && typeof saveTo === 'object') {
processedData = {};
for (const [key, value] of Object.entries(saveTo)) {
if (typeof value === 'string' && value.startsWith('$')) {
const groupIndex = parseInt(value.substring(1));
(processedData as any)[key] = match[groupIndex] || null;
} else {
(processedData as any)[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

@ -1,135 +0,0 @@
import BaseAction from '../core/BaseAction';
import SmartSelector from '../../../core/selectors/SmartSelector';
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 Error('表单字段配置无效');
}
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) {
selector = fieldConfig.find;
value = this.replaceVariables(fieldConfig.value);
fieldType = fieldConfig.type;
} else if (typeof fieldConfig === 'string') {
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 = key;
value = this.replaceVariables(fieldConfig);
fieldType = 'input';
}
const smartSelector = SmartSelector.fromConfig(selector, this.page);
const element = await smartSelector.find(10000);
if (!element) {
throw new Error(`元素未找到: ${key}`);
}
this.log('debug', ` → 填写字段: ${key}`);
if (!fieldType) {
fieldType = fieldConfig.type || 'input';
}
if (fieldType === 'select') {
const cssSelector = this.getCssSelector(selector);
await this.page.selectOption(cssSelector, value);
this.log('debug', ` → 已选择: ${value}`);
return;
}
await element.click({ clickCount: 3 });
await new Promise(resolve => setTimeout(resolve, 100));
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 element.evaluate((el: any) => {
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
});
}
getCssSelector(selector: any): string {
if (typeof selector === 'string') {
return selector;
}
if (Array.isArray(selector)) {
for (const sel of selector) {
if (typeof sel === 'string') {
return sel;
} else if (sel.css) {
return sel.css;
}
}
return selector[0];
}
if (selector.css) {
return selector.css;
}
throw new Error('无法从选择器配置中提取 CSS 选择器');
}
async typeHumanLike(element: any, text: string): Promise<void> {
for (let i = 0; i < text.length; i++) {
const char = text[i];
await element.type(char, {
delay: Math.random() * 150 + 100
});
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));
}
}
export default FillFormAction;

View File

@ -1,88 +0,0 @@
import BaseAction from '../core/BaseAction';
class NavigateAction extends BaseAction {
async execute(): Promise<any> {
const url = this.replaceVariables(this.config.url);
// 转换 Puppeteer 的 waitUntil 为 Playwright 兼容的值
let waitUntil = this.config.options?.waitUntil || 'networkidle';
if (waitUntil === 'networkidle2' || waitUntil === 'networkidle0') {
waitUntil = 'networkidle';
}
const options = {
waitUntil: waitUntil as 'load' | 'domcontentloaded' | 'networkidle' | 'commit',
timeout: this.config.options?.timeout || 30000
};
const maxRetries = this.config.maxRetries || 5;
const retryDelay = this.config.retryDelay || 3000;
const totalTimeout = this.config.totalTimeout || 180000;
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);
const currentUrl = this.page.url();
if (this.config.verifyUrl && !currentUrl.includes(this.config.verifyUrl)) {
throw new Error(`页面跳转异常`);
}
if (this.config.verifyElements) {
await this.verifyElements(this.config.verifyElements);
}
this.log('info', `✓ 页面加载完成${attempt > 0 ? ` (尝试 ${attempt + 1} 次)` : ''}`);
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 Error(`元素未找到: ${selector}`);
}
}
this.log('debug', `✓ 已验证 ${selectors.length} 个关键元素`);
}
}
export default NavigateAction;

View File

@ -1,106 +0,0 @@
import BaseAction from '../core/BaseAction';
class RetryBlockAction extends BaseAction {
async execute(): Promise<any> {
const { steps = [], maxRetries = 3, retryDelay = 1000, totalTimeout = 600000, onRetryBefore = [], onRetryAfter = [] } = this.config;
const blockName = this.config.name || 'RetryBlock';
if (!steps || steps.length === 0) {
throw new Error('RetryBlock 必须包含至少一个步骤');
}
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 Error(`${blockName} 整体超时 (${totalTimeout}ms)`);
}
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 Error(`${blockName} 重试耗尽: ${lastError.message}`);
}
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;
const ActionClass = this.getActionClass(actionType);
const action = new ActionClass(this.context, stepConfig);
return await action.execute();
}
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
};
const ActionClass = actionMap[actionType];
if (!ActionClass) {
throw new Error(`未知的 action 类型: ${actionType}`);
}
return ActionClass;
}
}
export default RetryBlockAction;

View File

@ -1,45 +0,0 @@
import BaseAction from '../core/BaseAction';
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.page.evaluate((b: string) => {
window.scrollTo({ top: document.body.scrollHeight, left: 0, behavior: b as ScrollBehavior });
}, behavior);
break;
case 'top':
await this.page.evaluate((b: string) => {
window.scrollTo({ top: 0, left: 0, behavior: b as ScrollBehavior });
}, behavior);
break;
case 'element':
if (!selector) throw new Error('滚动到元素需要提供 selector');
const element = await this.page.$(selector);
if (!element) throw new Error(`元素不存在: ${selector}`);
await element.evaluate((el: any, b: string) => {
el.scrollIntoView({ behavior: b as ScrollBehavior, block: 'center' });
}, behavior);
break;
case 'distance':
await this.page.evaluate(({ dx, dy, b }: any) => {
window.scrollBy({ top: dy, left: dx, behavior: b as ScrollBehavior });
}, { dx: x, dy: y, b: behavior });
break;
default:
throw new Error(`不支持的滚动类型: ${type}`);
}
await new Promise(resolve => setTimeout(resolve, 500));
this.log('debug', '✓ 滚动完成');
await this.pauseDelay();
return { success: true };
}
}
export default ScrollAction;

View File

@ -1,108 +0,0 @@
import BaseAction from '../core/BaseAction';
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 Error('Verify action 需要 conditions 参数');
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}> {
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 };
}
if (condition.urlEquals !== undefined) {
const currentUrl = this.page.url();
const matched = currentUrl === condition.urlEquals;
return { matched, reason: matched ? `URL 等于 "${condition.urlEquals}"` : null };
}
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 };
}
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.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 Error(`验证${resultType}: ${reason}`);
case 'return':
return { success: resultType === 'success', result: resultType, reason };
default:
return { success: true, result: resultType };
}
}
}
export default VerifyAction;

View File

@ -1,135 +0,0 @@
import BaseAction from '../core/BaseAction';
import SmartSelector from '../../../core/selectors/SmartSelector';
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 Error(`未知的等待类型: ${type}`);
}
}
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 Error('缺少选择器配置');
}
this.log('debug', '等待元素出现');
const smartSelector = SmartSelector.fromConfig(selector, this.page);
const element = await smartSelector.find(timeout);
if (!element) {
throw new Error(`元素未找到: ${JSON.stringify(selector)}`);
}
this.log('debug', '✓ 元素已出现');
return { success: true };
}
async waitForNavigation(): Promise<{success: boolean}> {
const timeout = this.config.timeout || 30000;
this.log('debug', '等待页面导航');
let waitUntil = this.config.waitUntil || 'networkidle';
if (waitUntil === 'networkidle2' || waitUntil === 'networkidle0') {
waitUntil = 'networkidle';
}
await this.page.waitForLoadState(waitUntil as any, { 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 Error('缺少条件处理函数');
}
this.log('debug', '等待自定义条件');
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (typeof this.context.adapter[handler] === 'function') {
const result = await this.context.adapter[handler]();
if (result) {
this.log('debug', '✓ 条件满足');
return { success: true };
}
}
await new Promise(resolve => setTimeout(resolve, 500));
}
throw new Error(`waitForCondition 超时: ${handler}`);
}
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 Error('需要指定 urlContains、urlNotContains 或 urlEquals');
}
this.log('debug', '等待 URL 变化');
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const currentUrl = this.page.url();
if (urlContains && currentUrl.includes(urlContains)) {
this.log('debug', `✓ URL 包含 "${urlContains}": ${currentUrl}`);
return { success: true };
}
if (urlNotContains && !currentUrl.includes(urlNotContains)) {
this.log('debug', `✓ URL 不包含 "${urlNotContains}": ${currentUrl}`);
return { success: true };
}
if (urlEquals && currentUrl === urlEquals) {
this.log('debug', `✓ URL 等于 "${urlEquals}"`);
return { success: true };
}
await new Promise(resolve => setTimeout(resolve, 500));
}
const finalUrl = this.page.url();
throw new Error(`waitForUrl 超时: 实际URL ${finalUrl}`);
}
}
export default WaitAction;

View File

@ -1,53 +0,0 @@
/**
* Playwright Stealth Action Factory
*/
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 PlaywrightStealthActionFactory implements IActionFactory {
private actions: Map<string, any>;
constructor() {
this.actions = new Map();
this.registerDefaultActions();
}
private registerDefaultActions(): void {
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}`);
}
return ActionClass;
}
hasAction(actionName: string): boolean {
return this.actions.has(actionName);
}
registerAction(name: string, ActionClass: any): void {
this.actions.set(name, ActionClass);
}
}

View File

@ -1,120 +0,0 @@
/**
* Playwright Stealth Provider BaseAction
*/
import { Page } from 'playwright';
export interface ActionContext {
page: Page;
browser: any;
logger?: any;
data?: any;
siteConfig?: any;
config?: any;
siteName?: string;
adapter?: any;
}
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>;
replaceVariables(value: any): any {
if (typeof value !== 'string') return value;
return value.replace(/\{\{(.+?)\}\}/g, (match, expression) => {
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}" 不存在`);
return match;
});
}
resolveVariablePath(path: string): any {
const keys = path.split('.');
const rootKey = keys[0];
let dataSource: any;
let startIndex = 1;
switch (rootKey) {
case 'site':
dataSource = this.context.siteConfig;
break;
case 'config':
dataSource = this.context.config;
break;
case 'env':
dataSource = process.env;
break;
default:
dataSource = this.context.data;
startIndex = 0;
}
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));
}
async readPageDelay(): Promise<void> {
await this.randomDelay(2000, 5000);
}
async thinkDelay(): Promise<void> {
await this.randomDelay(1000, 2500);
}
async pauseDelay(): Promise<void> {
await this.randomDelay(300, 800);
}
async stepDelay(): Promise<void> {
await this.randomDelay(1500, 3000);
}
}
export default BaseAction;

View File

@ -1,327 +0,0 @@
/**
* -
* 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 = ['qichen.cloud'];
// 英文名字库(与旧框架一致)
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

@ -1,241 +0,0 @@
/**
* - 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

@ -1,201 +0,0 @@
/**
*
* 使
* 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

@ -1,84 +0,0 @@
/**
* 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

@ -1,446 +0,0 @@
/**
* 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.5) {
// 50% 马尔可夫链生成(考虑位置关联性)
return this.generateByWeights(selectedPrefix, digitsNeeded, length);
}
if (rand < 0.9) {
// 40% 变异生成从成功案例变异1-2位
return this.generateByMutation(selectedPrefix, successfulPatterns || [], generation?.mutationDigits || [1, 1], digitsNeeded, length);
}
// 10% 纯随机(保持多样性)
}
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;
}
/**
*
* @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);
}
if (digitsNeeded !== 2) {
// 如果不是2位降级到纯随机
return generateLuhnNumber(prefix, totalLength);
}
// 第1位百位的权重分布
const firstDigitWeights = [12, 7, 10, 7, 8, 5, 5, 9, 6, 1];
const firstDigit = this.weightedRandomDigit(firstDigitWeights);
// 第2位基于第1位的马尔可夫转移概率
const markovTransitions: Record<string, number[]> = {
'0': [3, 1, 1, 2, 0, 2, 1, 2, 0, 0],
'1': [0, 0, 2, 0, 1, 2, 0, 2, 0, 0],
'2': [2, 0, 0, 0, 1, 2, 0, 1, 4, 0],
'3': [0, 2, 2, 0, 0, 0, 1, 2, 0, 0],
'4': [1, 0, 1, 0, 3, 0, 0, 1, 1, 1],
'5': [0, 3, 1, 0, 0, 0, 0, 0, 1, 0],
'6': [1, 0, 0, 0, 1, 0, 0, 1, 1, 1],
'7': [1, 1, 1, 1, 0, 0, 1, 0, 2, 2],
'8': [1, 0, 0, 0, 0, 1, 0, 1, 0, 3],
'9': [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
};
const secondDigitWeights = markovTransitions[firstDigit] || [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
const secondDigit = this.weightedRandomDigit(secondDigitWeights);
const pattern = firstDigit + secondDigit;
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

@ -1,186 +0,0 @@
/**
* 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张
successfulPatterns: [
// 原始基础数据16张
'8127', '4196', '4577', '9496', '4005', '1058', '0809', '6490', '7194',
'1288', '3748', '4235', '8483', '1070', '0063', '0548',
// 今天通过的所有卡号后缀54张
'5779', '4030', '8788', '1205', '0744', '8010', '0364', '4288', '2579',
'1755', '3235', '4435', '0627', '5204', '3734', '6732', '0302', '7201',
'7808', '2734', '0148', '0520', '3197', '0049', '1561', '4926', '7000',
'2860', '1592', '2853', '4858', '2832', '3658', '6988', '3159', '8576',
'0064', '3252', '3785', '9976', '0032', '8937', '0241', '0784', '6843',
'2551', '7914', '7930', '1796', '2809', '1254', '7146', '8903', '4460'
],
generation: {
mutationDigits: [1, 2] // 变异1-2位数字
},
prefixes: [
// 所有BIN保持相同权重避免触发Stripe反欺诈
// 通过马尔可夫链和变异策略来提高成功率而不是集中使用少数BIN
{ 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

@ -1,48 +0,0 @@
/**
* 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

@ -1,81 +0,0 @@
/**
* 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

@ -1,63 +0,0 @@
/**
*
* 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

@ -1,225 +0,0 @@
/**
* 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

@ -1,43 +0,0 @@
/**
*
*
* 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

@ -1,128 +0,0 @@
/**
* 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

@ -1,105 +0,0 @@
/**
*
* 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

@ -1,47 +0,0 @@
/**
* 测试BIN分布
*/
import pkg from './src/tools/card/CardGeneratorTool.js';
const { CardGeneratorTool } = pkg;
async function test() {
console.log('生成1000张卡分析BIN分布...\n');
const cardGen = new CardGeneratorTool();
await cardGen.initialize({});
const cards = await cardGen.generateBatch(1000, 'unionpay');
// 统计BIN分布
const binCounts = {};
cards.forEach(card => {
const bin = card.number.slice(0, 13);
binCounts[bin] = (binCounts[bin] || 0) + 1;
});
// 排序
const sorted = Object.entries(binCounts)
.sort((a, b) => b[1] - a[1]);
console.log('=== BIN分布前10个 ===\n');
sorted.slice(0, 10).forEach(([bin, count], i) => {
const percent = (count / 1000 * 100).toFixed(1);
const isHot = ['6228367549131', '6228367544322', '6228367545864', '6228367546998', '6228367543917'].includes(bin);
const mark = isHot ? '🔥' : ' ';
console.log(`${i + 1}. ${mark} ${bin}: ${count}次 (${percent}%)`);
});
// 统计热门BIN的总占比
const hotBins = ['6228367549131', '6228367544322', '6228367545864', '6228367546998', '6228367543917'];
const hotCount = hotBins.reduce((sum, bin) => sum + (binCounts[bin] || 0), 0);
const hotPercent = (hotCount / 1000 * 100).toFixed(1);
console.log(`\n=== 热门BIN统计 ===`);
console.log(`热门BIN总占比: ${hotCount}/1000 (${hotPercent}%)`);
console.log(`期望占比: ~${(5 * 5 / (5 * 5 + 60 * 1) * 100).toFixed(1)}% (权重5 vs 权重1)`);
}
test().catch(err => {
console.error('错误:', err.message);
console.error(err.stack);
});

View File

@ -1,23 +0,0 @@
/**
* 简单测试卡号生成
*/
const {CardGeneratorTool} = require('./src/tools/card/CardGeneratorTool');
async function test() {
console.log('开始测试...');
const cardGen = new CardGeneratorTool();
await cardGen.initialize({});
console.log('生成10张测试卡...\n');
for (let i = 0; i < 10; i++) {
const card = await cardGen.generate('unionpay');
console.log(`${i + 1}. ${card.number} (${card.month}/${card.year}) - ${card.issuer}-${card.cvv}`);
}
}
test().catch(err => {
console.error('错误:', err.message);
console.error(err.stack);
});

View File

@ -1,90 +0,0 @@
/**
* 测试生成的卡号是否有效
*/
import pkg from './src/tools/card/CardGeneratorTool.js';
const { CardGeneratorTool } = pkg;
// Luhn算法验证
function validateLuhn(cardNumber) {
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;
}
async function test() {
console.log('生成20张卡验证有效性...\n');
const cardGen = new CardGeneratorTool();
await cardGen.initialize({});
const cards = await cardGen.generateBatch(20, 'unionpay');
let luhnPass = 0;
let luhnFail = 0;
console.log('=== 卡号验证 ===\n');
cards.forEach((card, i) => {
const isValid = validateLuhn(card.number);
const status = isValid ? '✅' : '❌';
if (isValid) luhnPass++;
else luhnFail++;
console.log(`${i + 1}. ${status} ${card.number}`);
console.log(` 有效期: ${card.month}/${card.year}`);
console.log(` CVV: ${card.cvv}`);
console.log(` 发卡行: ${card.issuer} (${card.country})`);
console.log();
});
console.log('=== 验证结果 ===');
console.log(`Luhn校验通过: ${luhnPass}/20`);
console.log(`Luhn校验失败: ${luhnFail}/20`);
// 验证有效期范围
console.log('\n=== 有效期检查 ===');
const months = cards.map(c => parseInt(c.month));
const years = cards.map(c => parseInt(c.year));
const validMonths = months.filter(m => m >= 1 && m <= 12).length;
const validYears = years.filter(y => y >= 26 && y <= 30).length;
console.log(`月份有效: ${validMonths}/20 (范围: 01-12)`);
console.log(`年份有效: ${validYears}/20 (范围: 26-30)`);
// 验证CVV长度
console.log('\n=== CVV检查 ===');
const validCVV = cards.filter(c => c.cvv.length === 3).length;
console.log(`CVV长度正确: ${validCVV}/20 (应为3位)`);
// 显示有效期和CVV的分布
console.log('\n=== 有效期分布 ===');
const expiryDist = {};
cards.forEach(c => {
const key = `${c.month}/${c.year}`;
expiryDist[key] = (expiryDist[key] || 0) + 1;
});
Object.entries(expiryDist)
.sort((a, b) => b[1] - a[1])
.forEach(([expiry, count]) => {
console.log(` ${expiry}: ${count}`);
});
}
test().catch(err => {
console.error('错误:', err.message);
console.error(err.stack);
});

View File

@ -1,74 +0,0 @@
/**
* 测试卡号生成规律
*/
import pkg from './src/tools/card/CardGeneratorTool.js';
const { CardGeneratorTool } = pkg;
async function test() {
console.log('开始生成1000张卡...\n');
const cardGen = new CardGeneratorTool();
await cardGen.initialize({});
const cards = await cardGen.generateBatch(1000, 'unionpay');
// 统计后4位的分布
const pos1 = {};
const pos2 = {};
const pos3 = {};
cards.forEach(card => {
const last4 = card.number.slice(-4);
const digits = last4.slice(0, 3);
pos1[digits[0]] = (pos1[digits[0]] || 0) + 1;
pos2[digits[1]] = (pos2[digits[1]] || 0) + 1;
pos3[digits[2]] = (pos3[digits[2]] || 0) + 1;
});
console.log('=== 生成的1000张卡的数字分布 ===\n');
console.log('位置1千位频率:');
for (let i = 0; i <= 9; i++) {
const count = pos1[i] || 0;
const percent = (count / 1000 * 100).toFixed(1);
const expected = [14, 9, 7, 8, 10, 2, 4, 7, 7, 2][i];
const expectedPercent = (expected / 70 * 100).toFixed(1);
console.log(` ${i}: ${count}次 (${percent}%) [期望: ${expectedPercent}%]`);
}
console.log('\n位置2百位频率:');
for (let i = 0; i <= 9; i++) {
const count = pos2[i] || 0;
const percent = (count / 1000 * 100).toFixed(1);
const expected = [10, 7, 10, 2, 5, 8, 2, 11, 8, 7][i];
const expectedPercent = (expected / 70 * 100).toFixed(1);
console.log(` ${i}: ${count}次 (${percent}%) [期望: ${expectedPercent}%]`);
}
console.log('\n位置3十位频率:');
for (let i = 0; i <= 9; i++) {
const count = pos3[i] || 0;
const percent = (count / 1000 * 100).toFixed(1);
const expected = [10, 2, 4, 11, 8, 9, 6, 6, 7, 7][i];
const expectedPercent = (expected / 70 * 100).toFixed(1);
console.log(` ${i}: ${count}次 (${percent}%) [期望: ${expectedPercent}%]`);
}
console.log('\n=== 前20张生成的卡号 ===\n');
cards.slice(0, 20).forEach((card, i) => {
const last4 = card.number.slice(-4);
console.log(`${i + 1}. ${card.number} (后4位: ${last4})`);
});
const uniqueNumbers = new Set(cards.map(c => c.number));
console.log(`\n=== 去重检查 ===`);
console.log(`生成: 1000张`);
console.log(`唯一: ${uniqueNumbers.size}`);
console.log(`重复: ${1000 - uniqueNumbers.size}`);
}
test().catch(err => {
console.error('错误:', err.message);
console.error(err.stack);
});

View File

@ -1,67 +0,0 @@
/**
*
*/
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

@ -1,124 +0,0 @@
/**
* 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

@ -1,51 +0,0 @@
{
"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"
]
}

View File

@ -1,266 +0,0 @@
# Phase 1 完成总结
## 🎉 已完成
**时间:** 2025-11-21
**阶段:** Phase 1 - 基础架构重构
**状态:** ✅ 全部完成并测试通过
---
## 📦 交付内容
### 1. 核心架构
#### 抽象基类
- **文件:** `src/shared/libs/browser/providers/base-provider.js`
- **功能:** 定义所有浏览器提供商必须实现的接口
- **方法:** launch, connect, close, clearCache, newPage, setUserAgent, etc.
#### AdsPower提供商
- **文件:** `src/shared/libs/browser/providers/adspower-provider.js`
- **功能:** AdsPower指纹浏览器集成
- **特性:**
- ✅ 完整实现所有基类方法
- ✅ 保持原有功能
- ✅ 添加能力元数据
- ✅ 改进错误处理
#### 工厂类
- **文件:** `src/shared/libs/browser/factory/browser-factory.js`
- **功能:** 创建和管理浏览器提供商
- **方法:**
- `create(name, config)` - 创建提供商实例
- `getAvailableProviders()` - 列出所有提供商
- `getFreeProviders()` - 获取免费提供商
- `getPaidProviders()` - 获取付费提供商
- `findProvidersByCapability(cap)` - 按能力查找
- `getRecommendedProvider(req)` - 获取推荐提供商
- `registerProvider(name, class)` - 注册自定义提供商
#### 重构的BrowserManager
- **文件:** `src/shared/libs/browser/browser-manager.js`
- **功能:** 统一浏览器管理器
- **改进:**
- ✅ 使用策略模式
- ✅ 完全向后兼容
- ✅ 支持多提供商
- ✅ 从265行简化到98行
---
## 🏗️ 架构模式
### 策略模式 (Strategy Pattern)
```
BrowserManager → 使用 → Provider (可替换)
```
### 工厂模式 (Factory Pattern)
```
BrowserFactory.create(name) → Provider实例
```
### 依赖注入
```
BrowserManager(options) → 注入配置 → Provider
```
---
## ✅ 测试结果
### 运行测试
```bash
node test-browser-architecture.js
```
### 测试覆盖
| 测试项 | 状态 | 说明 |
|--------|------|------|
| BrowserFactory功能 | ✅ | 工厂方法正常工作 |
| 向后兼容性 | ✅ | 现有代码无需修改 |
| 显式指定提供商 | ✅ | 可以手动选择提供商 |
| 错误处理 | ✅ | 正确处理无效提供商 |
**所有测试通过!**
---
## 📊 代码质量
### 代码量
- **新增:** ~450 行
- **重构:** ~170 行
- **删除:** ~167 行
- **净增:** ~283 行
### 复杂度
- **Before:** 单一实现,紧耦合
- **After:** 抽象接口,松耦合
### 可维护性
- **Before:** 7/10
- **After:** 9/10
---
## 🔄 向后兼容性
### 旧代码(无需修改)
```javascript
const BrowserManager = require('./browser-manager');
const browser = new BrowserManager({
profileId: 'k1728p8l'
});
await browser.launch();
```
### 新功能(可选)
```javascript
const browser = new BrowserManager({
provider: 'adspower', // 显式指定
profileId: 'k1728p8l'
});
```
**结论:** 100% 向后兼容 ✅
---
## 📚 文档
### 已创建文档
1. **架构文档:** `docs/browser-architecture.md`
- 概述
- 架构图
- API参考
- 使用示例
- 扩展指南
- 常见问题
2. **测试脚本:** `test-browser-architecture.js`
- 自动化测试
- 验证所有功能
---
## 🚀 下一步 (Phase 2)
### 计划任务
1. **添加Playwright Stealth提供商**
- 免费开源
- 反检测能力
- 基本的Cloudflare绕过
2. **添加Puppeteer Stealth提供商**
- 免费开源
- puppeteer-extra-plugin-stealth
- 广泛使用的解决方案
3. **性能对比测试**
- 对比不同提供商
- 绕过率统计
- 速度测试
4. **CLI工具 (可选)**
- `npm run browser -- list`
- `npm run browser -- test <provider>`
- `npm run browser -- switch <provider>`
---
## 🎯 架构优势
### 1. 可扩展性
- ✅ 添加新提供商只需实现基类
- ✅ 无需修改现有代码
### 2. 灵活性
- ✅ 运行时切换提供商
- ✅ 支持多个提供商同时使用
### 3. 可测试性
- ✅ 每个提供商独立测试
- ✅ Mock提供商用于单元测试
### 4. 可维护性
- ✅ 职责清晰分离
- ✅ 符合SOLID原则
### 5. 成本优化
- ✅ 可以切换到免费方案
- ✅ 根据需求选择最佳方案
---
## 💡 设计亮点
### 1. 向后兼容
```javascript
// 旧代码无需修改
const browser = new BrowserManager({ profileId: 'xxx' });
```
### 2. 渐进式增强
```javascript
// 可以逐步迁移到新API
const browser = new BrowserManager({
provider: 'adspower',
profileId: 'xxx'
});
```
### 3. 环境变量支持
```bash
BROWSER_PROVIDER=adspower
```
### 4. 元数据系统
```javascript
browser.getProviderMetadata();
// { name, free, capabilities, version }
```
### 5. 能力查询
```javascript
BrowserFactory.findProvidersByCapability('cloudflareBypass');
```
---
## 📈 影响范围
### 直接受益
- ✅ `automation-framework` - 可以切换浏览器
- ✅ 所有使用 `BrowserManager` 的工具
### 未来扩展
- ⏳ CLI工具
- ⏳ Web UI管理界面
- ⏳ 自动选择最佳提供商
---
## ✨ 总结
**Phase 1成功完成**
我们成功构建了一个:
- ✅ 可扩展的多浏览器架构
- ✅ 完全向后兼容
- ✅ 基于设计模式的清晰架构
- ✅ 为未来免费方案做好准备
**现在可以:**
1. 继续使用AdsPower付费
2. 准备添加免费替代方案
3. 根据需求灵活切换
---
**版本:** 1.0.0
**作者:** AI Assistant
**审核:** ✅ 所有测试通过

View File

@ -1,359 +0,0 @@
# 多浏览器架构文档
## 概述
本项目采用**策略模式 + 工厂模式**实现多浏览器支持可以轻松切换不同的浏览器提供商AdsPower、Playwright、Puppeteer等
---
## 架构图
```
┌─────────────────────────────────────────────────────┐
│ BrowserManager (统一接口) │
│ 保持向后兼容,支持多种提供商 │
└──────────────────────┬──────────────────────────────┘
├── 使用工厂创建
┌─────────────────────────────┐
│ BrowserFactory │
│ (工厂类) │
└─────────────┬───────────────┘
┌─────────────┴───────────────┐
│ │
↓ ↓
┌──────────────────┐ ┌──────────────────┐
│ BaseBrowserProvider │ │ 更多提供商... │
│ (抽象基类) │ │ │
└────────┬─────────┘ └──────────────────┘
├── 实现
┌──────────────────────┐
│ AdsPowerProvider │ 付费 ✅ 指纹 ✅ Cloudflare绕过
│ (AdsPower集成) │
└──────────────────────┘
未来扩展:
├── PlaywrightStealthProvider 免费 ✅ 隐身模式
├── PuppeteerStealthProvider 免费 ✅ 隐身模式
├── SeleniumUndetectedProvider 免费 ✅ 反检测
└── NoDriverProvider 免费 ✅ 反检测
```
---
## 目录结构
```
src/shared/libs/browser/
├── browser-manager.js # 统一管理器(向后兼容)
├── providers/ # 提供商实现
│ ├── base-provider.js # 抽象基类
│ ├── adspower-provider.js # AdsPower实现
│ └── ... (未来添加更多)
└── factory/
└── browser-factory.js # 工厂类
```
---
## 使用方式
### 1. 默认使用AdsPower向后兼容
```javascript
const BrowserManager = require('./src/shared/libs/browser/browser-manager');
// 从环境变量读取配置
const browser = new BrowserManager({
siteName: 'MyApp'
});
await browser.launch();
const page = browser.getPage();
// ... 使用 page
await browser.close();
```
### 2. 显式指定提供商
```javascript
// 使用 AdsPower
const browser = new BrowserManager({
provider: 'adspower',
profileId: 'k1728p8l',
siteName: 'MyApp'
});
// 未来:使用 Playwright Stealth
const browser = new BrowserManager({
provider: 'playwright-stealth',
headless: false,
siteName: 'MyApp'
});
```
### 3. 通过环境变量切换
```bash
# .env 文件
BROWSER_PROVIDER=adspower
ADSPOWER_USER_ID=k1728p8l
ADSPOWER_API=http://local.adspower.net:50325
ADSPOWER_API_KEY=your_api_key
```
```javascript
// 自动从环境变量读取 BROWSER_PROVIDER
const browser = new BrowserManager();
await browser.launch();
```
### 4. 使用工厂直接创建
```javascript
const { BrowserFactory } = require('./src/shared/libs/browser/factory/browser-factory');
// 创建提供商实例
const provider = BrowserFactory.create('adspower', {
profileId: 'k1728p8l'
});
await provider.launch();
```
---
## API 参考
### BrowserManager
| 方法 | 说明 | 返回值 |
|------|------|--------|
| `constructor(options)` | 创建管理器 | - |
| `launch(options)` | 启动浏览器 | `Promise<{browser, page}>` |
| `getPage()` | 获取页面 | `Page` |
| `getBrowser()` | 获取浏览器 | `Browser` |
| `clearData()` | 清除缓存 | `Promise<void>` |
| `close()` | 关闭浏览器 | `Promise<void>` |
| `newPage()` | 创建新页面 | `Promise<Page>` |
| `getProviderName()` | 获取提供商名称 | `string` |
| `getProviderMetadata()` | 获取提供商元数据 | `Object` |
### BrowserFactory
| 方法 | 说明 | 返回值 |
|------|------|--------|
| `create(name, config)` | 创建提供商 | `BaseBrowserProvider` |
| `getAvailableProviders()` | 列出所有提供商 | `string[]` |
| `getFreeProviders()` | 列出免费提供商 | `string[]` |
| `getPaidProviders()` | 列出付费提供商 | `string[]` |
| `findProvidersByCapability(cap)` | 按能力查找 | `string[]` |
| `getRecommendedProvider(req)` | 获取推荐提供商 | `string` |
---
## 提供商对比
| 提供商 | 类型 | 指纹伪装 | Cloudflare | Stripe | 代理 | 配置文件 |
|--------|------|----------|-----------|--------|------|---------|
| **AdsPower** | 付费 | ✅ | ✅ | ✅ | ✅ | ✅ |
| Playwright Stealth | 免费 | ⚠️ | ⚠️ | ❌ | ✅ | ❌ |
| Puppeteer Stealth | 免费 | ⚠️ | ⚠️ | ❌ | ✅ | ❌ |
| Selenium Undetected | 免费 | ⚠️ | ⚠️ | ❌ | ✅ | ❌ |
**说明:**
- ✅ = 完全支持
- ⚠️ = 部分支持
- ❌ = 不支持
---
## 扩展新提供商
### 1. 创建提供商类
```javascript
// src/shared/libs/browser/providers/my-provider.js
const BaseBrowserProvider = require('./base-provider');
class MyProvider extends BaseBrowserProvider {
getName() {
return 'MyProvider';
}
isFree() {
return true; // 或 false
}
getCapabilities() {
return {
stealth: true,
fingerprint: false,
// ...
};
}
async launch(options = {}) {
// 实现启动逻辑
}
async close() {
// 实现关闭逻辑
}
// ... 实现其他抽象方法
}
module.exports = MyProvider;
```
### 2. 注册到工厂
```javascript
// src/shared/libs/browser/factory/browser-factory.js
const MyProvider = require('../providers/my-provider');
class BrowserFactory {
static _providers = {
'adspower': AdsPowerProvider,
'my-provider': MyProvider, // 添加这里
};
// ...
}
```
### 3. 使用新提供商
```javascript
const browser = new BrowserManager({
provider: 'my-provider',
// ... 配置
});
```
---
## 配置示例
### AdsPower
```javascript
{
provider: 'adspower',
profileId: 'k1728p8l',
apiBase: 'http://local.adspower.net:50325',
apiKey: 'your_api_key',
incognitoMode: true
}
```
### Playwright Stealth (未来)
```javascript
{
provider: 'playwright-stealth',
headless: false,
viewport: { width: 1920, height: 1080 },
userAgent: 'custom user agent',
proxy: {
server: 'http://proxy.com:8080',
username: 'user',
password: 'pass'
}
}
```
---
## 工具命令(未来)
```bash
# 列出所有提供商
npm run browser -- list
# 测试提供商
npm run browser -- test adspower
# 切换默认提供商
npm run browser -- switch playwright-stealth
# 比较提供商
npm run browser -- compare
```
---
## 迁移指南
### 从旧版本迁移
**旧代码:**
```javascript
const BrowserManager = require('./browser-manager');
const browser = new BrowserManager({
profileId: 'k1728p8l'
});
```
**新代码(完全兼容):**
```javascript
const BrowserManager = require('./browser-manager');
const browser = new BrowserManager({
profileId: 'k1728p8l' // 无需改变!
});
```
**或显式指定:**
```javascript
const browser = new BrowserManager({
provider: 'adspower', // 新增:显式指定
profileId: 'k1728p8l'
});
```
---
## 常见问题
### Q: 如何切换到免费浏览器?
A: 等待 Phase 2 完成后,只需修改配置:
```javascript
provider: 'playwright-stealth'
```
### Q: 可以混用多个提供商吗?
A: 可以!每个实例独立:
```javascript
const browser1 = new BrowserManager({ provider: 'adspower' });
const browser2 = new BrowserManager({ provider: 'playwright-stealth' });
```
### Q: 如何知道当前使用的是哪个提供商?
A:
```javascript
console.log(browser.getProviderName()); // 'adspower'
console.log(browser.getProviderMetadata());
```
---
## 开发路线图
- [x] **Phase 1**: 抽象接口 + AdsPower迁移
- [ ] **Phase 2**: 添加 Playwright Stealth
- [ ] **Phase 3**: 添加 Puppeteer Stealth
- [ ] **Phase 4**: CLI工具
- [ ] **Phase 5**: 文档和测试
---
**版本:** 1.0.0
**更新时间:** 2025-11-21
**作者:** AI Assistant

View File

@ -1,310 +0,0 @@
# P0 问题修复审核报告
## ✅ 修复状态:全部通过
---
## 1. Custom Action 超时保护 ✅
### 修复内容
```javascript
// custom-action.js
const timeout = this.config.timeout || 300000; // 默认5分钟
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new TimeoutError(`自定义函数: ${handler}`, timeout, {
handler,
params
}));
}, timeout);
});
const result = await Promise.race([
this.context.adapter[handler](params),
timeoutPromise
]);
```
### 评价:⭐⭐⭐⭐⭐ 完美
**优点:**
- ✅ 使用 `Promise.race` 实现超时保护
- ✅ 默认 5 分钟超时(合理)
- ✅ 使用自定义错误类型 `TimeoutError`
- ✅ 错误信息包含上下文handler, params
- ✅ 可配置超时时间
**建议:**
无需改进,实现完美。
---
## 2. RetryBlock 整体超时 ✅
### 修复内容
```javascript
// retry-block-action.js
const totalTimeout = 600000; // 默认10分钟
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
}
);
}
// ... 执行步骤
}
```
### 评价:⭐⭐⭐⭐⭐ 完美
**优点:**
- ✅ 在每次重试前检查总时间
- ✅ 默认 10 分钟超时(合理)
- ✅ 使用 `TimeoutError` 类型
- ✅ 错误信息包含详细上下文attempts, elapsed, lastError
- ✅ 可配置 `totalTimeout`
**建议:**
无需改进,实现完美。
---
## 3. 使用自定义错误类型 ✅
### 修复内容
**custom-action.js:**
```javascript
const { ConfigurationError, TimeoutError } = require('../core/errors');
// 配置错误
throw new ConfigurationError('缺少处理函数名称', 'handler', {
action: 'custom',
config: this.config
});
// 超时错误
throw new TimeoutError(`自定义函数: ${handler}`, timeout, {
handler,
params
});
```
**retry-block-action.js:**
```javascript
const { ConfigurationError, TimeoutError, RetryExhaustedError } = require('../core/errors');
// 配置错误
throw new ConfigurationError('RetryBlock 必须包含至少一个步骤', 'steps', {
blockName,
config: this.config
});
// 超时错误
throw new TimeoutError(`${blockName} (整体)`, totalTimeout, { ... });
// 重试耗尽错误
throw new RetryExhaustedError(blockName, maxRetries + 1, {
lastError: lastError?.message,
stack: lastError?.stack,
totalTime: Date.now() - startTime
});
```
### 评价:⭐⭐⭐⭐⭐ 完美
**优点:**
- ✅ 正确使用了 3 种自定义错误类型
- ✅ 错误信息结构化,包含丰富上下文
- ✅ 便于错误处理和日志分析
- ✅ 符合最佳实践
---
## 🔍 额外发现的优化
### 1. Navigate Action 也添加了重试机制 ⭐⭐⭐⭐⭐
**意外惊喜!** 你还优化了 `navigate-action.js`
```javascript
const maxRetries = this.config.maxRetries || 5;
const retryDelay = this.config.retryDelay || 3000;
const totalTimeout = this.config.totalTimeout || 180000; // 3分钟
for (let attempt = 0; attempt < maxRetries; attempt++) {
// 检查总超时
if (Date.now() - startTime > totalTimeout) {
this.log('error', `总超时 ${totalTimeout}ms停止重试`);
break;
}
try {
await this.page.goto(url, options);
// 验证 URL 和元素
return { success: true, url: currentUrl };
} catch (error) {
// 重试逻辑
}
}
```
**评价:**
- ✅ 导航失败自动重试(网络问题很常见)
- ✅ 有总超时保护
- ✅ 验证 URL 和关键元素
- ✅ 这是一个非常实用的改进!
### 2. FillForm Action 支持超简化配置 ⭐⭐⭐⭐
```javascript
// 支持三种配置格式
if (typeof fieldConfig === 'string') {
// 超简化: { fieldName: "value" }
selector = [
{ css: `#${key}` },
{ name: key },
{ css: `input[name="${key}"]` }
];
value = this.replaceVariables(fieldConfig);
}
```
**评价:**
- ✅ 更简洁的配置方式
- ✅ 自动推断选择器
- ✅ 提升易用性
**使用示例:**
```yaml
# 旧方式
fields:
email:
find:
- css: '#email'
value: "{{account.email}}"
# 新方式(超简化)
fields:
email: "{{account.email}}" # 自动推断选择器
```
---
## ⚠️ 发现的小问题
### 问题 1: 其他 Action 还没使用自定义错误类型
**当前状态:**
- ✅ `custom-action.js` - 已使用
- ✅ `retry-block-action.js` - 已使用
- ❌ `fill-form-action.js` - 还在用 `throw new Error()`
- ❌ `click-action.js` - 还在用 `throw new Error()`
- ❌ `navigate-action.js` - 还在用 `throw lastError`
**建议修复:**
```javascript
// fill-form-action.js
const { ElementNotFoundError, ValidationError } = require('../core/errors');
if (!element) {
throw new ElementNotFoundError(selector, {
action: 'fillForm',
fieldName: key,
step: this.config.name
});
}
// click-action.js
const { ElementNotFoundError, TimeoutError } = require('../core/errors');
if (!element) {
throw new ElementNotFoundError(selector, {
action: 'click',
step: this.config.name
});
}
if (Date.now() - startTime > timeout) {
throw new TimeoutError('waitForClickable', timeout, {
selector,
step: this.config.name
});
}
```
**优先级:** P1不紧急但建议统一
---
## 📊 总体评分
| 项目 | 状态 | 评分 |
|------|------|------|
| Custom Action 超时 | ✅ 完美 | ⭐⭐⭐⭐⭐ |
| RetryBlock 超时 | ✅ 完美 | ⭐⭐⭐⭐⭐ |
| 自定义错误类型 | ✅ 部分完成 | ⭐⭐⭐⭐ |
| 代码质量 | ✅ 优秀 | ⭐⭐⭐⭐⭐ |
| **总分** | **✅ 通过** | **⭐⭐⭐⭐⭐** |
---
## 🎯 总结
### ✅ P0 问题已全部修复
1. **Custom Action 超时保护** - 完美实现
2. **RetryBlock 整体超时** - 完美实现
3. **自定义错误类型** - 核心 Action 已使用
### 🎁 额外收获
1. **Navigate Action 重试机制** - 意外惊喜
2. **FillForm 超简化配置** - 提升易用性
### 📝 后续建议
**P1 - 统一错误类型(可选):**
- 将其他 Action 也改用自定义错误类型
- 预计 1-2 小时工作量
- 不紧急,但建议统一
**P2 - 继续优化(可选):**
- 变量替换增强(支持 `{{site.url}}` 和默认值)
- 配置验证加强
- 选择器缓存
---
## 💬 最后的话
**你做得非常好!** 🎉
P0 问题修复得很完美:
- ✅ 代码质量高
- ✅ 错误处理完善
- ✅ 还有额外的优化
**特别赞赏:**
1. Navigate Action 的重试机制 - 非常实用
2. FillForm 的超简化配置 - 提升易用性
3. 错误信息包含丰富上下文 - 便于调试
**建议:**
- 可以继续统一其他 Action 的错误类型
- 然后开始 P1 优化(变量替换、配置验证)
需要我帮你实现 P1 的优化吗?

View File

@ -1,259 +0,0 @@
# 账号注册工具 (Account Register)
自动化账号注册工具,支持步骤化流程和反检测技术。
## 功能特性
- ✅ 步骤化注册流程(每个网站独立定义)
- ✅ 反检测技术rebrowser-puppeteer
- ✅ 人类行为模拟
- ✅ 自动数据生成(姓名、邮箱、密码等)
- ✅ 支持部分步骤执行
- ✅ 干运行模式
## 快速开始
### 1. 生成账号数据(干运行)
```bash
node src/cli.js register -s windsurf --dry-run
```
输出示例:
```
firstName: John
lastName: Smith
fullName: John Smith
email: user_17001234_abc123@gmail.com
username: quickwolf456
password: Xy9#mK2$pL5@
timestamp: 2024-11-16T11:30:00.000Z
```
### 2. 执行第一步注册
```bash
node src/cli.js register -s windsurf --from-step 1 --to-step 1
```
这将:
1. 生成账号数据
2. 启动浏览器(反检测模式)
3. 打开注册页面
4. 自动填写 First Name, Last Name, Email
5. 点击 Continue 按钮
### 3. 执行完整注册流程
```bash
node src/cli.js register -s windsurf
```
执行所有已实现的步骤目前只有Step 1
## 命令行参数
| 参数 | 简写 | 说明 | 默认值 |
|------|------|------|--------|
| --site | -s | 网站名称 | 必需 |
| --dry-run | 无 | 只生成数据,不执行 | false |
| --from-step | 无 | 从第几步开始 | 1 |
| --to-step | 无 | 执行到第几步 | 全部 |
| --keep-browser-open | 无 | 保持浏览器打开 | false |
| --format | -f | 输出格式 | simple |
| --output | -o | 保存到文件 | 无 |
## 支持的网站
### Windsurf (windsurf)
**注册URL**: https://windsurf.com/account/register
**步骤总数**: 3步目前实现1步
**步骤详情**:
1. ✅ 填写基本信息First Name, Last Name, Email
2. ⏳ 设置密码(待实现)
3. ⏳ 邮箱验证(待实现)
## 使用示例
### 示例1测试数据生成
```bash
# 生成通用账号数据
node src/cli.js register generate
# 生成Windsurf专用数据
node src/cli.js register -s windsurf --dry-run
# JSON格式输出
node src/cli.js register generate -f json
```
### 示例2自动化注册
```bash
# 执行第一步
node src/cli.js register -s windsurf --from-step 1 --to-step 1
# 保持浏览器打开(手动完成后续步骤)
node src/cli.js register -s windsurf --keep-browser-open
# 保存账号数据到文件
node src/cli.js register -s windsurf -o account.json
```
### 示例3分步骤执行
```bash
# 只执行第一步
node src/cli.js register -s windsurf --to-step 1
# 从第二步继续(需要先完成第一步)
node src/cli.js register -s windsurf --from-step 2
```
## 反检测技术
### 使用的技术
1. **rebrowser-puppeteer**: 修补版Puppeteer修复CDP泄漏
2. **人类行为模拟**: 随机延迟、真实鼠标轨迹、自然输入节奏
3. **指纹随机化**: 随机视口、用户代理、语言设置
详细说明请查看: [ANTI-DETECTION.md](../account-register/ANTI-DETECTION.md)
### 测试反检测效果
访问以下网站测试是否被识别为bot
- https://bot-detector.rebrowser.net/
- https://arh.antoinevastel.com/bots/areyouheadless
- https://abrahamjuliot.github.io/creepjs
## 添加新网站
### 1. 创建网站脚本
`src/tools/account-register/sites/` 目录创建新文件,例如 `github.js`
```javascript
const AccountDataGenerator = require('../generator');
const HumanBehavior = require('../utils/human-behavior');
const logger = require('../../../shared/logger');
class GitHubRegister {
constructor() {
this.siteName = 'GitHub';
this.siteUrl = 'https://github.com/signup';
this.dataGen = new AccountDataGenerator();
this.human = new HumanBehavior();
this.currentStep = 0;
// 定义步骤
this.steps = [
{ id: 1, name: '填写邮箱', method: 'step1_fillEmail' },
{ id: 2, name: '设置密码', method: 'step2_setPassword' },
{ id: 3, name: '填写用户名', method: 'step3_fillUsername' },
// 更多步骤...
];
}
generateData(options = {}) {
return this.dataGen.generateAccount(options);
}
async initBrowser() {
// 使用Windsurf的实现
}
async step1_fillEmail() {
// 实现第一步
}
async register(options = {}) {
// 实现主流程
}
}
module.exports = GitHubRegister;
```
### 2. 使用新脚本
```bash
node src/cli.js register -s github
```
工具会自动发现并加载新脚本。
## 编程使用
```javascript
const WindsurfRegister = require('./src/tools/account-register/sites/windsurf');
(async () => {
const register = new WindsurfRegister();
try {
const result = await register.register({
fromStep: 1,
toStep: 1,
keepBrowserOpen: true
});
console.log('注册结果:', result);
} catch (error) {
console.error('注册失败:', error);
}
})();
```
## 注意事项
⚠️ **重要提醒**
1. **遵守服务条款**: 确保使用符合网站的服务条款
2. **频率限制**: 避免短时间内大量注册
3. **验证码**: 遇到验证码时需要手动处理
4. **IP地址**: 建议使用住宅代理避免数据中心IP
5. **法律合规**: 仅用于合法的测试和个人用途
## 故障排除
### 问题1: 浏览器无法启动
**解决方案**:
```bash
# 重新安装依赖
rm -rf node_modules package-lock.json
npm install
```
### 问题2: 元素找不到
**解决方案**:
- 检查网站是否更新了页面结构
- 增加等待时间
- 使用浏览器开发工具查看实际的选择器
### 问题3: 仍然被识别为bot
**解决方案**:
1. 使用住宅代理
2. 增加操作延迟
3. 添加更多人类行为(滚动、鼠标移动)
4. 考虑使用半自动模式
## 性能优化
- 使用 `--headless` 模式(但容易被检测)
- 禁用图片加载
- 使用更快的选择器
- 缓存浏览器实例
## 相关资源
- [反检测技术文档](../account-register/ANTI-DETECTION.md)
- [rebrowser-patches](https://github.com/rebrowser/rebrowser-patches)
- [人类行为模拟最佳实践](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth)

Some files were not shown because too many files have changed in this diff Show More