dasdasd
This commit is contained in:
parent
5e6a0adc73
commit
4fcd5d1df5
7
.env
7
.env
@ -2,3 +2,10 @@
|
||||
ADSPOWER_USER_ID=k1728p8l
|
||||
ADSPOWER_API_KEY=35de43696f6241f3df895f2f48777a99
|
||||
# ADSPOWER_API=http://local.adspower.net:50325
|
||||
|
||||
# MySQL 数据库配置
|
||||
MYSQL_HOST=172.22.222.111
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=windsurf-auto-register
|
||||
MYSQL_PASSWORD=Qichen5210523
|
||||
MYSQL_DATABASE=auto_register
|
||||
|
||||
241
.windsurf/tasks/capsolver-debug.md
Normal file
241
.windsurf/tasks/capsolver-debug.md
Normal file
@ -0,0 +1,241 @@
|
||||
# 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. **根据结果**: 选择对应的修复方案
|
||||
199
.windsurf/tasks/capsolver-fix-implementation.md
Normal file
199
.windsurf/tasks/capsolver-fix-implementation.md
Normal file
@ -0,0 +1,199 @@
|
||||
# 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行)保持不变
|
||||
107
TASK_REMOVE_CAPSOLVER.md
Normal file
107
TASK_REMOVE_CAPSOLVER.md
Normal file
@ -0,0 +1,107 @@
|
||||
# 任务:删除 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()
|
||||
- [ ] 验证: 检查代码逻辑完整性
|
||||
@ -26,6 +26,7 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"imap": "^0.8.19",
|
||||
"mailparser": "^3.6.5",
|
||||
"mysql2": "^3.6.5",
|
||||
"node-capsolver": "^1.2.0",
|
||||
"puppeteer": "npm:rebrowser-puppeteer@^23.9.0",
|
||||
"puppeteer-real-browser": "^1.4.4"
|
||||
|
||||
@ -17,6 +17,7 @@ const logger = require('../../../shared/logger');
|
||||
const EmailVerificationService = require('../email-verification');
|
||||
const { DEFAULT_CONFIG } = require('../config');
|
||||
const CardGenerator = require('../../card-generator/generator');
|
||||
const database = require('../../database');
|
||||
|
||||
class WindsurfRegister {
|
||||
constructor() {
|
||||
@ -30,6 +31,12 @@ class WindsurfRegister {
|
||||
this.currentStep = 0;
|
||||
this.accountData = null;
|
||||
|
||||
// 记录注册时间和额外信息
|
||||
this.registrationTime = null;
|
||||
this.quotaInfo = null;
|
||||
this.billingInfo = null;
|
||||
this.cardInfo = null;
|
||||
|
||||
// 定义所有步骤
|
||||
this.steps = [
|
||||
{ id: 1, name: '填写基本信息', method: 'step1_fillBasicInfo' },
|
||||
@ -39,10 +46,9 @@ class WindsurfRegister {
|
||||
{ id: 5, name: '选择计划', method: 'step5_selectPlan' },
|
||||
{ id: 6, name: '填写支付信息', method: 'step6_fillPayment' },
|
||||
{ id: 7, name: '获取订阅信息', method: 'step7_getSubscriptionInfo' },
|
||||
{ id: 8, name: '保存到数据库', method: 'step8_saveToDatabase' },
|
||||
{ id: 9, name: '清理并关闭浏览器', method: 'step9_clearAndCloseBrowser' },
|
||||
];
|
||||
|
||||
// 记录注册时间
|
||||
this.registrationTime = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -268,45 +274,6 @@ class WindsurfRegister {
|
||||
logger.success(this.siteName, '✓ AdsPower 浏览器连接成功');
|
||||
logger.info(this.siteName, '✓ 使用真实指纹,可同时绕过 Cloudflare 和 Stripe');
|
||||
|
||||
// 清除所有浏览器数据(类似 Ctrl+Shift+Delete)
|
||||
logger.info(this.siteName, ' → 清除所有浏览器数据(Cookies、Cache、Storage等)...');
|
||||
try {
|
||||
// 使用 Chrome DevTools Protocol 进行深度清理
|
||||
const client = await this.page.target().createCDPSession();
|
||||
|
||||
// 1. 清除浏览器 Cookies
|
||||
await client.send('Network.clearBrowserCookies');
|
||||
logger.success(this.siteName, ' → ✓ 已清除所有 Cookies');
|
||||
|
||||
// 2. 清除浏览器缓存
|
||||
await client.send('Network.clearBrowserCache');
|
||||
logger.success(this.siteName, ' → ✓ 已清除浏览器缓存');
|
||||
|
||||
// 3. 清除所有存储数据(localStorage, sessionStorage, IndexedDB, WebSQL, Cache Storage, Service Workers)
|
||||
await client.send('Storage.clearDataForOrigin', {
|
||||
origin: '*',
|
||||
storageTypes: 'all'
|
||||
});
|
||||
logger.success(this.siteName, ' → ✓ 已清除所有存储数据');
|
||||
|
||||
// 4. 额外清理:访问目标网站并清除其存储
|
||||
await this.page.goto('https://windsurf.com', { waitUntil: 'domcontentloaded' });
|
||||
await this.page.evaluate(() => {
|
||||
try {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
// 5. 关闭 CDP 会话
|
||||
await client.detach();
|
||||
|
||||
logger.success(this.siteName, ' → ✓ 浏览器数据清除完成(全新状态)');
|
||||
|
||||
} catch (e) {
|
||||
logger.warn(this.siteName, ` → 清除浏览器数据失败: ${e.message}`);
|
||||
}
|
||||
|
||||
logger.info(this.siteName, '等待浏览器完全准备...');
|
||||
await this.human.randomDelay(2000, 3000);
|
||||
|
||||
@ -969,6 +936,15 @@ class WindsurfRegister {
|
||||
logger.info(this.siteName, ` → 有效期: ${card.month}/${card.year}`);
|
||||
logger.info(this.siteName, ` → CVV: ${card.cvv}`);
|
||||
|
||||
// 保存卡信息供后续使用
|
||||
this.cardInfo = {
|
||||
number: card.number,
|
||||
month: card.month,
|
||||
year: card.year,
|
||||
cvv: card.cvv,
|
||||
country: 'MO'
|
||||
};
|
||||
|
||||
// 2. 点击选择"银行卡"支付方式
|
||||
logger.info(this.siteName, ' → 选择银行卡支付方式...');
|
||||
const cardRadio = await this.page.$('input[type="radio"][value="card"]');
|
||||
@ -1051,12 +1027,99 @@ class WindsurfRegister {
|
||||
logger.info(this.siteName, ' → 点击订阅按钮...');
|
||||
await this.human.randomDelay(2000, 3000);
|
||||
|
||||
// 9. 点击订阅按钮并检测卡片拒绝(支持重试)
|
||||
const maxRetries = 5; // 最多重试5次
|
||||
let retryCount = 0;
|
||||
let paymentSuccess = false;
|
||||
|
||||
while (!paymentSuccess && retryCount < maxRetries) {
|
||||
const submitButton = await this.page.$('button[type="submit"][data-testid="hosted-payment-submit-button"]');
|
||||
if (submitButton) {
|
||||
if (!submitButton) {
|
||||
logger.warn(this.siteName, ' → 未找到订阅按钮');
|
||||
break;
|
||||
}
|
||||
|
||||
if (retryCount > 0) {
|
||||
logger.info(this.siteName, ` → 第 ${retryCount + 1} 次尝试提交...`);
|
||||
}
|
||||
|
||||
await submitButton.click();
|
||||
logger.success(this.siteName, ' → ✓ 已点击订阅按钮');
|
||||
|
||||
// 等待支付处理完成(检测离开Stripe页面)
|
||||
// 等待一下让页面响应
|
||||
await this.human.randomDelay(2000, 3000);
|
||||
|
||||
// 检测是否出现"银行卡被拒绝"错误
|
||||
const cardRejected = await this.page.evaluate(() => {
|
||||
const errorDiv = document.querySelector('.FieldError-container');
|
||||
if (errorDiv) {
|
||||
const errorText = errorDiv.textContent;
|
||||
return errorText.includes('银行卡被拒绝') ||
|
||||
errorText.includes('card was declined') ||
|
||||
errorText.includes('被拒绝');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (cardRejected) {
|
||||
retryCount++;
|
||||
logger.warn(this.siteName, ` → ⚠️ 银行卡被拒绝!(第 ${retryCount}/${maxRetries} 次)`);
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
logger.error(this.siteName, ` → ✗ 已达到最大重试次数 (${maxRetries})`);
|
||||
throw new Error(`银行卡被拒绝,已重试 ${maxRetries} 次`);
|
||||
}
|
||||
|
||||
// 生成新的银行卡
|
||||
logger.info(this.siteName, ' → 生成新的银行卡信息...');
|
||||
const cardGen = new CardGenerator();
|
||||
const newCard = cardGen.generate('unionpay');
|
||||
logger.info(this.siteName, ` → 新卡号: ${newCard.number}`);
|
||||
logger.info(this.siteName, ` → 有效期: ${newCard.month}/${newCard.year}`);
|
||||
logger.info(this.siteName, ` → CVV: ${newCard.cvv}`);
|
||||
|
||||
// 更新卡信息
|
||||
this.cardInfo = {
|
||||
number: newCard.number,
|
||||
month: newCard.month,
|
||||
year: newCard.year,
|
||||
cvv: newCard.cvv,
|
||||
country: 'MO'
|
||||
};
|
||||
|
||||
// 清空并重新填写卡号
|
||||
logger.info(this.siteName, ' → 清空并重新填写卡号...');
|
||||
const cardNumberField = await this.page.$('#cardNumber');
|
||||
await cardNumberField.click({ clickCount: 3 });
|
||||
await this.page.keyboard.press('Backspace');
|
||||
await this.human.randomDelay(500, 1000);
|
||||
await cardNumberField.type(newCard.number, { delay: 250 });
|
||||
|
||||
// 清空并重新填写有效期
|
||||
logger.info(this.siteName, ' → 清空并重新填写有效期...');
|
||||
const cardExpiryField = await this.page.$('#cardExpiry');
|
||||
await cardExpiryField.click({ clickCount: 3 });
|
||||
await this.page.keyboard.press('Backspace');
|
||||
await this.human.randomDelay(300, 500);
|
||||
const expiry = `${newCard.month}${newCard.year}`;
|
||||
await cardExpiryField.type(expiry, { delay: 250 });
|
||||
|
||||
// 清空并重新填写CVC
|
||||
logger.info(this.siteName, ' → 清空并重新填写CVC...');
|
||||
const cardCvcField = await this.page.$('#cardCvc');
|
||||
await cardCvcField.click({ clickCount: 3 });
|
||||
await this.page.keyboard.press('Backspace');
|
||||
await this.human.randomDelay(300, 500);
|
||||
await cardCvcField.type(newCard.cvv, { delay: 250 });
|
||||
|
||||
logger.success(this.siteName, ' → ✓ 已更新银行卡信息,准备重试...');
|
||||
await this.human.randomDelay(2000, 3000);
|
||||
|
||||
// 继续下一次循环重试
|
||||
continue;
|
||||
}
|
||||
|
||||
// 没有错误,等待支付处理完成
|
||||
logger.info(this.siteName, ' → 等待支付处理...');
|
||||
logger.info(this.siteName, ' → 将持续等待直到支付完成(无时间限制)...');
|
||||
|
||||
@ -1069,6 +1132,7 @@ class WindsurfRegister {
|
||||
// 检测是否已经离开 Stripe 页面(支付成功的标志)
|
||||
if (!currentUrl.includes('stripe.com') && !currentUrl.includes('checkout.stripe.com')) {
|
||||
paymentComplete = true;
|
||||
paymentSuccess = true;
|
||||
const totalTime = ((Date.now() - paymentStartTime) / 1000).toFixed(1);
|
||||
|
||||
// 记录注册时间(支付成功的时间)
|
||||
@ -1087,6 +1151,7 @@ class WindsurfRegister {
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
// 额外等待页面稳定
|
||||
await this.human.randomDelay(2000, 3000);
|
||||
@ -1116,7 +1181,12 @@ class WindsurfRegister {
|
||||
try {
|
||||
await this.page.waitForTimeout(2000); // 等待弹窗出现
|
||||
|
||||
// 尝试查找并点击"取消"或"打开Windsurf"按钮
|
||||
// 方法1: 尝试按ESC键关闭浏览器原生对话框
|
||||
logger.info(this.siteName, ' → 尝试按ESC键关闭原生对话框...');
|
||||
await this.page.keyboard.press('Escape');
|
||||
await this.human.randomDelay(500, 1000);
|
||||
|
||||
// 方法2: 尝试查找并点击网页内的按钮(如果是HTML弹窗)
|
||||
const closeDialog = await this.page.evaluate(() => {
|
||||
// 查找包含"取消"或"打开Windsurf"的按钮
|
||||
const buttons = Array.from(document.querySelectorAll('button'));
|
||||
@ -1144,13 +1214,13 @@ class WindsurfRegister {
|
||||
});
|
||||
|
||||
if (closeDialog) {
|
||||
logger.success(this.siteName, ` → ✓ 已关闭弹窗(点击了"${closeDialog}")`);
|
||||
logger.success(this.siteName, ` → ✓ 已关闭HTML弹窗(点击了"${closeDialog}")`);
|
||||
await this.human.randomDelay(1000, 2000);
|
||||
} else {
|
||||
logger.info(this.siteName, ' → 未检测到弹窗');
|
||||
logger.success(this.siteName, ' → ✓ 已尝试关闭原生对话框(ESC键)');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.info(this.siteName, ' → 未检测到弹窗或已自动关闭');
|
||||
logger.info(this.siteName, ` → 关闭弹窗时出错: ${e.message}`);
|
||||
}
|
||||
|
||||
// 1. 跳转到订阅使用页面
|
||||
@ -1240,6 +1310,10 @@ class WindsurfRegister {
|
||||
|
||||
logger.info(this.siteName, '');
|
||||
|
||||
// 保存订阅信息供后续使用
|
||||
this.quotaInfo = quotaInfo;
|
||||
this.billingInfo = billingInfo;
|
||||
|
||||
this.currentStep = 7;
|
||||
logger.success(this.siteName, `步骤 7 完成`);
|
||||
|
||||
@ -1249,6 +1323,112 @@ class WindsurfRegister {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 步骤8: 保存到数据库
|
||||
*/
|
||||
async step8_saveToDatabase() {
|
||||
logger.info(this.siteName, `[步骤 8/${this.getTotalSteps()}] 保存到数据库`);
|
||||
|
||||
try {
|
||||
// 初始化数据库连接
|
||||
logger.info(this.siteName, ' → 连接数据库...');
|
||||
await database.initialize();
|
||||
|
||||
// 准备账号数据
|
||||
const accountData = {
|
||||
email: this.accountData.email,
|
||||
password: this.accountData.password,
|
||||
firstName: this.accountData.firstName,
|
||||
lastName: this.accountData.lastName,
|
||||
registrationTime: this.registrationTime,
|
||||
quotaUsed: this.quotaInfo ? parseFloat(this.quotaInfo.used) : 0,
|
||||
quotaTotal: this.quotaInfo ? parseFloat(this.quotaInfo.total) : 0,
|
||||
billingDays: this.billingInfo ? parseInt(this.billingInfo.days) : null,
|
||||
billingDate: this.billingInfo ? this.billingInfo.date : null,
|
||||
paymentCardNumber: this.cardInfo ? this.cardInfo.number : null,
|
||||
paymentCountry: this.cardInfo ? this.cardInfo.country : 'MO',
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
// 保存到数据库
|
||||
logger.info(this.siteName, ' → 保存账号信息...');
|
||||
const accountRepo = database.getRepository('account');
|
||||
const accountId = await accountRepo.create(accountData);
|
||||
|
||||
logger.success(this.siteName, ` → ✓ 账号信息已保存到数据库 (ID: ${accountId})`);
|
||||
logger.info(this.siteName, ` → 邮箱: ${accountData.email}`);
|
||||
logger.info(this.siteName, ` → 配额: ${accountData.quotaUsed} / ${accountData.quotaTotal}`);
|
||||
logger.info(this.siteName, ` → 卡号: ${accountData.paymentCardNumber}`);
|
||||
|
||||
this.currentStep = 8;
|
||||
logger.success(this.siteName, `步骤 8 完成`);
|
||||
|
||||
} catch (error) {
|
||||
logger.error(this.siteName, `保存到数据库失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 步骤9: 清理并关闭浏览器
|
||||
*/
|
||||
async step9_clearAndCloseBrowser() {
|
||||
logger.info(this.siteName, `[步骤 9/${this.getTotalSteps()}] 清理并关闭浏览器`);
|
||||
|
||||
try {
|
||||
// 清除所有浏览器数据(类似 Ctrl+Shift+Delete)
|
||||
logger.info(this.siteName, ' → 清除所有浏览器数据(Cookies、Cache、Storage等)...');
|
||||
try {
|
||||
// 使用 Chrome DevTools Protocol 进行深度清理
|
||||
const client = await this.page.target().createCDPSession();
|
||||
|
||||
// 1. 清除浏览器 Cookies
|
||||
await client.send('Network.clearBrowserCookies');
|
||||
logger.success(this.siteName, ' → ✓ 已清除所有 Cookies');
|
||||
|
||||
// 2. 清除浏览器缓存
|
||||
await client.send('Network.clearBrowserCache');
|
||||
logger.success(this.siteName, ' → ✓ 已清除浏览器缓存');
|
||||
|
||||
// 3. 清除所有存储数据(localStorage, sessionStorage, IndexedDB, WebSQL, Cache Storage, Service Workers)
|
||||
await client.send('Storage.clearDataForOrigin', {
|
||||
origin: '*',
|
||||
storageTypes: 'all'
|
||||
});
|
||||
logger.success(this.siteName, ' → ✓ 已清除所有存储数据');
|
||||
|
||||
// 4. 额外清理:访问目标网站并清除其存储
|
||||
await this.page.goto('https://windsurf.com', { waitUntil: 'domcontentloaded' });
|
||||
await this.page.evaluate(() => {
|
||||
try {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
// 5. 关闭 CDP 会话
|
||||
await client.detach();
|
||||
|
||||
logger.success(this.siteName, ' → ✓ 浏览器数据清除完成(全新状态)');
|
||||
|
||||
} catch (e) {
|
||||
logger.warn(this.siteName, ` → 清除浏览器数据失败: ${e.message}`);
|
||||
}
|
||||
|
||||
// 关闭浏览器
|
||||
logger.info(this.siteName, ' → 关闭浏览器...');
|
||||
await this.closeBrowser();
|
||||
logger.success(this.siteName, ' → ✓ 浏览器已关闭');
|
||||
|
||||
this.currentStep = 9;
|
||||
logger.success(this.siteName, `步骤 9 完成`);
|
||||
|
||||
} catch (error) {
|
||||
logger.error(this.siteName, `清理并关闭浏览器失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行注册流程
|
||||
* @param {Object} options - 选项
|
||||
@ -1291,20 +1471,7 @@ class WindsurfRegister {
|
||||
}
|
||||
|
||||
const stepInfo = this.getCurrentStepInfo();
|
||||
logger.success(this.siteName, `\n已完成步骤 ${fromStep} 到 ${this.currentStep}`);
|
||||
|
||||
if (this.currentStep < this.getTotalSteps()) {
|
||||
logger.info(this.siteName, `剩余步骤需要手动完成或等待后续开发`);
|
||||
}
|
||||
|
||||
// 不自动关闭浏览器,让用户查看结果
|
||||
if (!options.keepBrowserOpen) {
|
||||
logger.info(this.siteName, '10秒后关闭浏览器...');
|
||||
await this.page.waitForTimeout(10000);
|
||||
await this.closeBrowser();
|
||||
} else {
|
||||
logger.info(this.siteName, '浏览器保持打开状态');
|
||||
}
|
||||
logger.success(this.siteName, `\n✅ 所有步骤已完成 (${fromStep} 到 ${this.currentStep})`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
169
src/tools/database/README.md
Normal file
169
src/tools/database/README.md
Normal file
@ -0,0 +1,169 @@
|
||||
# Database Tool - MySQL 数据库工具
|
||||
|
||||
## 📋 表设计
|
||||
|
||||
### windsurf_accounts 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE `windsurf_accounts` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
`email` VARCHAR(255) NOT NULL COMMENT '邮箱地址',
|
||||
`password` VARCHAR(255) NOT NULL COMMENT '密码',
|
||||
`first_name` VARCHAR(100) NOT NULL COMMENT '名',
|
||||
`last_name` VARCHAR(100) NOT NULL COMMENT '姓',
|
||||
`registration_time` DATETIME COMMENT '注册时间',
|
||||
`quota_used` DECIMAL(10,2) DEFAULT 0.00 COMMENT '已使用配额',
|
||||
`quota_total` DECIMAL(10,2) DEFAULT 0.00 COMMENT '总配额',
|
||||
`billing_days` INT COMMENT '下次账单天数',
|
||||
`billing_date` VARCHAR(50) COMMENT '账单日期',
|
||||
`payment_card_number` VARCHAR(20) COMMENT '支付卡号(完整)',
|
||||
`payment_country` VARCHAR(10) DEFAULT 'MO' COMMENT '支付国家代码',
|
||||
`status` ENUM('active', 'expired', 'error') DEFAULT 'active' COMMENT '账号状态',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
UNIQUE KEY `uk_email` (`email`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_registration_time` (`registration_time`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Windsurf 账号表';
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
|
||||
| 字段名 | 类型 | 说明 | 备注 |
|
||||
|--------|------|------|------|
|
||||
| id | BIGINT | 主键ID | 自增 |
|
||||
| email | VARCHAR(255) | 邮箱地址 | 唯一索引 |
|
||||
| password | VARCHAR(255) | 密码 | 明文存储(测试用) |
|
||||
| first_name | VARCHAR(100) | 名 | - |
|
||||
| last_name | VARCHAR(100) | 姓 | - |
|
||||
| registration_time | DATETIME | 注册时间 | 支付成功时间 |
|
||||
| quota_used | DECIMAL(10,2) | 已使用配额 | 默认0 |
|
||||
| quota_total | DECIMAL(10,2) | 总配额 | 默认0 |
|
||||
| billing_days | INT | 下次账单天数 | 可空 |
|
||||
| billing_date | VARCHAR(50) | 账单日期 | 可空 |
|
||||
| payment_card_number | VARCHAR(20) | 支付卡号 | **完整卡号** |
|
||||
| payment_country | VARCHAR(10) | 支付国家 | 默认MO(澳门) |
|
||||
| status | ENUM | 账号状态 | active/expired/error |
|
||||
| created_at | TIMESTAMP | 记录创建时间 | 自动 |
|
||||
| updated_at | TIMESTAMP | 记录更新时间 | 自动 |
|
||||
|
||||
## 🔧 使用方式
|
||||
|
||||
### 1. 初始化数据库
|
||||
|
||||
```bash
|
||||
# 登录 MySQL
|
||||
mysql -h 172.22.222.111 -P 3306 -u windsurf-auto-register -p
|
||||
|
||||
# 执行初始化脚本
|
||||
source src/tools/database/init.sql
|
||||
```
|
||||
|
||||
### 2. 配置环境变量
|
||||
|
||||
在 `.env` 文件中:
|
||||
|
||||
```bash
|
||||
MYSQL_HOST=172.22.222.111
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=windsurf-auto-register
|
||||
MYSQL_PASSWORD=Qichen5210523
|
||||
MYSQL_DATABASE=auto_register
|
||||
```
|
||||
|
||||
### 3. 在代码中使用
|
||||
|
||||
```javascript
|
||||
const database = require('./src/tools/database');
|
||||
|
||||
// 初始化连接
|
||||
await database.initialize();
|
||||
|
||||
// 获取 Repository
|
||||
const accountRepo = database.getRepository('account');
|
||||
|
||||
// 创建账号
|
||||
const accountId = await accountRepo.create({
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
registrationTime: new Date(),
|
||||
quotaTotal: 600.00,
|
||||
paymentCardNumber: '6228367541234567',
|
||||
paymentCountry: 'MO',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// 查询账号
|
||||
const account = await accountRepo.findByEmail('test@example.com');
|
||||
|
||||
// 更新账号
|
||||
await accountRepo.update('test@example.com', {
|
||||
quotaUsed: 10.5,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// 关闭连接
|
||||
await database.close();
|
||||
```
|
||||
|
||||
## 🏗️ 设计模式
|
||||
|
||||
### 单例模式 (Singleton)
|
||||
`DatabaseConnection` 使用单例模式,确保全局只有一个数据库连接池。
|
||||
|
||||
### 仓储模式 (Repository)
|
||||
`AccountRepository` 封装所有数据库操作,业务逻辑无需关心 SQL 细节。
|
||||
|
||||
### 工厂模式 (Factory)
|
||||
`database.getRepository()` 使用工厂方法获取不同的 Repository。
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
src/tools/database/
|
||||
├── index.js # 工具入口
|
||||
├── connection.js # 数据库连接(单例)
|
||||
├── config.js # 配置文件
|
||||
├── account-repository.js # 账号仓储
|
||||
├── init.sql # 初始化脚本
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
## 🔄 扩展性
|
||||
|
||||
### 添加新表
|
||||
|
||||
1. 在 `init.sql` 添加建表语句
|
||||
2. 创建对应的 Repository 文件(如 `user-repository.js`)
|
||||
3. 在 `index.js` 注册新的 Repository
|
||||
|
||||
示例:
|
||||
|
||||
```javascript
|
||||
// src/tools/database/index.js
|
||||
getRepository(name) {
|
||||
const repositories = {
|
||||
account: new AccountRepository(connection),
|
||||
user: new UserRepository(connection) // 新增
|
||||
};
|
||||
|
||||
return repositories[name];
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **安全性**: 当前密码为明文存储,仅用于测试环境
|
||||
2. **连接池**: 默认最大连接数为10,可在 `config.js` 调整
|
||||
3. **重复插入**: 邮箱已存在时会自动更新记录
|
||||
4. **事务**: 暂未实现,复杂操作需自行管理事务
|
||||
|
||||
## 📊 数据库配置信息
|
||||
|
||||
- **地址**: 172.22.222.111:3306
|
||||
- **用户**: windsurf-auto-register
|
||||
- **密码**: Qichen5210523
|
||||
- **数据库**: auto_register
|
||||
189
src/tools/database/account-repository.js
Normal file
189
src/tools/database/account-repository.js
Normal file
@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Account Repository - 账号数据访问层(Repository 模式)
|
||||
*/
|
||||
|
||||
const logger = require('../../shared/logger');
|
||||
|
||||
class AccountRepository {
|
||||
constructor(connection) {
|
||||
this.db = connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建账号记录
|
||||
*/
|
||||
async create(accountData) {
|
||||
const {
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
registrationTime,
|
||||
quotaUsed,
|
||||
quotaTotal,
|
||||
billingDays,
|
||||
billingDate,
|
||||
paymentCardNumber,
|
||||
paymentCountry,
|
||||
status
|
||||
} = accountData;
|
||||
|
||||
const sql = `
|
||||
INSERT INTO windsurf_accounts (
|
||||
email, password, first_name, last_name, registration_time,
|
||||
quota_used, quota_total, billing_days, billing_date,
|
||||
payment_card_number, payment_country, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const params = [
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
registrationTime,
|
||||
quotaUsed || 0,
|
||||
quotaTotal || 0,
|
||||
billingDays || null,
|
||||
billingDate || null,
|
||||
paymentCardNumber || null,
|
||||
paymentCountry || 'MO',
|
||||
status || 'active'
|
||||
];
|
||||
|
||||
try {
|
||||
const result = await this.db.query(sql, params);
|
||||
logger.success('AccountRepository', `✓ Account created: ${email} (ID: ${result.insertId})`);
|
||||
return result.insertId;
|
||||
} catch (error) {
|
||||
if (error.code === 'ER_DUP_ENTRY') {
|
||||
logger.warn('AccountRepository', `Account already exists: ${email}`);
|
||||
// 如果账号已存在,尝试更新
|
||||
return await this.update(email, accountData);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新账号记录
|
||||
*/
|
||||
async update(email, accountData) {
|
||||
const updates = [];
|
||||
const params = [];
|
||||
|
||||
if (accountData.password !== undefined) {
|
||||
updates.push('password = ?');
|
||||
params.push(accountData.password);
|
||||
}
|
||||
if (accountData.firstName !== undefined) {
|
||||
updates.push('first_name = ?');
|
||||
params.push(accountData.firstName);
|
||||
}
|
||||
if (accountData.lastName !== undefined) {
|
||||
updates.push('last_name = ?');
|
||||
params.push(accountData.lastName);
|
||||
}
|
||||
if (accountData.registrationTime !== undefined) {
|
||||
updates.push('registration_time = ?');
|
||||
params.push(accountData.registrationTime);
|
||||
}
|
||||
if (accountData.quotaUsed !== undefined) {
|
||||
updates.push('quota_used = ?');
|
||||
params.push(accountData.quotaUsed);
|
||||
}
|
||||
if (accountData.quotaTotal !== undefined) {
|
||||
updates.push('quota_total = ?');
|
||||
params.push(accountData.quotaTotal);
|
||||
}
|
||||
if (accountData.billingDays !== undefined) {
|
||||
updates.push('billing_days = ?');
|
||||
params.push(accountData.billingDays);
|
||||
}
|
||||
if (accountData.billingDate !== undefined) {
|
||||
updates.push('billing_date = ?');
|
||||
params.push(accountData.billingDate);
|
||||
}
|
||||
if (accountData.paymentCardNumber !== undefined) {
|
||||
updates.push('payment_card_number = ?');
|
||||
params.push(accountData.paymentCardNumber);
|
||||
}
|
||||
if (accountData.paymentCountry !== undefined) {
|
||||
updates.push('payment_country = ?');
|
||||
params.push(accountData.paymentCountry);
|
||||
}
|
||||
if (accountData.status !== undefined) {
|
||||
updates.push('status = ?');
|
||||
params.push(accountData.status);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
logger.warn('AccountRepository', 'No fields to update');
|
||||
return null;
|
||||
}
|
||||
|
||||
params.push(email);
|
||||
const sql = `UPDATE windsurf_accounts SET ${updates.join(', ')} WHERE email = ?`;
|
||||
|
||||
const result = await this.db.query(sql, params);
|
||||
logger.success('AccountRepository', `✓ Account updated: ${email}`);
|
||||
return result.affectedRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询账号
|
||||
*/
|
||||
async findByEmail(email) {
|
||||
const sql = 'SELECT * FROM windsurf_accounts WHERE email = ?';
|
||||
const rows = await this.db.query(sql, [email]);
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有账号
|
||||
*/
|
||||
async findAll(options = {}) {
|
||||
const { status, limit = 100, offset = 0 } = options;
|
||||
|
||||
let sql = 'SELECT * FROM windsurf_accounts';
|
||||
const params = [];
|
||||
|
||||
if (status) {
|
||||
sql += ' WHERE status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||
params.push(limit, offset);
|
||||
|
||||
return await this.db.query(sql, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除账号
|
||||
*/
|
||||
async delete(email) {
|
||||
const sql = 'DELETE FROM windsurf_accounts WHERE email = ?';
|
||||
const result = await this.db.query(sql, [email]);
|
||||
logger.info('AccountRepository', `Account deleted: ${email}`);
|
||||
return result.affectedRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计账号数量
|
||||
*/
|
||||
async count(status = null) {
|
||||
let sql = 'SELECT COUNT(*) as total FROM windsurf_accounts';
|
||||
const params = [];
|
||||
|
||||
if (status) {
|
||||
sql += ' WHERE status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
const rows = await this.db.query(sql, params);
|
||||
return rows[0].total;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AccountRepository;
|
||||
24
src/tools/database/config.js
Normal file
24
src/tools/database/config.js
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Database Configuration - 数据库配置
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// 默认连接配置
|
||||
connection: {
|
||||
host: process.env.MYSQL_HOST || '172.22.222.111',
|
||||
port: parseInt(process.env.MYSQL_PORT || '3306'),
|
||||
user: process.env.MYSQL_USER || 'windsurf-auto-register',
|
||||
password: process.env.MYSQL_PASSWORD || 'Qichen5210523',
|
||||
database: process.env.MYSQL_DATABASE || 'auto_register',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
enableKeepAlive: true,
|
||||
keepAliveInitialDelay: 0
|
||||
},
|
||||
|
||||
// 表名配置
|
||||
tables: {
|
||||
accounts: 'windsurf_accounts'
|
||||
}
|
||||
};
|
||||
85
src/tools/database/connection.js
Normal file
85
src/tools/database/connection.js
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Database Connection - 数据库连接管理(单例模式)
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const logger = require('../../shared/logger');
|
||||
const { connection: defaultConfig } = require('./config');
|
||||
|
||||
class DatabaseConnection {
|
||||
constructor() {
|
||||
if (DatabaseConnection.instance) {
|
||||
return DatabaseConnection.instance;
|
||||
}
|
||||
|
||||
this.pool = null;
|
||||
DatabaseConnection.instance = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化连接池
|
||||
*/
|
||||
async initialize(config) {
|
||||
if (this.pool) {
|
||||
logger.warn('Database', 'Connection pool already initialized');
|
||||
return this.pool;
|
||||
}
|
||||
|
||||
const dbConfig = config || defaultConfig;
|
||||
|
||||
try {
|
||||
this.pool = mysql.createPool(dbConfig);
|
||||
|
||||
// 测试连接
|
||||
const connection = await this.pool.getConnection();
|
||||
await connection.ping();
|
||||
connection.release();
|
||||
|
||||
logger.success('Database', `✓ Connected to MySQL: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`);
|
||||
|
||||
return this.pool;
|
||||
} catch (error) {
|
||||
logger.error('Database', `Failed to connect: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接池
|
||||
*/
|
||||
getPool() {
|
||||
if (!this.pool) {
|
||||
throw new Error('Database pool not initialized. Call initialize() first.');
|
||||
}
|
||||
return this.pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行查询
|
||||
*/
|
||||
async query(sql, params) {
|
||||
const pool = this.getPool();
|
||||
try {
|
||||
const [rows] = await pool.execute(sql, params);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
logger.error('Database', `Query failed: ${error.message}`);
|
||||
logger.error('Database', `SQL: ${sql}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接池
|
||||
*/
|
||||
async close() {
|
||||
if (this.pool) {
|
||||
await this.pool.end();
|
||||
this.pool = null;
|
||||
logger.info('Database', 'Connection pool closed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
module.exports = new DatabaseConnection();
|
||||
37
src/tools/database/index.js
Normal file
37
src/tools/database/index.js
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Database Tool - 数据库工具入口
|
||||
*/
|
||||
|
||||
const connection = require('./connection');
|
||||
const AccountRepository = require('./account-repository');
|
||||
|
||||
module.exports = {
|
||||
name: 'database',
|
||||
alias: 'db',
|
||||
description: 'MySQL数据库工具',
|
||||
|
||||
/**
|
||||
* 初始化数据库连接
|
||||
*/
|
||||
async initialize(config) {
|
||||
return await connection.initialize(config);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取 Repository
|
||||
*/
|
||||
getRepository(name) {
|
||||
const repositories = {
|
||||
account: new AccountRepository(connection)
|
||||
};
|
||||
|
||||
return repositories[name];
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭数据库连接
|
||||
*/
|
||||
async close() {
|
||||
return await connection.close();
|
||||
}
|
||||
};
|
||||
37
src/tools/database/init.sql
Normal file
37
src/tools/database/init.sql
Normal file
@ -0,0 +1,37 @@
|
||||
-- ============================================
|
||||
-- Windsurf Auto Register 数据库初始化脚本
|
||||
-- ============================================
|
||||
|
||||
-- 创建数据库(如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS `auto_register`
|
||||
DEFAULT CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE `auto_register`;
|
||||
|
||||
-- 创建 Windsurf 账号表
|
||||
CREATE TABLE IF NOT EXISTS `windsurf_accounts` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
`email` VARCHAR(255) NOT NULL COMMENT '邮箱地址',
|
||||
`password` VARCHAR(255) NOT NULL COMMENT '密码',
|
||||
`first_name` VARCHAR(100) NOT NULL COMMENT '名',
|
||||
`last_name` VARCHAR(100) NOT NULL COMMENT '姓',
|
||||
`registration_time` DATETIME COMMENT '注册时间',
|
||||
`quota_used` DECIMAL(10,2) DEFAULT 0.00 COMMENT '已使用配额',
|
||||
`quota_total` DECIMAL(10,2) DEFAULT 0.00 COMMENT '总配额',
|
||||
`billing_days` INT COMMENT '下次账单天数',
|
||||
`billing_date` VARCHAR(50) COMMENT '账单日期',
|
||||
`payment_card_number` VARCHAR(20) COMMENT '支付卡号',
|
||||
`payment_country` VARCHAR(10) DEFAULT 'MO' COMMENT '支付国家代码',
|
||||
`status` ENUM('active', 'expired', 'error') DEFAULT 'active' COMMENT '账号状态',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
UNIQUE KEY `uk_email` (`email`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_registration_time` (`registration_time`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Windsurf 账号表';
|
||||
|
||||
-- 插入示例数据(可选)
|
||||
-- INSERT INTO `windsurf_accounts` (email, password, first_name, last_name, quota_total)
|
||||
-- VALUES ('test@example.com', 'Password123!', 'John', 'Doe', 600.00);
|
||||
Loading…
Reference in New Issue
Block a user