Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74770fb3dd | ||
|
|
8fc5a04903 | ||
|
|
92f7c0f3e0 | ||
|
|
85c3095e98 | ||
|
|
69bf430525 | ||
|
|
de057ae183 | ||
|
|
901c8c95e1 | ||
|
|
9d00c0b58e | ||
|
|
5919b7e657 | ||
|
|
d16f6bdc62 | ||
|
|
31fe73f998 | ||
|
|
221d7d79f1 | ||
|
|
6dad187156 | ||
|
|
058f172aba | ||
|
|
2390592bb2 | ||
|
|
6d618d36ac | ||
|
|
46ee69d2f8 | ||
|
|
067dfc84a1 | ||
|
|
b20094adfc | ||
|
|
7dad9f6b2f |
166
AUTO_SERVICE_README.md
Normal file
166
AUTO_SERVICE_README.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Cursor自动化服务使用说明
|
||||
|
||||
本自动化服务实现了Cursor注册流程的完全自动化,包括API状态监控、邮箱自动获取和账号上传功能。
|
||||
|
||||
## 功能特点
|
||||
|
||||
1. **API状态监控**
|
||||
- 实时监控注册API状态,自动开启/关闭注册进程
|
||||
- 支持配置检查间隔和自定义服务器标识(hostname)
|
||||
|
||||
2. **邮箱账号自动管理**
|
||||
- 监控本地邮箱池,当可用邮箱不足时自动从API获取
|
||||
- 支持配置获取阈值和批次数量
|
||||
|
||||
3. **账号自动上传**
|
||||
- 定期上传注册成功的账号到API
|
||||
- 支持配置上传间隔和代理设置
|
||||
- 通过extracted字段跟踪已上传账号
|
||||
|
||||
4. **进程管理**
|
||||
- 智能管理注册子进程的启动和停止
|
||||
- 在API关闭注册时自动停止进程
|
||||
|
||||
5. **完善的日志记录**
|
||||
- 详细的运行日志,支持日志轮转和压缩
|
||||
- 关键操作和错误的可追溯性
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. 已配置好MySQL数据库
|
||||
2. Redis服务(可选,但推荐)
|
||||
3. 已在config.yaml中配置好hostname参数
|
||||
4. 已安装所需依赖:`pip install -r requirements.txt`
|
||||
|
||||
## 配置说明
|
||||
|
||||
在`config.yaml`中添加以下配置:
|
||||
|
||||
```yaml
|
||||
# 服务器配置
|
||||
server_config:
|
||||
hostname: "sg424" # 服务器标识,用于API调用
|
||||
|
||||
# 代理配置
|
||||
proxy:
|
||||
# ... 其他代理配置 ...
|
||||
api_proxy: "http://your-proxy-server:port" # API专用代理(可选)
|
||||
|
||||
# 自动服务配置
|
||||
auto_service:
|
||||
check_interval: 60 # 检查API状态的间隔(秒)
|
||||
upload_interval: 300 # 上传账号间隔(秒)
|
||||
email_check_threshold: 30 # 当可用邮箱少于这个数时获取新邮箱
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. **初始化数据库**(首次使用)
|
||||
```bash
|
||||
python init_database.py
|
||||
```
|
||||
|
||||
2. **升级数据库**(现有数据库升级)
|
||||
```bash
|
||||
python upgrade_database.py
|
||||
```
|
||||
|
||||
> 注意:如果您从之前版本升级,需要运行此脚本添加新的`extracted`字段
|
||||
|
||||
3. **启动自动化服务**
|
||||
```bash
|
||||
python auto_cursor_service.py
|
||||
```
|
||||
|
||||
4. **查看运行状态**
|
||||
- 服务会在控制台输出基本运行信息
|
||||
- 详细日志保存在`auto_cursor_service.log`文件中
|
||||
|
||||
5. **停止服务**
|
||||
- 按Ctrl+C安全停止服务
|
||||
- 服务会自动停止注册进程并清理资源
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. 服务启动,初始化数据库连接
|
||||
2. 定期检查API注册状态(`/regstates`)
|
||||
3. 如果注册开启:
|
||||
- 检查本地邮箱池,如果数量不足,从API获取新邮箱
|
||||
- 启动注册进程(main.py)
|
||||
4. 如果注册关闭:
|
||||
- 停止注册进程
|
||||
5. 定期上传注册成功的账号,并标记为已上传(extracted=1)
|
||||
6. 循环以上步骤,直到服务被手动停止
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **API连接问题**
|
||||
- 检查网络连接和代理设置
|
||||
- 确认API URL和参数正确
|
||||
|
||||
2. **邮箱获取失败**
|
||||
- 检查API返回的错误信息
|
||||
- 可能是服务端暂无可用邮箱
|
||||
|
||||
3. **子进程管理问题**
|
||||
- Windows平台可能需要管理员权限来强制终止进程
|
||||
- 检查main.py是否能正常独立运行
|
||||
|
||||
4. **内存占用过高**
|
||||
- 调整日志设置,减少日志详细程度
|
||||
- 检查是否有内存泄漏
|
||||
|
||||
5. **数据库结构问题**
|
||||
- 如遇到"Unknown column 'extracted' in 'where clause'"错误
|
||||
- 运行`python upgrade_database.py`更新数据库结构
|
||||
|
||||
## 进阶使用
|
||||
|
||||
### 设置为系统服务(Linux)
|
||||
|
||||
1. 创建服务文件`/etc/systemd/system/cursor-service.service`:
|
||||
```
|
||||
[Unit]
|
||||
Description=Cursor Auto Registration Service
|
||||
After=network.target mysql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your_user
|
||||
WorkingDirectory=/path/to/cursor/app
|
||||
ExecStart=/usr/bin/python3 /path/to/cursor/app/auto_cursor_service.py
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
2. 重新加载systemd:
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
3. 启动服务:
|
||||
```bash
|
||||
sudo systemctl start cursor-service
|
||||
```
|
||||
|
||||
4. 设置开机自启:
|
||||
```bash
|
||||
sudo systemctl enable cursor-service
|
||||
```
|
||||
|
||||
### 使用screen或tmux(简易方法)
|
||||
|
||||
```bash
|
||||
# 使用screen
|
||||
screen -S cursor
|
||||
python auto_cursor_service.py
|
||||
# 按Ctrl+A然后按D分离screen
|
||||
|
||||
# 使用tmux
|
||||
tmux new -s cursor
|
||||
python auto_cursor_service.py
|
||||
# 按Ctrl+B然后按D分离tmux
|
||||
```
|
||||
284
DEPLOYMENT_README.md
Normal file
284
DEPLOYMENT_README.md
Normal file
@@ -0,0 +1,284 @@
|
||||
# Cursor自动化服务部署指南
|
||||
|
||||
本文档详细说明如何在新服务器上从零开始部署Cursor自动化服务,包括环境准备、数据库配置、服务启动等各个步骤。
|
||||
|
||||
## 系统要求
|
||||
|
||||
- **操作系统**: Linux (推荐Ubuntu/Debian),也支持Windows和macOS
|
||||
- **Python版本**: 3.7+
|
||||
- **数据库**: MySQL 5.7+ 或 MariaDB 10+
|
||||
- **缓存系统**: Redis 5+ (可选但推荐)
|
||||
- **内存**: 至少2GB RAM
|
||||
- **存储**: 至少1GB可用空间
|
||||
- **网络**: 稳定的互联网连接
|
||||
|
||||
## 部署流程概述
|
||||
|
||||
1. 安装必要的系统依赖
|
||||
2. 准备Python环境
|
||||
3. 下载代码并安装依赖
|
||||
4. 配置和初始化数据库
|
||||
5. 导入邮箱账号
|
||||
6. 启动服务
|
||||
7. 设置为系统服务(可选)
|
||||
|
||||
## 详细部署步骤
|
||||
|
||||
### 1. 安装必要的系统依赖
|
||||
|
||||
#### Ubuntu/Debian系统:
|
||||
|
||||
```bash
|
||||
# 更新系统包
|
||||
sudo apt update
|
||||
|
||||
# 安装Python、MySQL和Redis
|
||||
sudo apt install -y python3 python3-venv python3-full mysql-server redis-server
|
||||
|
||||
# 安装pip和开发库
|
||||
sudo apt install -y python3-pip python3-dev default-libmysqlclient-dev build-essential
|
||||
```
|
||||
|
||||
#### CentOS/RHEL系统:
|
||||
|
||||
```bash
|
||||
# 安装Python
|
||||
sudo yum install -y python3 python3-devel
|
||||
|
||||
# 安装MySQL
|
||||
sudo yum install -y mariadb-server mariadb-devel
|
||||
|
||||
# 安装Redis
|
||||
sudo yum install -y redis
|
||||
|
||||
# 启动服务
|
||||
sudo systemctl start mariadb
|
||||
sudo systemctl start redis
|
||||
sudo systemctl enable mariadb
|
||||
sudo systemctl enable redis
|
||||
```
|
||||
|
||||
### 2. 准备Python环境
|
||||
|
||||
```bash
|
||||
# 创建项目目录
|
||||
mkdir -p /opt/cursor-service
|
||||
cd /opt/cursor-service
|
||||
|
||||
sudo apt install python3-venv python3-full
|
||||
# 创建并激活虚拟环境
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
### 3. 下载代码并安装依赖
|
||||
|
||||
```bash
|
||||
# 下载代码(示例使用git,也可以直接上传文件)
|
||||
git clone https://your-repo-url.git .
|
||||
# 或者直接解压上传的zip文件
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
pip install cryptography # 确保安装cryptography包
|
||||
```
|
||||
|
||||
### 4. 配置和初始化数据库
|
||||
|
||||
我们提供了两种初始化数据库的方式:
|
||||
|
||||
#### 方式一:使用交互式设置向导(推荐)
|
||||
|
||||
```bash
|
||||
# 修复可能的MySQL格式化问题
|
||||
python fix_setup.py
|
||||
|
||||
# 运行设置向导
|
||||
python setup_environment.py
|
||||
```
|
||||
|
||||
设置向导将引导您完成:
|
||||
- 配置服务器标识
|
||||
- 设置MySQL数据库连接信息
|
||||
- 配置Redis(可选)
|
||||
- 创建数据库和表结构
|
||||
|
||||
#### 方式二:手动执行初始化脚本
|
||||
|
||||
```bash
|
||||
# 直接运行数据库初始化脚本
|
||||
python init_database.py
|
||||
```
|
||||
|
||||
脚本会交互式地询问:
|
||||
- MySQL root用户名和密码
|
||||
- 应用数据库名和用户信息
|
||||
- Redis配置(可选)
|
||||
|
||||
### 5. 导入邮箱账号
|
||||
|
||||
```bash
|
||||
# 准备email.txt文件,格式为:
|
||||
# email@example.com----密码----client_id----refresh_token
|
||||
|
||||
# 运行导入脚本
|
||||
python import_emails.py
|
||||
```
|
||||
|
||||
### 6. 启动服务
|
||||
|
||||
有多种方式可以启动服务:
|
||||
|
||||
#### 方式一:使用统一管理脚本(推荐)
|
||||
|
||||
```bash
|
||||
# 启用并启动服务
|
||||
python start.py --enable
|
||||
|
||||
# 或者直接启动(会根据数据库中的启用状态决定是否启动服务)
|
||||
python start.py
|
||||
```
|
||||
|
||||
#### 方式二:直接启动自动服务
|
||||
|
||||
```bash
|
||||
# 直接启动自动化服务
|
||||
python auto_cursor_service.py
|
||||
```
|
||||
|
||||
#### 方式三:仅运行注册进程
|
||||
|
||||
```bash
|
||||
# 仅启动注册进程
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 7. 设置为系统服务(可选但推荐)
|
||||
|
||||
#### 创建系统服务文件:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/cursor-service.service
|
||||
```
|
||||
|
||||
添加以下内容:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Cursor Auto Registration Service
|
||||
After=network.target mysql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your_username
|
||||
WorkingDirectory=/opt/cursor-service
|
||||
ExecStart=/opt/cursor-service/venv/bin/python start.py
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
#### 启动并设置开机自启:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl start cursor-service
|
||||
sudo systemctl enable cursor-service
|
||||
```
|
||||
|
||||
## 管理服务
|
||||
|
||||
### 检查服务状态
|
||||
|
||||
```bash
|
||||
# 查看服务状态
|
||||
python start.py --status
|
||||
|
||||
# 如果设置为系统服务,可以使用
|
||||
sudo systemctl status cursor-service
|
||||
```
|
||||
|
||||
### 启用/禁用服务
|
||||
|
||||
```bash
|
||||
# 启用服务
|
||||
python start.py --enable
|
||||
|
||||
# 禁用服务
|
||||
python start.py --disable
|
||||
```
|
||||
|
||||
### 手动上传账号
|
||||
|
||||
```bash
|
||||
python start.py --upload
|
||||
```
|
||||
|
||||
## 数据库升级
|
||||
|
||||
如果您从旧版本升级,需要运行数据库升级脚本:
|
||||
|
||||
```bash
|
||||
python upgrade_database.py
|
||||
```
|
||||
|
||||
这会添加新的`extracted`字段和相关索引,以支持账号上传功能。
|
||||
|
||||
## 监控与故障排除
|
||||
|
||||
### 日志文件
|
||||
|
||||
主要日志文件:
|
||||
- `auto_cursor_service.log` - 自动服务日志
|
||||
- `upload_accounts.log` - 账号上传日志
|
||||
- `register.log` - 注册进程日志
|
||||
|
||||
### 常见问题解决
|
||||
|
||||
1. **数据库连接失败**
|
||||
- 检查MySQL服务是否运行: `sudo systemctl status mysql`
|
||||
- 验证数据库用户名和密码是否正确
|
||||
|
||||
2. **Redis连接失败**
|
||||
- 检查Redis服务是否运行: `sudo systemctl status redis`
|
||||
- 如不需要Redis,可在config.yaml中设置`use_redis: false`
|
||||
|
||||
3. **服务无法启动**
|
||||
- 检查日志文件中的具体错误信息
|
||||
- 确保所有依赖包都已安装: `pip install -r requirements.txt`
|
||||
|
||||
4. **账号注册失败率高**
|
||||
- 检查网络连接和代理设置
|
||||
- 查看注册日志中的具体错误信息
|
||||
|
||||
5. **邮箱账号不足**
|
||||
- 手动导入更多邮箱账号: `python import_emails.py`
|
||||
- 检查API获取邮箱是否正常
|
||||
|
||||
## 备份与维护
|
||||
|
||||
建议定期备份数据库:
|
||||
|
||||
```bash
|
||||
# MySQL备份
|
||||
mysqldump -u [用户名] -p [数据库名] > backup_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
## 安全考虑
|
||||
|
||||
1. 使用强密码保护数据库
|
||||
2. 限制MySQL和Redis只接受本地连接
|
||||
3. 保护config.yaml文件,不要泄露API密钥和数据库密码
|
||||
4. 定期更新系统和依赖包
|
||||
|
||||
## 更新服务
|
||||
|
||||
更新服务代码后,请按以下步骤操作:
|
||||
|
||||
1. 停止服务: `python start.py --disable` 或 `sudo systemctl stop cursor-service`
|
||||
2. 更新代码
|
||||
3. 如有新的依赖,安装: `pip install -r requirements.txt`
|
||||
4. 如有数据库结构变更,运行升级脚本: `python upgrade_database.py`
|
||||
5. 重新启动服务: `python start.py --enable` 或 `sudo systemctl start cursor-service`
|
||||
83
IMPORT_README.md
Normal file
83
IMPORT_README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# 邮箱导入工具使用说明
|
||||
|
||||
这个导入工具用于将邮箱账号导入到MySQL数据库中,支持Redis缓存。
|
||||
|
||||
## 功能特点
|
||||
|
||||
1. 支持MySQL数据库存储
|
||||
2. 可选启用Redis缓存
|
||||
3. 详细的导入日志
|
||||
4. 自动处理重复邮箱
|
||||
5. Windows平台兼容性优化
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. MySQL/MariaDB数据库服务已运行
|
||||
2. 已在`config.yaml`中配置好数据库连接信息
|
||||
3. Redis服务(可选)
|
||||
|
||||
## 邮箱数据格式
|
||||
|
||||
邮箱数据文件应使用以下格式,每行一个账号:
|
||||
|
||||
```
|
||||
email@example.com----密码----client_id----refresh_token
|
||||
```
|
||||
|
||||
字段说明:
|
||||
- `email`: 邮箱地址
|
||||
- `password`: 邮箱密码
|
||||
- `client_id`: Microsoft应用的客户端ID
|
||||
- `refresh_token`: Microsoft OAuth的刷新令牌
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 确保MySQL数据库已正确配置
|
||||
|
||||
编辑`config.yaml`文件,设置正确的MySQL连接信息:
|
||||
```yaml
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 3306
|
||||
username: "auto_cursor_reg"
|
||||
password: "your_password"
|
||||
database: "auto_cursor_reg"
|
||||
```
|
||||
|
||||
2. 准备邮箱数据文件
|
||||
|
||||
默认读取`email.txt`文件,也可以在`config.yaml`中指定:
|
||||
```yaml
|
||||
email:
|
||||
file_path: "path/to/your/email_file.txt"
|
||||
```
|
||||
|
||||
3. 运行导入工具
|
||||
|
||||
```bash
|
||||
python import_emails.py
|
||||
```
|
||||
|
||||
4. 查看导入结果
|
||||
|
||||
导入过程和结果会显示在控制台,详细日志保存在`import_emails.log`文件中。
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **无法连接数据库**
|
||||
- 检查MySQL服务是否启动
|
||||
- 确认用户名和密码正确
|
||||
- 确认数据库名称存在
|
||||
|
||||
2. **导入失败**
|
||||
- 检查邮箱数据文件格式是否正确
|
||||
- 查看导入日志获取详细错误信息
|
||||
|
||||
3. **重复邮箱处理**
|
||||
- 系统会自动跳过重复的邮箱,并在日志中标记
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 导入前建议备份原有数据
|
||||
- 大批量导入时,建议适当增加MySQL的连接超时设置
|
||||
- 导入成功后可以运行主程序开始注册流程
|
||||
91
INIT_README.md
Normal file
91
INIT_README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 数据库初始化工具使用说明
|
||||
|
||||
这个初始化工具用于自动配置MySQL数据库和Redis(可选),创建所需的表结构,并更新配置文件。此工具适用于服务器端首次部署时的快速配置。
|
||||
|
||||
## 功能特点
|
||||
|
||||
1. 交互式配置MySQL数据库和用户
|
||||
2. 自动创建所需的表结构
|
||||
3. 可选配置Redis缓存
|
||||
4. 自动更新config.yaml配置文件
|
||||
5. 详细的操作日志
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. 已安装MySQL/MariaDB服务
|
||||
2. 已安装Redis服务(可选)
|
||||
3. 知道MySQL root用户密码
|
||||
4. 安装必要的Python依赖:`pip install -r requirements.txt`
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 安装必要的依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. 运行初始化脚本
|
||||
|
||||
```bash
|
||||
python init_database.py
|
||||
```
|
||||
|
||||
3. 根据提示输入信息
|
||||
- MySQL root用户名和密码
|
||||
- 应用程序数据库和用户设置
|
||||
- Redis配置(可选)
|
||||
|
||||
4. 初始化完成后,脚本会:
|
||||
- 创建数据库和用户
|
||||
- 设置适当的权限
|
||||
- 创建必要的表结构
|
||||
- 更新配置文件
|
||||
|
||||
## 配置选项说明
|
||||
|
||||
### MySQL配置
|
||||
- **主机地址**: MySQL服务器地址,默认为`localhost`
|
||||
- **端口**: MySQL服务端口,默认为`3306`
|
||||
- **Root用户**: 有权限创建数据库和用户的MySQL管理员账号
|
||||
- **数据库名**: 应用程序使用的数据库名,默认为`auto_cursor_reg`
|
||||
- **应用用户名**: 应用程序使用的数据库用户,默认为`auto_cursor_reg`
|
||||
|
||||
### Redis配置
|
||||
- **是否启用**: 是否使用Redis缓存
|
||||
- **主机地址**: Redis服务器地址,默认为`127.0.0.1`
|
||||
- **端口**: Redis服务端口,默认为`6379`
|
||||
- **密码**: Redis认证密码(如果设置了)
|
||||
- **数据库索引**: Redis数据库索引,默认为`0`
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **无法连接到MySQL**
|
||||
- 确认MySQL服务已启动
|
||||
- 验证root密码是否正确
|
||||
- 检查防火墙设置
|
||||
|
||||
2. **无法连接到Redis**
|
||||
- 确认Redis服务已启动
|
||||
- 验证Redis密码是否正确
|
||||
- 如不需要Redis可选择禁用
|
||||
|
||||
3. **权限问题**
|
||||
- 确保使用的用户有创建数据库和用户的权限
|
||||
|
||||
4. **配置文件备份**
|
||||
- 脚本会自动备份原始配置文件为`config.yaml.bak`
|
||||
|
||||
## 完成后的步骤
|
||||
|
||||
初始化完成后,您可以:
|
||||
|
||||
1. 导入邮箱账号:
|
||||
```bash
|
||||
python import_emails.py
|
||||
```
|
||||
|
||||
2. 运行主程序:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
107
MYSQL_README.md
Normal file
107
MYSQL_README.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Cursor注册工具 - MySQL & Redis支持
|
||||
|
||||
本文档说明了如何将Cursor注册工具从SQLite数据库迁移到MySQL数据库,并可选地启用Redis缓存以提高性能。
|
||||
|
||||
## 变更内容
|
||||
|
||||
1. 数据库后端从SQLite更换为MySQL
|
||||
2. 可选启用Redis缓存
|
||||
3. 提供数据迁移脚本
|
||||
4. 优化数据库查询性能
|
||||
5. 增加数据库连接池管理
|
||||
|
||||
## 前置要求
|
||||
|
||||
1. Python 3.7+
|
||||
2. MySQL/MariaDB 服务器
|
||||
3. Redis服务器 (可选)
|
||||
4. 安装依赖:`pip install -r requirements.txt`
|
||||
|
||||
## 配置说明
|
||||
|
||||
在`config.yaml`中添加了MySQL和Redis相关配置:
|
||||
|
||||
```yaml
|
||||
# 数据库配置
|
||||
database:
|
||||
# SQLite配置(兼容旧版本)
|
||||
path: "cursor.db"
|
||||
pool_size: 10
|
||||
|
||||
# MySQL配置
|
||||
host: "localhost"
|
||||
port: 3306
|
||||
username: "root"
|
||||
password: ""
|
||||
database: "cursor_register"
|
||||
|
||||
# 是否使用Redis缓存
|
||||
use_redis: true
|
||||
|
||||
# Redis配置(可选,当use_redis为true时生效)
|
||||
redis:
|
||||
host: "127.0.0.1"
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
```
|
||||
|
||||
## 数据迁移步骤
|
||||
|
||||
1. 确保MySQL服务器已启动,并已创建好数据库
|
||||
2. 更新`config.yaml`配置文件,设置正确的数据库连接信息
|
||||
3. 运行迁移脚本:`python migrate_db.py`
|
||||
4. 迁移脚本会自动创建表结构并将旧数据导入MySQL
|
||||
|
||||
## 使用Redis缓存
|
||||
|
||||
若要启用Redis缓存以提高性能:
|
||||
|
||||
1. 安装Redis服务器:
|
||||
- Windows: 使用WSL或[Windows版Redis](https://github.com/microsoftarchive/redis/releases)
|
||||
- Linux: `sudo apt install redis-server` (Ubuntu) 或 `sudo yum install redis` (CentOS)
|
||||
- macOS: `brew install redis`
|
||||
|
||||
2. 在`config.yaml`中设置`use_redis: true`并配置Redis连接信息
|
||||
3. 确保安装了`aioredis`包:`pip install aioredis>=2.0.0`
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **无法连接到MySQL**
|
||||
- 确认MySQL服务已启动
|
||||
- 检查用户名和密码是否正确
|
||||
- 确认数据库是否已创建
|
||||
- 检查防火墙设置
|
||||
|
||||
2. **无法连接到Redis**
|
||||
- 确认Redis服务已启动
|
||||
- 检查端口和密码设置
|
||||
- 如不需要Redis,可设置`use_redis: false`
|
||||
|
||||
3. **迁移失败**
|
||||
- 检查原SQLite数据库文件是否存在且有效
|
||||
- 确认MySQL用户有创建表和写入数据的权限
|
||||
|
||||
## 性能调优
|
||||
|
||||
1. 优化MySQL配置:
|
||||
```ini
|
||||
[mysqld]
|
||||
innodb_buffer_pool_size = 128M
|
||||
innodb_log_file_size = 32M
|
||||
max_connections = 100
|
||||
```
|
||||
|
||||
2. 优化Redis配置:
|
||||
```
|
||||
maxmemory 128mb
|
||||
maxmemory-policy allkeys-lru
|
||||
```
|
||||
|
||||
3. 优化连接池大小:根据并发需要调整`pool_size`参数
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 请确保定期备份MySQL数据库
|
||||
- Redis仅用于缓存,断电或重启会丢失缓存数据
|
||||
- 如在多机部署,需确保时区配置一致
|
||||
137
SETUP_README.md
Normal file
137
SETUP_README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Cursor自动化服务 - 环境设置向导
|
||||
|
||||
这个交互式设置向导(`setup_environment.py`)用于在新服务器上快速配置Cursor自动化服务环境,包括安装依赖、配置数据库和Redis、创建必要的表结构。
|
||||
|
||||
## 功能特点
|
||||
|
||||
1. **全自动化配置**
|
||||
- 自动检测并安装必要的Python依赖
|
||||
- 交互式配置MySQL和Redis
|
||||
- 自动创建数据库和用户
|
||||
- 创建必要的表结构
|
||||
- 更新配置文件
|
||||
|
||||
2. **智能默认值**
|
||||
- 使用主机名作为默认服务器标识
|
||||
- 识别现有配置并提供作为默认值
|
||||
- 为重要参数提供安全的建议值
|
||||
|
||||
3. **连接性测试**
|
||||
- 验证MySQL连接是否可用
|
||||
- 测试Redis连接
|
||||
- 提供详细的错误反馈
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 步骤1: 准备环境
|
||||
|
||||
确保服务器上已安装:
|
||||
- Python 3.7+
|
||||
- MySQL/MariaDB
|
||||
- Redis (可选)
|
||||
|
||||
### 步骤2: 运行设置向导
|
||||
|
||||
```bash
|
||||
python setup_environment.py
|
||||
```
|
||||
|
||||
### 步骤3: 按照提示完成配置
|
||||
|
||||
设置向导将引导您完成以下步骤:
|
||||
|
||||
1. **检查并安装依赖**
|
||||
- 自动安装必要的Python包
|
||||
|
||||
2. **配置服务器标识**
|
||||
- 设置唯一的服务器标识,用于API调用
|
||||
|
||||
3. **配置MySQL数据库**
|
||||
- 输入MySQL服务器信息
|
||||
- 提供MySQL root用户信息(用于创建数据库和用户)
|
||||
- 设置应用程序数据库和用户
|
||||
|
||||
4. **配置Redis缓存(可选)**
|
||||
- 选择是否使用Redis缓存
|
||||
- 配置Redis连接信息
|
||||
|
||||
5. **创建数据库和用户**
|
||||
- 自动创建数据库
|
||||
- 创建应用用户并设置权限
|
||||
|
||||
6. **创建表结构**
|
||||
- 创建email_accounts表
|
||||
- 创建system_settings表
|
||||
|
||||
7. **更新配置文件**
|
||||
- 备份现有配置
|
||||
- 写入新配置到config.yaml
|
||||
|
||||
## 配置选项说明
|
||||
|
||||
### 服务器标识
|
||||
服务器标识是一个唯一的名称,用于在API调用中标识当前服务器。默认使用系统主机名。
|
||||
|
||||
### MySQL配置
|
||||
- **主机地址**: MySQL服务器地址,默认为`localhost`
|
||||
- **端口**: MySQL服务端口,默认为`3306`
|
||||
- **数据库名**: 应用程序使用的数据库名,默认为`auto_cursor_reg`
|
||||
- **应用用户名**: 应用程序使用的数据库用户,默认为`auto_cursor_reg`
|
||||
- **应用用户密码**: 默认生成一个安全的随机密码
|
||||
|
||||
### Redis配置
|
||||
- **是否启用**: 是否使用Redis缓存
|
||||
- **主机地址**: Redis服务器地址,默认为`127.0.0.1`
|
||||
- **端口**: Redis服务端口,默认为`6379`
|
||||
- **密码**: Redis认证密码(如果设置了)
|
||||
- **数据库索引**: Redis数据库索引,默认为`0`
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
- 本脚本需要MySQL root权限来创建数据库和用户
|
||||
- 数据库密码会以明文保存在配置文件中
|
||||
- 脚本会自动备份现有配置文件,以`.bak`扩展名保存
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **无法连接到MySQL**
|
||||
- 确认MySQL服务已启动
|
||||
- 验证root密码是否正确
|
||||
- 检查防火墙设置
|
||||
|
||||
2. **无法连接到Redis**
|
||||
- 确认Redis服务已启动
|
||||
- 验证Redis密码是否正确
|
||||
- 如不需要Redis可选择禁用
|
||||
|
||||
3. **权限问题**
|
||||
- 确保使用的用户有创建数据库和用户的权限
|
||||
|
||||
4. **配置文件备份**
|
||||
- 脚本会自动备份原始配置文件为`config.yaml.bak.[时间戳]`
|
||||
|
||||
## 完成后的步骤
|
||||
|
||||
设置完成后,您可以:
|
||||
|
||||
1. 导入邮箱账号:
|
||||
```bash
|
||||
python import_emails.py
|
||||
```
|
||||
|
||||
2. 启动自动服务:
|
||||
```bash
|
||||
python start.py
|
||||
```
|
||||
|
||||
3. 或直接启动自动化服务:
|
||||
```bash
|
||||
python auto_cursor_service.py
|
||||
```
|
||||
|
||||
## 服务器系统要求
|
||||
|
||||
- **操作系统**: 支持Linux、Windows、macOS
|
||||
- **内存**: 至少2GB RAM
|
||||
- **存储**: 至少1GB可用空间
|
||||
- **网络**: 稳定的互联网连接
|
||||
118
START_README.md
Normal file
118
START_README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Cursor自动化服务统一启动脚本
|
||||
|
||||
这个统一启动脚本(`start.py`)用于集中管理Cursor注册的各项功能,包括注册状态检查、账号上传和自动化服务。
|
||||
|
||||
## 功能特点
|
||||
|
||||
1. **集中管理**
|
||||
- 通过数据库标记控制自动服务的启停
|
||||
- 提供统一的命令行接口管理各项功能
|
||||
|
||||
2. **灵活操作**
|
||||
- 可以单独启动注册进程
|
||||
- 可以执行单次账号上传
|
||||
- 可以管理自动化服务的状态
|
||||
|
||||
3. **简化部署**
|
||||
- 无需手动启动多个脚本
|
||||
- 支持作为服务自动启动
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本用法
|
||||
|
||||
不带参数启动时,脚本会检查数据库中的启动标记,并据此启动或停止自动服务:
|
||||
|
||||
```bash
|
||||
python start.py
|
||||
```
|
||||
|
||||
### 启用自动服务
|
||||
|
||||
设置启动标记并启动自动服务:
|
||||
|
||||
```bash
|
||||
python start.py --enable
|
||||
```
|
||||
|
||||
### 禁用自动服务
|
||||
|
||||
清除启动标记并停止自动服务:
|
||||
|
||||
```bash
|
||||
python start.py --disable
|
||||
```
|
||||
|
||||
### 查看服务状态
|
||||
|
||||
查看当前自动服务的状态:
|
||||
|
||||
```bash
|
||||
python start.py --status
|
||||
```
|
||||
|
||||
### 手动上传账号
|
||||
|
||||
执行一次账号上传操作,不影响自动服务:
|
||||
|
||||
```bash
|
||||
python start.py --upload
|
||||
```
|
||||
|
||||
### 仅运行注册进程
|
||||
|
||||
直接启动注册进程,不启动完整的自动服务:
|
||||
|
||||
```bash
|
||||
python start.py --register
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. 脚本使用数据库中的`system_settings`表存储自动服务的启用状态
|
||||
2. 当状态为启用时,脚本会自动启动`auto_cursor_service.py`
|
||||
3. 自动服务会负责检查API状态、获取邮箱和上传账号等操作
|
||||
4. 如果只需单独功能,可以使用相应的命令行参数
|
||||
|
||||
## 设置为系统服务
|
||||
|
||||
### Linux (systemd)
|
||||
|
||||
创建服务文件`/etc/systemd/system/cursor-service.service`:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Cursor Auto Registration Service
|
||||
After=network.target mysql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your_user
|
||||
WorkingDirectory=/path/to/cursor/app
|
||||
ExecStart=/usr/bin/python3 /path/to/cursor/app/start.py
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
启动服务:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl start cursor-service
|
||||
sudo systemctl enable cursor-service
|
||||
```
|
||||
|
||||
### Windows (开机自启)
|
||||
|
||||
创建批处理文件`start_cursor.bat`:
|
||||
|
||||
```bat
|
||||
@echo off
|
||||
cd /d "C:\path\to\cursor\app"
|
||||
python start.py
|
||||
```
|
||||
|
||||
将此批处理文件添加到启动文件夹。
|
||||
55
UPGRADE_DATABASE_README.md
Normal file
55
UPGRADE_DATABASE_README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# 数据库升级指南
|
||||
|
||||
本文档说明如何使用 `upgrade_database.py` 脚本升级现有数据库结构,添加新的 `extracted` 字段和相关索引。
|
||||
|
||||
## 升级目的
|
||||
|
||||
新版本的 Cursor 自动化服务增加了账号上传功能,需要在数据库中添加 `extracted` 字段来标记已上传的账号。此脚本会自动完成以下操作:
|
||||
|
||||
1. 检查并添加 `extracted` 布尔字段(默认为 0)
|
||||
2. 创建 `idx_extracted` 索引以优化查询性能
|
||||
3. 将现有的成功注册账号的 `extracted` 状态设为 0(未提取)
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 确保 MySQL 数据库正在运行,且 `config.yaml` 中的数据库配置正确
|
||||
|
||||
2. 运行升级脚本:
|
||||
```bash
|
||||
python upgrade_database.py
|
||||
```
|
||||
|
||||
3. 脚本将自动检测是否需要添加字段和索引,并仅在需要时执行相应的 SQL 语句
|
||||
|
||||
4. 查看输出日志确认升级结果,详细日志保存在 `upgrade_database.log` 文件中
|
||||
|
||||
## 安全考虑
|
||||
|
||||
- 此脚本会修改数据库结构,但不会删除任何数据
|
||||
- 建议在执行前备份数据库
|
||||
- 脚本执行前会检查字段和索引是否已存在,不会重复创建
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **权限错误**
|
||||
- 确保数据库用户有 ALTER TABLE 权限
|
||||
- 错误信息通常包含 "Access denied" 或 "Insufficient privileges"
|
||||
|
||||
2. **字段已存在**
|
||||
- 如果收到 "Duplicate column name" 错误,说明字段已存在
|
||||
- 脚本会检测字段是否存在,通常不会出现此问题
|
||||
|
||||
3. **执行成功但无更改**
|
||||
- 如果日志显示 "extracted字段已存在,无需添加",说明数据库结构已是最新
|
||||
- 这是正常现象,无需担心
|
||||
|
||||
## 验证升级
|
||||
|
||||
升级完成后,可以通过以下SQL语句验证新字段是否添加成功:
|
||||
|
||||
```sql
|
||||
DESCRIBE email_accounts;
|
||||
SHOW INDEX FROM email_accounts;
|
||||
```
|
||||
|
||||
应该能看到 `extracted` 字段和 `idx_extracted` 索引。
|
||||
449
auto_cursor_service.py
Normal file
449
auto_cursor_service.py
Normal file
@@ -0,0 +1,449 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cursor自动化服务
|
||||
- 监控API,控制注册进程
|
||||
- 自动获取邮箱账号
|
||||
- 自动上传注册成功的账号
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import json
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
|
||||
# Windows平台特殊处理,强制使用SelectorEventLoop
|
||||
if sys.platform.startswith("win"):
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
from core.config import Config
|
||||
from core.database import DatabaseManager
|
||||
from services.fetch_manager import FetchManager
|
||||
from services.self_hosted_email import SelfHostedEmail
|
||||
from services.proxy_pool import ProxyPool
|
||||
|
||||
|
||||
class AutoCursorService:
|
||||
def __init__(self):
|
||||
self.config = Config.from_yaml()
|
||||
self.db_manager = DatabaseManager(self.config)
|
||||
self.fetch_manager = FetchManager(self.config)
|
||||
|
||||
# API相关
|
||||
self.api_base_url = "https://cursorapi.nosqli.com/admin/api.AutoCursor"
|
||||
self.proxy = None
|
||||
|
||||
# 初始化自建邮箱服务
|
||||
self.self_hosted_email = None
|
||||
if hasattr(self.config, 'self_hosted_email_config') and self.config.self_hosted_email_config:
|
||||
self.self_hosted_email = SelfHostedEmail(
|
||||
self.fetch_manager,
|
||||
self.config.self_hosted_email_config.api_base_url,
|
||||
self.config.self_hosted_email_config.api_key
|
||||
)
|
||||
logger.info("自建邮箱服务已初始化")
|
||||
else:
|
||||
logger.warning("未配置自建邮箱服务,部分功能可能不可用")
|
||||
|
||||
# 初始化代理池
|
||||
self.proxy_pool = ProxyPool(self.config, self.fetch_manager)
|
||||
|
||||
# 获取hostname,用于API请求参数
|
||||
self.hostname = getattr(self.config, "hostname", None)
|
||||
if not self.hostname:
|
||||
# 尝试从config.yaml中的server_config获取
|
||||
server_config = getattr(self.config, "server_config", None)
|
||||
if server_config:
|
||||
self.hostname = getattr(server_config, "hostname", "unknown")
|
||||
else:
|
||||
self.hostname = "unknown"
|
||||
logger.warning(f"未在配置中找到hostname,使用默认值: {self.hostname}")
|
||||
|
||||
# 子进程控制
|
||||
self.registration_process = None
|
||||
self.reg_enabled = False
|
||||
|
||||
# 从配置中获取自动服务参数
|
||||
auto_service_config = getattr(self.config, "auto_service_config", None)
|
||||
if auto_service_config:
|
||||
self.check_interval = getattr(auto_service_config, "check_interval", 60)
|
||||
self.upload_interval = getattr(auto_service_config, "upload_interval", 300)
|
||||
self.email_check_threshold = getattr(auto_service_config, "email_check_threshold", 30)
|
||||
self.email_batch_size = getattr(auto_service_config, "email_batch_size", 10)
|
||||
else:
|
||||
self.check_interval = 60 # 检查API状态的间隔(秒)
|
||||
self.upload_interval = 300 # 上传账号间隔(秒)
|
||||
self.email_check_threshold = 30 # 当可用邮箱少于这个数时获取新邮箱
|
||||
self.email_batch_size = 10 # 每次获取邮箱的数量
|
||||
|
||||
# 处理邮箱的最小数量阈值,只要有这么多邮箱就会立即处理
|
||||
self.min_email_to_process = 1
|
||||
|
||||
# 运行状态控制
|
||||
self.running = True
|
||||
|
||||
# 设置信号处理
|
||||
signal.signal(signal.SIGINT, self._handle_signal)
|
||||
signal.signal(signal.SIGTERM, self._handle_signal)
|
||||
|
||||
def _handle_signal(self, signum, frame):
|
||||
"""处理信号,优雅关闭服务"""
|
||||
logger.info(f"收到信号 {signum},准备关闭服务")
|
||||
self.running = False
|
||||
|
||||
# 确保这不是在异步上下文中调用的
|
||||
if self.registration_process:
|
||||
logger.info("正在终止注册进程...")
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.registration_process.pid)])
|
||||
else:
|
||||
self.registration_process.terminate()
|
||||
except Exception as e:
|
||||
logger.error(f"终止注册进程时出错: {e}")
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化服务"""
|
||||
logger.info("初始化自动化服务")
|
||||
await self.db_manager.initialize()
|
||||
|
||||
# 检查并设置代理
|
||||
if hasattr(self.config, "proxy_config") and self.config.proxy_config:
|
||||
if hasattr(self.config.proxy_config, "api_proxy") and self.config.proxy_config.api_proxy:
|
||||
self.proxy = self.config.proxy_config.api_proxy
|
||||
logger.info(f"使用API代理: {self.proxy}")
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理资源"""
|
||||
logger.info("清理服务资源")
|
||||
if self.registration_process and self.registration_process.poll() is None:
|
||||
logger.info("终止注册进程")
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.registration_process.pid)])
|
||||
else:
|
||||
self.registration_process.terminate()
|
||||
self.registration_process.wait(timeout=5)
|
||||
except Exception as e:
|
||||
logger.error(f"终止注册进程时出错: {e}")
|
||||
|
||||
await self.db_manager.cleanup()
|
||||
|
||||
async def check_registration_status(self) -> bool:
|
||||
"""检查注册API是否开启
|
||||
|
||||
Returns:
|
||||
bool: True表示开启注册,False表示关闭注册
|
||||
"""
|
||||
url = f"{self.api_base_url}/regstates"
|
||||
params = {"hostname": self.hostname}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params, proxy=self.proxy, ssl=False) as response:
|
||||
if response.status != 200:
|
||||
logger.error(f"API请求失败,状态码: {response.status}")
|
||||
return self.reg_enabled # 保持当前状态
|
||||
|
||||
data = await response.json()
|
||||
if data.get("code") != 0:
|
||||
logger.error(f"API返回错误: {data.get('msg', 'Unknown error')}")
|
||||
return self.reg_enabled # 保持当前状态
|
||||
|
||||
reg_state = data.get("data", {}).get("reg_state", False)
|
||||
logger.info(f"注册状态: {'开启' if reg_state else '关闭'}")
|
||||
return reg_state
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"检查注册状态时出错: {e}")
|
||||
return self.reg_enabled # 出错时保持当前状态
|
||||
|
||||
async def fetch_email_accounts(self, count: int = 10) -> List[Dict[str, str]]:
|
||||
"""使用自建邮箱服务获取邮箱账号
|
||||
|
||||
Args:
|
||||
count: 需要获取的邮箱数量
|
||||
|
||||
Returns:
|
||||
List[Dict[str, str]]: 邮箱账号列表,每个账号包含email和password字段
|
||||
"""
|
||||
if not self.self_hosted_email:
|
||||
logger.error("自建邮箱服务未初始化,无法获取邮箱")
|
||||
return []
|
||||
|
||||
result = []
|
||||
for _ in range(count):
|
||||
try:
|
||||
# 获取一个邮箱地址
|
||||
email = await self.self_hosted_email.get_email()
|
||||
if not email:
|
||||
logger.warning("获取邮箱失败,跳过")
|
||||
continue
|
||||
|
||||
# 生成随机密码
|
||||
password = ''.join(random.choices(string.ascii_letters + string.digits, k=12))
|
||||
|
||||
# 添加到结果列表
|
||||
account = {
|
||||
"email": email,
|
||||
"password": password,
|
||||
"client_id": "", # 如果有需要可以生成
|
||||
"refresh_token": "" # 如果有需要可以生成
|
||||
}
|
||||
result.append(account)
|
||||
logger.info(f"已获取邮箱: {email}")
|
||||
|
||||
# 每次获取后等待一小段时间,避免请求过快
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取邮箱时出错: {e}")
|
||||
await asyncio.sleep(1) # 出错后等待较长时间
|
||||
|
||||
logger.info(f"本次共获取 {len(result)} 个邮箱账号")
|
||||
return result
|
||||
|
||||
async def import_self_hosted_emails(self, count: int = 10) -> List[Dict[str, str]]:
|
||||
"""获取并导入自建邮箱
|
||||
|
||||
Args:
|
||||
count: 要获取的邮箱数量
|
||||
|
||||
Returns:
|
||||
List[Dict[str, str]]: 获取的邮箱列表
|
||||
"""
|
||||
# 直接获取邮箱列表,不再需要导入到数据库
|
||||
emails = await self.fetch_email_accounts(count)
|
||||
return emails
|
||||
|
||||
async def start_registration_process(self):
|
||||
"""启动注册进程"""
|
||||
# 如果注册进程已在运行,不做任何事
|
||||
if self.registration_process and self.registration_process.poll() is None:
|
||||
logger.info("注册进程已在运行中")
|
||||
return
|
||||
|
||||
logger.info("启动注册进程")
|
||||
try:
|
||||
# 获取配置中的batch_size,确保并发注册
|
||||
batch_size = 1 # 默认值
|
||||
if hasattr(self.config, "register_config") and self.config.register_config:
|
||||
batch_size = getattr(self.config.register_config, "batch_size", 1)
|
||||
|
||||
logger.info(f"注册批次大小设置为: {batch_size}")
|
||||
|
||||
# 使用subprocess启动main.py
|
||||
if sys.platform.startswith("win"):
|
||||
self.registration_process = subprocess.Popen(
|
||||
["python", "main.py"],
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
)
|
||||
else:
|
||||
self.registration_process = subprocess.Popen(
|
||||
["python3", "main.py"]
|
||||
)
|
||||
|
||||
logger.info(f"注册进程已启动,PID: {self.registration_process.pid}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动注册进程时出错: {e}")
|
||||
self.registration_process = None
|
||||
|
||||
def stop_registration_process(self):
|
||||
"""停止注册进程"""
|
||||
if not self.registration_process or self.registration_process.poll() is not None:
|
||||
logger.info("注册进程未在运行")
|
||||
self.registration_process = None
|
||||
return
|
||||
|
||||
logger.info("停止注册进程")
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
# Windows下使用taskkill
|
||||
subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.registration_process.pid)])
|
||||
else:
|
||||
# Linux/Mac下使用terminate
|
||||
self.registration_process.terminate()
|
||||
self.registration_process.wait(timeout=5)
|
||||
|
||||
logger.info("注册进程已停止")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"停止注册进程时出错: {e}")
|
||||
|
||||
self.registration_process = None
|
||||
|
||||
async def upload_accounts(self, accounts: List[Dict[str, Any]]) -> bool:
|
||||
"""上传账号到API
|
||||
|
||||
Args:
|
||||
accounts: 要上传的账号列表,每个账号包含email、password、cursor_password等信息
|
||||
|
||||
Returns:
|
||||
bool: 是否上传成功
|
||||
"""
|
||||
if not accounts:
|
||||
return True
|
||||
|
||||
url = f"{self.api_base_url}/commonadd"
|
||||
|
||||
try:
|
||||
# 准备上传数据
|
||||
upload_data = []
|
||||
for account in accounts:
|
||||
upload_item = {
|
||||
"email": account["email"],
|
||||
"email_password": account.get("password", ""), # 使用account的password字段作为email_password
|
||||
"cursor_email": account["email"],
|
||||
"cursor_password": account["cursor_password"],
|
||||
"cookie": account.get("cursor_cookie", "") or "",
|
||||
"token": account.get("cursor_jwt", ""),
|
||||
"hostname": self.hostname
|
||||
}
|
||||
upload_data.append(upload_item)
|
||||
|
||||
# 打印上传数据的部分细节(去除敏感信息)
|
||||
debug_data = []
|
||||
for item in upload_data[:2]: # 只打印前2个账号作为示例
|
||||
debug_item = item.copy()
|
||||
if "cookie" in debug_item and debug_item["cookie"]:
|
||||
debug_item["cookie"] = debug_item["cookie"][:20] + "..." if len(debug_item["cookie"]) > 20 else debug_item["cookie"]
|
||||
if "token" in debug_item and debug_item["token"]:
|
||||
debug_item["token"] = debug_item["token"][:20] + "..." if len(debug_item["token"]) > 20 else debug_item["token"]
|
||||
debug_data.append(debug_item)
|
||||
|
||||
logger.debug(f"准备上传 {len(upload_data)} 个账号")
|
||||
logger.debug(f"上传数据示例: {json.dumps(debug_data, ensure_ascii=False)}")
|
||||
logger.debug(f"API URL: {url}")
|
||||
|
||||
# 发送请求
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
url,
|
||||
json=upload_data,
|
||||
proxy=self.proxy,
|
||||
ssl=False
|
||||
) as response:
|
||||
response_text = await response.text()
|
||||
logger.debug(f"API响应状态码: {response.status}")
|
||||
logger.debug(f"API响应内容: {response_text}")
|
||||
|
||||
if response.status != 200:
|
||||
logger.error(f"上传账号API请求失败,状态码: {response.status}")
|
||||
return False
|
||||
|
||||
try:
|
||||
data = json.loads(response_text)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"解析响应失败,非JSON格式: {response_text[:100]}...")
|
||||
return False
|
||||
|
||||
if data.get("code") != 0:
|
||||
error_msg = data.get("msg", "Unknown error")
|
||||
logger.error(f"上传账号API返回错误: {error_msg}")
|
||||
return False
|
||||
|
||||
success_count = data.get("data", {}).get("success", 0)
|
||||
failed_count = data.get("data", {}).get("failed", 0)
|
||||
|
||||
# 检查是否有详细的错误信息
|
||||
if "details" in data.get("data", {}):
|
||||
details = data.get("data", {}).get("details", [])
|
||||
if details:
|
||||
logger.error("错误详情:")
|
||||
for i, detail in enumerate(details[:5]): # 只显示前5个错误
|
||||
logger.error(f" 错误 {i+1}: {detail.get('email', '未知邮箱')} - {detail.get('message', '未知错误')}")
|
||||
|
||||
logger.info(f"账号上传结果: 成功 {success_count}, 失败 {failed_count}")
|
||||
|
||||
return success_count > 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"上传账号时出错: {e}")
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""运行服务主循环"""
|
||||
logger.info("启动Cursor自动化服务")
|
||||
|
||||
# 设置永久开启注册
|
||||
self.reg_enabled = True
|
||||
logger.info("注册功能已永久开启")
|
||||
|
||||
last_upload_time = 0
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# 1. 获取邮箱
|
||||
logger.info("准备获取自建邮箱")
|
||||
emails = await self.import_self_hosted_emails(self.email_batch_size)
|
||||
if emails:
|
||||
logger.info(f"成功获取 {len(emails)} 个自建邮箱")
|
||||
|
||||
# 2. 确保注册进程正在运行
|
||||
if not self.registration_process or self.registration_process.poll() is not None:
|
||||
if emails:
|
||||
logger.info(f"有 {len(emails)} 个可用邮箱,启动注册进程")
|
||||
await self.start_registration_process()
|
||||
else:
|
||||
logger.warning("没有可用邮箱,暂不启动注册进程")
|
||||
|
||||
# 3. 等待下一次检查
|
||||
logger.debug(f"等待 {self.check_interval} 秒后进行下一次检查")
|
||||
for _ in range(self.check_interval):
|
||||
if not self.running:
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"服务运行时出错: {e}")
|
||||
await asyncio.sleep(30) # 出错后等待30秒再继续
|
||||
|
||||
logger.info("服务已停止")
|
||||
|
||||
@classmethod
|
||||
async def start_service(cls):
|
||||
"""启动服务的静态方法"""
|
||||
service = cls()
|
||||
await service.initialize()
|
||||
|
||||
try:
|
||||
await service.run()
|
||||
finally:
|
||||
await service.cleanup()
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
# 设置日志
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="INFO")
|
||||
logger.add(
|
||||
"logs/auto_cursor_service.log",
|
||||
rotation="50 MB",
|
||||
retention="10 days",
|
||||
level="DEBUG",
|
||||
compression="zip"
|
||||
)
|
||||
|
||||
logger.info(f"Cursor自动化服务启动于: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
try:
|
||||
await AutoCursorService.start_service()
|
||||
except Exception as e:
|
||||
logger.error(f"服务异常终止: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.platform.startswith("win"):
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
asyncio.run(main())
|
||||
40
config.yaml
40
config.yaml
@@ -4,16 +4,41 @@ global:
|
||||
timeout: 30
|
||||
retry_times: 3
|
||||
|
||||
# 服务器配置
|
||||
server_config:
|
||||
hostname: "sg424" # 服务器标识,用于API调用
|
||||
|
||||
# 数据库配置
|
||||
database:
|
||||
# SQLite配置(兼容旧版本)
|
||||
path: "cursor.db"
|
||||
pool_size: 10
|
||||
|
||||
# MySQL配置
|
||||
host: "localhost"
|
||||
port: 3306
|
||||
username: "auto_cursor_reg"
|
||||
password: "this_password_jiaqiao"
|
||||
database: "auto_cursor_reg"
|
||||
|
||||
# 是否使用Redis缓存
|
||||
# 如果使用Python 3.12,请确保安装redis>=4.2.0而不是aioredis
|
||||
use_redis: true
|
||||
|
||||
# Redis配置(可选,当use_redis为true时生效)
|
||||
redis:
|
||||
host: "127.0.0.1"
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
# 代理配置
|
||||
proxy:
|
||||
api_url: "https://api.proxy.com/getProxy"
|
||||
api_url: "https://share.proxy.qg.net/get?key=969331C5&num=1&area=&isp=0&format=txt&seq=\r\n&distinct=false"
|
||||
batch_size: 100
|
||||
check_interval: 300
|
||||
# API专用代理(可选)
|
||||
api_proxy: "http://1ddbeae0f7a67106fd58:f72e512b10893a1d@gw.dataimpulse.com:823"
|
||||
|
||||
# 注册配置
|
||||
register:
|
||||
@@ -25,10 +50,21 @@ register:
|
||||
email:
|
||||
file_path: "email.txt"
|
||||
|
||||
# 自建邮箱服务配置
|
||||
self_hosted_email:
|
||||
api_base_url: "https://api.cursorpro.com.cn"
|
||||
api_key: "" # 如果API需要认证,请填写
|
||||
|
||||
# 自动服务配置
|
||||
auto_service:
|
||||
check_interval: 60 # 检查API状态的间隔(秒)
|
||||
upload_interval: 300 # 上传账号间隔(秒)
|
||||
email_check_threshold: 30 # 当可用邮箱少于这个数时获取新邮箱
|
||||
|
||||
captcha:
|
||||
provider: "capsolver" # 可选值: "capsolver" 或 "yescaptcha"
|
||||
capsolver:
|
||||
api_key: "CAP-E0A11882290AC7ADE2F799286B8E2DA497D7CD0510BFA477F3900507809F8AA3"
|
||||
api_key: "CAP-36D01B0995C7C8705DF68ACCFE4E2004FE182DDA72AC5A80F25F1E3B601C31F0"
|
||||
website_url: "https://authenticator.cursor.sh"
|
||||
website_key: "0x4AAAAAAAMNIvC45A4Wjjln"
|
||||
yescaptcha:
|
||||
|
||||
75
config_example.yaml
Normal file
75
config_example.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
# 全局配置
|
||||
global:
|
||||
max_concurrency: 20
|
||||
timeout: 30
|
||||
retry_times: 3
|
||||
|
||||
# 服务器配置
|
||||
server_config:
|
||||
hostname: "sg424" # 服务器标识,用于API调用
|
||||
|
||||
# 数据库配置
|
||||
database:
|
||||
# SQLite配置(兼容旧版本)
|
||||
path: "cursor.db"
|
||||
pool_size: 10
|
||||
|
||||
# MySQL配置
|
||||
host: "localhost"
|
||||
port: 3306
|
||||
username: "auto_cursor_reg"
|
||||
password: "this_password_jiaqiao"
|
||||
database: "auto_cursor_reg"
|
||||
|
||||
# 是否使用Redis缓存
|
||||
# 如果使用Python 3.12,请确保安装redis>=4.2.0而不是aioredis
|
||||
use_redis: true
|
||||
|
||||
# Redis配置(可选,当use_redis为true时生效)
|
||||
redis:
|
||||
host: "127.0.0.1"
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
# 代理配置
|
||||
proxy:
|
||||
api_url: "https://share.proxy.qg.net/get?key=969331C5&num=1&area=&isp=0&format=txt&seq=\r\n&distinct=false"
|
||||
batch_size: 100
|
||||
check_interval: 300
|
||||
# API专用代理(可选)
|
||||
api_proxy: "http://1ddbeae0f7a67106fd58:f72e512b10893a1d@gw.dataimpulse.com:823"
|
||||
|
||||
# 注册配置
|
||||
register:
|
||||
delay_range: [1, 2]
|
||||
batch_size: 15
|
||||
#这里是注册的并发数量
|
||||
|
||||
# 邮件配置
|
||||
email:
|
||||
file_path: "email.txt"
|
||||
|
||||
# 自建邮箱服务配置
|
||||
# 如果添加了这部分配置,系统将优先使用自建邮箱进行注册
|
||||
self_hosted_email:
|
||||
api_base_url: "https://api.cursorpro.com.cn"
|
||||
api_key: "your_api_key_here" # 可选,如果API需要认证
|
||||
|
||||
# 自动服务配置
|
||||
auto_service:
|
||||
check_interval: 60 # 检查API状态的间隔(秒)
|
||||
upload_interval: 300 # 上传账号间隔(秒)
|
||||
email_check_threshold: 30 # 当可用邮箱少于这个数时获取新邮箱
|
||||
|
||||
captcha:
|
||||
provider: "capsolver" # 可选值: "capsolver" 或 "yescaptcha"
|
||||
capsolver:
|
||||
api_key: "CAP-36D01B0995C7C8705DF68ACCFE4E2004FE182DDA72AC5A80F25F1E3B601C31F0"
|
||||
website_url: "https://authenticator.cursor.sh"
|
||||
website_key: "0x4AAAAAAAMNIvC45A4Wjjln"
|
||||
yescaptcha:
|
||||
client_key: "a5ef0062c1d2674900e78722c5670e3a3484bc8c64273"
|
||||
website_url: "https://authenticator.cursor.sh"
|
||||
website_key: "a5ef0062c1d2674900e78722c5670e3a3484bc8c64273"
|
||||
use_cn_server: false
|
||||
113
core/config.py
113
core/config.py
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple
|
||||
from typing import Tuple, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -13,8 +13,25 @@ class GlobalConfig:
|
||||
|
||||
@dataclass
|
||||
class DatabaseConfig:
|
||||
path: str
|
||||
pool_size: int
|
||||
# SQLite的配置字段保留,用于兼容
|
||||
path: Optional[str] = None
|
||||
pool_size: int = 10
|
||||
# MySQL配置
|
||||
host: str = "localhost"
|
||||
port: int = 3306
|
||||
username: str = "auto_cursor_reg"
|
||||
password: str = "this_password_jiaqiao"
|
||||
database: str = "auto_cursor_reg"
|
||||
# Redis配置
|
||||
use_redis: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class RedisConfig:
|
||||
host: str
|
||||
port: int
|
||||
password: str = ""
|
||||
db: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -22,6 +39,7 @@ class ProxyConfig:
|
||||
api_url: str
|
||||
batch_size: int
|
||||
check_interval: int
|
||||
api_proxy: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -35,6 +53,27 @@ class EmailConfig:
|
||||
file_path: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SelfHostedEmailConfig:
|
||||
"""自建邮箱配置"""
|
||||
api_base_url: str
|
||||
api_key: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServerConfig:
|
||||
hostname: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AutoServiceConfig:
|
||||
check_interval: int = 60
|
||||
upload_interval: int = 300
|
||||
email_check_threshold: int = 30
|
||||
email_fetch_count: int = 2
|
||||
|
||||
|
||||
@dataclass
|
||||
class CapsolverConfig:
|
||||
api_key: str
|
||||
@@ -61,29 +100,69 @@ class CaptchaConfig:
|
||||
class Config:
|
||||
global_config: GlobalConfig
|
||||
database_config: DatabaseConfig
|
||||
proxy_config: ProxyConfig
|
||||
register_config: RegisterConfig
|
||||
email_config: EmailConfig
|
||||
captcha_config: CaptchaConfig
|
||||
redis_config: Optional[RedisConfig] = None
|
||||
proxy_config: ProxyConfig = None
|
||||
register_config: RegisterConfig = None
|
||||
email_config: EmailConfig = None
|
||||
captcha_config: CaptchaConfig = None
|
||||
server_config: Optional[ServerConfig] = None
|
||||
auto_service_config: Optional[AutoServiceConfig] = None
|
||||
self_hosted_email_config: Optional[SelfHostedEmailConfig] = None
|
||||
hostname: Optional[str] = None # 向后兼容
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, path: str = "config.yaml"):
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# 创建 database 配置对象
|
||||
db_config = DatabaseConfig(**data.get('database', {}))
|
||||
|
||||
# 创建 redis 配置对象(如果有)
|
||||
redis_config = None
|
||||
if 'redis' in data and db_config.use_redis:
|
||||
redis_config = RedisConfig(**data['redis'])
|
||||
|
||||
# 创建 captcha 配置对象
|
||||
captcha_data = data['captcha']
|
||||
captcha_data = data.get('captcha', {})
|
||||
captcha_config = None
|
||||
if captcha_data:
|
||||
captcha_config = CaptchaConfig(
|
||||
provider=captcha_data['provider'],
|
||||
capsolver=CapsolverConfig(**captcha_data['capsolver']),
|
||||
yescaptcha=YesCaptchaConfig(**captcha_data['yescaptcha'])
|
||||
provider=captcha_data.get('provider', 'capsolver'),
|
||||
capsolver=CapsolverConfig(**captcha_data.get('capsolver', {})),
|
||||
yescaptcha=YesCaptchaConfig(**captcha_data.get('yescaptcha', {}))
|
||||
)
|
||||
|
||||
# 创建其他配置对象
|
||||
global_config = GlobalConfig(**data.get('global', {}))
|
||||
proxy_config = ProxyConfig(**data.get('proxy', {})) if 'proxy' in data else None
|
||||
register_config = RegisterConfig(**data.get('register', {})) if 'register' in data else None
|
||||
email_config = EmailConfig(**data.get('email', {})) if 'email' in data else None
|
||||
|
||||
# 创建服务器配置对象
|
||||
server_config = ServerConfig(**data.get('server_config', {})) if 'server_config' in data else None
|
||||
|
||||
# 创建自动服务配置对象
|
||||
auto_service_config = AutoServiceConfig(**data.get('auto_service', {})) if 'auto_service' in data else None
|
||||
|
||||
# 创建自建邮箱配置对象
|
||||
self_hosted_email_config = SelfHostedEmailConfig(**data.get('self_hosted_email', {})) if 'self_hosted_email' in data else None
|
||||
|
||||
# 设置hostname (优先使用server_config中的hostname)
|
||||
hostname = None
|
||||
if server_config and hasattr(server_config, 'hostname'):
|
||||
hostname = server_config.hostname
|
||||
|
||||
return cls(
|
||||
global_config=GlobalConfig(**data['global']),
|
||||
database_config=DatabaseConfig(**data['database']),
|
||||
proxy_config=ProxyConfig(**data['proxy']),
|
||||
register_config=RegisterConfig(**data['register']),
|
||||
email_config=EmailConfig(**data['email']),
|
||||
captcha_config=captcha_config
|
||||
global_config=global_config,
|
||||
database_config=db_config,
|
||||
redis_config=redis_config,
|
||||
proxy_config=proxy_config,
|
||||
register_config=register_config,
|
||||
email_config=email_config,
|
||||
captcha_config=captcha_config,
|
||||
server_config=server_config,
|
||||
auto_service_config=auto_service_config,
|
||||
self_hosted_email_config=self_hosted_email_config,
|
||||
hostname=hostname
|
||||
)
|
||||
|
||||
274
core/database.py
274
core/database.py
@@ -1,86 +1,276 @@
|
||||
import asyncio
|
||||
import json
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import aiosqlite
|
||||
import aiomysql
|
||||
from loguru import logger
|
||||
|
||||
# 使用条件导入替代直接导入
|
||||
REDIS_AVAILABLE = False
|
||||
try:
|
||||
# 尝试导入redis.asyncio (Redis-py 4.2.0+)
|
||||
import redis.asyncio as redis_asyncio
|
||||
REDIS_AVAILABLE = True
|
||||
REDIS_TYPE = "redis-py"
|
||||
except ImportError:
|
||||
try:
|
||||
# 尝试导入aioredis (旧版本)
|
||||
import aioredis
|
||||
REDIS_AVAILABLE = True
|
||||
REDIS_TYPE = "aioredis"
|
||||
except (ImportError, TypeError):
|
||||
REDIS_AVAILABLE = False
|
||||
REDIS_TYPE = None
|
||||
|
||||
from core.config import Config
|
||||
|
||||
|
||||
class DatabaseManager:
|
||||
def __init__(self, config: Config):
|
||||
self.db_path = config.database_config.path
|
||||
self._pool_size = config.database_config.pool_size
|
||||
self._pool: List[aiosqlite.Connection] = []
|
||||
# 数据库配置
|
||||
self.db_config = config.database_config
|
||||
self._pool_size = self.db_config.pool_size
|
||||
self._pool = None # 连接池
|
||||
self._pool_lock = asyncio.Lock()
|
||||
|
||||
# Redis配置
|
||||
self.use_redis = self.db_config.use_redis
|
||||
self.redis_config = config.redis_config if hasattr(config, 'redis_config') else None
|
||||
self.redis = None
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化数据库连接池"""
|
||||
logger.info("初始化数据库连接池")
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute('''
|
||||
|
||||
# 创建MySQL连接池
|
||||
try:
|
||||
logger.info(f"连接MySQL: {self.db_config.host}:{self.db_config.port}, 用户: {self.db_config.username}, 数据库: {self.db_config.database}")
|
||||
self._pool = await aiomysql.create_pool(
|
||||
host=self.db_config.host,
|
||||
port=self.db_config.port,
|
||||
user=self.db_config.username,
|
||||
password=self.db_config.password,
|
||||
db=self.db_config.database,
|
||||
maxsize=self._pool_size,
|
||||
autocommit=True,
|
||||
charset='utf8mb4'
|
||||
)
|
||||
logger.info("MySQL连接池创建成功")
|
||||
except Exception as e:
|
||||
logger.error(f"MySQL连接池创建失败: {str(e)}")
|
||||
logger.error("请检查MySQL配置是否正确,以及MySQL服务是否已启动")
|
||||
logger.info(f"您可能需要创建MySQL用户和数据库:")
|
||||
logger.info(f" CREATE USER '{self.db_config.username}'@'localhost' IDENTIFIED BY '{self.db_config.password}';")
|
||||
logger.info(f" CREATE DATABASE {self.db_config.database} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;")
|
||||
logger.info(f" GRANT ALL PRIVILEGES ON {self.db_config.database}.* TO '{self.db_config.username}'@'localhost';")
|
||||
logger.info(f" FLUSH PRIVILEGES;")
|
||||
raise
|
||||
|
||||
# 初始化表结构
|
||||
async with self.get_connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS email_accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
client_id TEXT NOT NULL,
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT NOT NULL,
|
||||
in_use BOOLEAN DEFAULT 0,
|
||||
cursor_password TEXT,
|
||||
cursor_password VARCHAR(255),
|
||||
cursor_cookie TEXT,
|
||||
cursor_token TEXT,
|
||||
sold BOOLEAN DEFAULT 0,
|
||||
status TEXT DEFAULT 'pending',
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
extracted BOOLEAN DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_status_inuse_sold (status, in_use, sold),
|
||||
INDEX idx_extracted (extracted, status, sold)
|
||||
)
|
||||
''')
|
||||
await db.commit()
|
||||
|
||||
# 初始化连接池
|
||||
for i in range(self._pool_size):
|
||||
conn = await aiosqlite.connect(self.db_path)
|
||||
self._pool.append(conn)
|
||||
# 初始化Redis连接(如果配置了)
|
||||
if self.use_redis and REDIS_AVAILABLE and self.redis_config:
|
||||
try:
|
||||
# 根据检测到的Redis库类型创建连接
|
||||
if REDIS_TYPE == "redis-py":
|
||||
# 使用redis.asyncio创建连接
|
||||
logger.info(f"使用redis-py连接Redis: {self.redis_config.host}:{self.redis_config.port}")
|
||||
self.redis = redis_asyncio.Redis(
|
||||
host=self.redis_config.host,
|
||||
port=self.redis_config.port,
|
||||
password=self.redis_config.password or None,
|
||||
db=self.redis_config.db,
|
||||
decode_responses=True
|
||||
)
|
||||
# 测试连接
|
||||
await self.redis.ping()
|
||||
elif REDIS_TYPE == "aioredis":
|
||||
# 使用旧版aioredis创建连接
|
||||
logger.info(f"使用aioredis连接Redis: {self.redis_config.host}:{self.redis_config.port}")
|
||||
self.redis = await aioredis.from_url(
|
||||
f"redis://{self.redis_config.host}:{self.redis_config.port}",
|
||||
password=self.redis_config.password or None,
|
||||
db=self.redis_config.db,
|
||||
encoding="utf-8",
|
||||
decode_responses=True
|
||||
)
|
||||
logger.info("Redis连接初始化成功")
|
||||
except Exception as e:
|
||||
logger.error(f"Redis连接初始化失败: {e}")
|
||||
logger.info("Redis缓存将被禁用")
|
||||
self.redis = None
|
||||
|
||||
logger.info(f"数据库连接池初始化完成,大小: {self._pool_size}")
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理数据库连接"""
|
||||
for conn in self._pool:
|
||||
await conn.close()
|
||||
self._pool.clear()
|
||||
if self._pool:
|
||||
self._pool.close()
|
||||
await self._pool.wait_closed()
|
||||
|
||||
if self.redis:
|
||||
if REDIS_TYPE == "redis-py":
|
||||
await self.redis.close()
|
||||
else:
|
||||
await self.redis.close()
|
||||
|
||||
logger.info("数据库连接已清理")
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_connection(self):
|
||||
"""获取数据库连接"""
|
||||
async with self._pool_lock:
|
||||
if not self._pool:
|
||||
conn = await aiosqlite.connect(self.db_path)
|
||||
else:
|
||||
conn = self._pool.pop()
|
||||
if self._pool is None:
|
||||
raise Exception("数据库连接池未初始化")
|
||||
|
||||
async with self._pool.acquire() as conn:
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
if len(self._pool) < self._pool_size:
|
||||
self._pool.append(conn)
|
||||
else:
|
||||
await conn.close()
|
||||
pass # 连接会自动返回池中
|
||||
|
||||
async def execute(self, query: str, params: tuple = ()) -> Any:
|
||||
"""执行SQL语句"""
|
||||
logger.debug(f"执行SQL: {query}, 参数: {params}")
|
||||
try:
|
||||
async with self.get_connection() as conn:
|
||||
cursor = await conn.execute(query, params)
|
||||
await conn.commit()
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute(query, params)
|
||||
|
||||
# 对于INSERT语句,返回最后插入的ID
|
||||
if query.strip().upper().startswith("INSERT"):
|
||||
return cursor.lastrowid
|
||||
|
||||
async def fetch_one(self, query: str, params: tuple = ()) -> Optional[tuple]:
|
||||
"""查询单条记录"""
|
||||
async with self.get_connection() as conn:
|
||||
cursor = await conn.execute(query, params)
|
||||
return await cursor.fetchone()
|
||||
# 对于UPDATE/DELETE语句,返回影响的行数
|
||||
return cursor.rowcount
|
||||
except Exception as e:
|
||||
logger.error(f"SQL执行失败: {query}, 参数: {params}, 错误: {str(e)}")
|
||||
raise
|
||||
|
||||
async def fetch_all(self, query: str, params: tuple = ()) -> List[tuple]:
|
||||
"""查询多条记录"""
|
||||
async def fetch_one(self, query: str, params: tuple = ()) -> Optional[Dict]:
|
||||
"""查询单条记录"""
|
||||
logger.debug(f"查询单条: {query}, 参数: {params}")
|
||||
|
||||
# 尝试从Redis获取缓存
|
||||
cache_key = f"db:{self._make_cache_key(query, params)}"
|
||||
cached_result = await self._get_from_cache(cache_key)
|
||||
if cached_result is not None:
|
||||
return cached_result
|
||||
|
||||
try:
|
||||
async with self.get_connection() as conn:
|
||||
cursor = await conn.execute(query, params)
|
||||
return await cursor.fetchall()
|
||||
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(query, params)
|
||||
result = await cursor.fetchone()
|
||||
|
||||
# 缓存结果
|
||||
if result and self.redis:
|
||||
await self._store_in_cache(cache_key, result)
|
||||
|
||||
logger.debug(f"查询结果: {result}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"查询单条失败: {query}, 参数: {params}, 错误: {str(e)}")
|
||||
raise
|
||||
|
||||
async def fetch_all(self, query: str, params: tuple = ()) -> List[Dict]:
|
||||
"""查询多条记录"""
|
||||
logger.debug(f"查询多条: {query}, 参数: {params}")
|
||||
|
||||
# 尝试从Redis获取缓存
|
||||
cache_key = f"db:{self._make_cache_key(query, params)}"
|
||||
cached_result = await self._get_from_cache(cache_key)
|
||||
if cached_result is not None:
|
||||
return cached_result
|
||||
|
||||
try:
|
||||
async with self.get_connection() as conn:
|
||||
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(query, params)
|
||||
results = await cursor.fetchall()
|
||||
|
||||
# 缓存结果
|
||||
if results and self.redis:
|
||||
await self._store_in_cache(cache_key, results)
|
||||
|
||||
logger.debug(f"查询结果数量: {len(results)}")
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"查询多条失败: {query}, 参数: {params}, 错误: {str(e)}")
|
||||
raise
|
||||
|
||||
async def _get_from_cache(self, key: str) -> Optional[Union[Dict, List[Dict]]]:
|
||||
"""从Redis缓存获取数据"""
|
||||
if not self.redis:
|
||||
return None
|
||||
|
||||
try:
|
||||
cached_data = await self.redis.get(key)
|
||||
if cached_data:
|
||||
return json.loads(cached_data)
|
||||
except Exception as e:
|
||||
logger.error(f"从缓存获取数据失败: {e}")
|
||||
|
||||
return None
|
||||
|
||||
async def _store_in_cache(self, key: str, data: Union[Dict, List[Dict]], ttl: int = 300) -> bool:
|
||||
"""存储数据到Redis缓存"""
|
||||
if not self.redis:
|
||||
return False
|
||||
|
||||
try:
|
||||
json_data = json.dumps(data)
|
||||
if REDIS_TYPE == "redis-py":
|
||||
await self.redis.setex(key, ttl, json_data)
|
||||
else:
|
||||
await self.redis.setex(key, ttl, json_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"存储数据到缓存失败: {e}")
|
||||
return False
|
||||
|
||||
async def clear_cache(self, pattern: str = "db:*") -> int:
|
||||
"""清除缓存"""
|
||||
if not self.redis:
|
||||
return 0
|
||||
|
||||
try:
|
||||
if REDIS_TYPE == "redis-py":
|
||||
keys = await self.redis.keys(pattern)
|
||||
if not keys:
|
||||
return 0
|
||||
return await self.redis.delete(*keys)
|
||||
else:
|
||||
keys = await self.redis.keys(pattern)
|
||||
if not keys:
|
||||
return 0
|
||||
return await self.redis.delete(*keys)
|
||||
except Exception as e:
|
||||
logger.error(f"清除缓存失败: {e}")
|
||||
return 0
|
||||
|
||||
def _make_cache_key(self, query: str, params: tuple) -> str:
|
||||
"""生成缓存键"""
|
||||
return f"{query}:{hash(params)}"
|
||||
1
database_schema.sql
Normal file
1
database_schema.sql
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
2
debug_response.txt
Normal file
2
debug_response.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
0:["$@1",["ARI0To80kOF93OzKZnFMN",null]]
|
||||
1:{"payload":"Fe26.2*1*fb496fc3e44b03fba263d4904365d08ced6500c32da425de639cf4fd54b97e67*q0VzEVML3A50n2VWgjWCbw*jnmrTYQYUCSf8e1ly67O4w*1749190572271*0180c7834ee9ad01e6c2cb234416f8824ec91f4d96526d5aadbc1569e89f6245*GUdEqde3p6MD0n_JGEPb7-HF5-WEff4CtzpwyNl95k4~2","ttl":5184000000}
|
||||
186
fix_setup.py
Normal file
186
fix_setup.py
Normal file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cursor自动化服务 - 修复脚本
|
||||
添加虚拟环境支持并修复MySQL连接问题
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
import time
|
||||
|
||||
# 彩色输出函数
|
||||
class Colors:
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
RED = '\033[91m'
|
||||
BLUE = '\033[94m'
|
||||
ENDC = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
|
||||
def print_success(msg):
|
||||
print(f"{Colors.GREEN}✓ {msg}{Colors.ENDC}")
|
||||
|
||||
def print_warning(msg):
|
||||
print(f"{Colors.YELLOW}⚠ {msg}{Colors.ENDC}")
|
||||
|
||||
def print_error(msg):
|
||||
print(f"{Colors.RED}✗ {msg}{Colors.ENDC}")
|
||||
|
||||
def print_title(title):
|
||||
print("\n" + "=" * 60)
|
||||
print(f"{Colors.BOLD}{Colors.BLUE}{title.center(60)}{Colors.ENDC}")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
def check_virtual_env():
|
||||
"""检查是否在虚拟环境中运行"""
|
||||
return hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
|
||||
|
||||
def setup_virtual_env():
|
||||
"""设置Python虚拟环境"""
|
||||
print_title("虚拟环境设置")
|
||||
|
||||
if check_virtual_env():
|
||||
print_success("当前已在虚拟环境中运行")
|
||||
return True
|
||||
|
||||
print_warning("当前未在虚拟环境中运行")
|
||||
create_venv = input("是否创建虚拟环境? (推荐) (y/n, 默认: y): ").strip().lower()
|
||||
if create_venv == 'n':
|
||||
print_warning("跳过虚拟环境创建")
|
||||
return True
|
||||
|
||||
venv_path = input("请输入虚拟环境路径 (默认: ./venv): ").strip() or "./venv"
|
||||
|
||||
try:
|
||||
# 创建虚拟环境
|
||||
print(f"创建虚拟环境: {venv_path}")
|
||||
subprocess.check_call([sys.executable, "-m", "venv", venv_path])
|
||||
|
||||
# 获取激活脚本路径
|
||||
if sys.platform.startswith('win'):
|
||||
activate_script = os.path.join(venv_path, "Scripts", "activate.bat")
|
||||
python_exe = os.path.join(venv_path, "Scripts", "python.exe")
|
||||
else:
|
||||
activate_script = os.path.join(venv_path, "bin", "activate")
|
||||
python_exe = os.path.join(venv_path, "bin", "python")
|
||||
|
||||
# 创建安装脚本
|
||||
print("创建安装脚本...")
|
||||
setup_script = "setup_venv.sh"
|
||||
with open(setup_script, "w") as f:
|
||||
if sys.platform.startswith('win'):
|
||||
f.write(f"@echo off\n")
|
||||
f.write(f"call {activate_script}\n")
|
||||
f.write(f"pip install -r requirements.txt\n")
|
||||
f.write(f"pip install cryptography\n")
|
||||
f.write(f"python setup_environment.py\n")
|
||||
f.write(f"pause\n")
|
||||
else:
|
||||
f.write(f"#!/bin/bash\n")
|
||||
f.write(f"source {activate_script}\n")
|
||||
f.write(f"pip install -r requirements.txt\n")
|
||||
f.write(f"pip install cryptography\n")
|
||||
f.write(f"python setup_environment.py\n")
|
||||
|
||||
if not sys.platform.startswith('win'):
|
||||
os.chmod(setup_script, 0o755)
|
||||
|
||||
print_success(f"虚拟环境创建成功: {venv_path}")
|
||||
print_success(f"安装脚本已创建: {setup_script}")
|
||||
|
||||
print("\n请运行以下命令完成安装:")
|
||||
if sys.platform.startswith('win'):
|
||||
print(f" {setup_script}")
|
||||
else:
|
||||
print(f" ./{setup_script}")
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
print_error(f"创建虚拟环境失败: {str(e)}")
|
||||
return True
|
||||
|
||||
def fix_mysql_format_issue():
|
||||
"""修复MySQL格式化问题"""
|
||||
print_title("修复MySQL连接问题")
|
||||
|
||||
if not os.path.exists("setup_environment.py"):
|
||||
print_error("未找到setup_environment.py文件")
|
||||
return False
|
||||
|
||||
backup_file = f"setup_environment.py.bak.{int(time.time())}"
|
||||
try:
|
||||
# 创建备份
|
||||
shutil.copy2("setup_environment.py", backup_file)
|
||||
print_success(f"已创建备份: {backup_file}")
|
||||
|
||||
# 读取文件内容
|
||||
with open("setup_environment.py", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 修复格式化问题
|
||||
fixed_content = content
|
||||
|
||||
# 1. 添加cryptography到required_packages
|
||||
if "cryptography" not in content:
|
||||
fixed_content = fixed_content.replace(
|
||||
'required_packages = [\n "loguru",\n "pymysql",\n "aiomysql",\n "redis",\n "pyyaml"',
|
||||
'required_packages = [\n "loguru",\n "pymysql",\n "aiomysql",\n "redis",\n "pyyaml",\n "cryptography"'
|
||||
)
|
||||
print_success("已添加cryptography到依赖包列表")
|
||||
|
||||
# 2. 修复CREATE USER语句的格式化问题
|
||||
if "cursor.execute(f\"CREATE USER '{db_user}'@'%'" in content:
|
||||
fixed_content = fixed_content.replace(
|
||||
"cursor.execute(f\"CREATE USER '{db_user}'@'%' IDENTIFIED BY %s\", (db_password,))",
|
||||
"cursor.execute(\"CREATE USER %s@'%%' IDENTIFIED BY %s\", (db_user, db_password))"
|
||||
)
|
||||
print_success("已修复CREATE USER语句")
|
||||
|
||||
# 3. 修复ALTER USER语句的格式化问题
|
||||
if "cursor.execute(f\"ALTER USER '{db_user}'@'%'" in content:
|
||||
fixed_content = fixed_content.replace(
|
||||
"cursor.execute(f\"ALTER USER '{db_user}'@'%' IDENTIFIED BY %s\", (db_password,))",
|
||||
"cursor.execute(\"ALTER USER %s@'%%' IDENTIFIED BY %s\", (db_user, db_password))"
|
||||
)
|
||||
print_success("已修复ALTER USER语句")
|
||||
|
||||
# 写入修复后的内容
|
||||
with open("setup_environment.py", "w", encoding="utf-8") as f:
|
||||
f.write(fixed_content)
|
||||
|
||||
print_success("MySQL连接问题修复完成")
|
||||
return True
|
||||
except Exception as e:
|
||||
print_error(f"修复过程出错: {str(e)}")
|
||||
try:
|
||||
if os.path.exists(backup_file):
|
||||
shutil.copy2(backup_file, "setup_environment.py")
|
||||
print_warning("已恢复原始文件")
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def main():
|
||||
print_title("Cursor自动化服务 - 修复工具")
|
||||
|
||||
# 设置虚拟环境
|
||||
if not setup_virtual_env():
|
||||
# 如果创建了虚拟环境并生成了安装脚本,直接退出
|
||||
sys.exit(0)
|
||||
|
||||
# 修复MySQL格式化问题
|
||||
if not fix_mysql_format_issue():
|
||||
print_error("修复失败,请手动检查setup_environment.py文件")
|
||||
sys.exit(1)
|
||||
|
||||
print_title("修复完成")
|
||||
print("现在可以安全地运行设置向导:")
|
||||
print(" python setup_environment.py")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n操作已取消")
|
||||
sys.exit(1)
|
||||
129
import_emails.py
129
import_emails.py
@@ -1,61 +1,110 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
# Windows平台特殊处理,强制使用SelectorEventLoop
|
||||
if sys.platform.startswith('win'):
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
import aiosqlite
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
from core.database import DatabaseManager
|
||||
|
||||
|
||||
async def import_emails(config: Config, file_path: str):
|
||||
"""导入邮箱账号到数据库"""
|
||||
async def import_emails(config: Config, db_manager: DatabaseManager, file_path: str):
|
||||
"""导入邮箱账号到MySQL数据库"""
|
||||
DEFAULT_CLIENT_ID = "9e5f94bc-e8a4-4e73-b8be-63364c29d753"
|
||||
|
||||
async with aiosqlite.connect(config.database_config.path) as db:
|
||||
# 创建表
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS email_accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
client_id TEXT NOT NULL,
|
||||
refresh_token TEXT NOT NULL,
|
||||
in_use BOOLEAN DEFAULT 0,
|
||||
cursor_password TEXT,
|
||||
cursor_cookie TEXT,
|
||||
cursor_token TEXT,
|
||||
sold BOOLEAN DEFAULT 0,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
# 确保数据库连接已初始化
|
||||
if not db_manager._pool:
|
||||
await db_manager.initialize()
|
||||
|
||||
# 读取文件并导入数据
|
||||
count = 0
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
if line.strip():
|
||||
try:
|
||||
email, password, client_id, refresh_token = line.strip().split('----')
|
||||
await db.execute('''
|
||||
INSERT INTO email_accounts (
|
||||
email, password, client_id, refresh_token, status
|
||||
) VALUES (?, ?, ?, ?, 'pending')
|
||||
''', (email, password, client_id, refresh_token))
|
||||
count += 1
|
||||
except aiosqlite.IntegrityError:
|
||||
logger.warning(f"重复的邮箱: {email}")
|
||||
except ValueError:
|
||||
logger.error(f"无效的数据行: {line.strip()}")
|
||||
duplicate_count = 0
|
||||
error_count = 0
|
||||
|
||||
await db.commit()
|
||||
logger.success(f"成功导入 {count} 个邮箱账号")
|
||||
logger.info(f"开始从 {file_path} 导入邮箱账号")
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line_num, line in enumerate(f, 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
try:
|
||||
# 解析数据行
|
||||
parts = line.strip().split('----')
|
||||
if len(parts) < 4:
|
||||
logger.error(f"行 {line_num}: 格式不正确,期望 'email----password----client_id----refresh_token'")
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
email, password, client_id, refresh_token = parts
|
||||
|
||||
# 插入数据库
|
||||
insert_query = '''
|
||||
INSERT INTO email_accounts
|
||||
(email, password, client_id, refresh_token, status)
|
||||
VALUES (%s, %s, %s, %s, 'pending')
|
||||
'''
|
||||
|
||||
try:
|
||||
await db_manager.execute(insert_query, (email, password, client_id, refresh_token))
|
||||
count += 1
|
||||
|
||||
if count % 100 == 0:
|
||||
logger.info(f"已导入 {count} 个邮箱账号")
|
||||
|
||||
except Exception as e:
|
||||
if "Duplicate entry" in str(e):
|
||||
logger.warning(f"行 {line_num}: 重复的邮箱: {email}")
|
||||
duplicate_count += 1
|
||||
else:
|
||||
logger.error(f"行 {line_num}: 导入失败: {str(e)}")
|
||||
error_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"行 {line_num}: 处理时出错: {str(e)}")
|
||||
error_count += 1
|
||||
|
||||
# 如果启用了Redis缓存,清除相关缓存
|
||||
if db_manager.redis:
|
||||
cleared = await db_manager.clear_cache("db:*")
|
||||
logger.info(f"已清除 {cleared} 个Redis缓存键")
|
||||
|
||||
logger.success(f"导入完成: 成功 {count} 个, 重复 {duplicate_count} 个, 失败 {error_count} 个")
|
||||
return count
|
||||
|
||||
|
||||
async def main():
|
||||
try:
|
||||
# 加载配置
|
||||
config = Config.from_yaml()
|
||||
await import_emails(config, "email.txt")
|
||||
|
||||
# 初始化数据库管理器
|
||||
db_manager = DatabaseManager(config)
|
||||
await db_manager.initialize()
|
||||
|
||||
# 从配置中获取邮箱文件路径,或使用默认值
|
||||
file_path = config.email_config.file_path if hasattr(config, 'email_config') and config.email_config else "email.txt"
|
||||
|
||||
# 导入邮箱
|
||||
await import_emails(config, db_manager, file_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"程序执行出错: {str(e)}")
|
||||
finally:
|
||||
# 清理资源
|
||||
if 'db_manager' in locals():
|
||||
await db_manager.cleanup()
|
||||
logger.info("程序执行完毕")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 设置日志
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="INFO")
|
||||
logger.add("import_emails.log", rotation="1 MB", level="DEBUG")
|
||||
|
||||
# 执行导入
|
||||
asyncio.run(main())
|
||||
329
init_database.py
Normal file
329
init_database.py
Normal file
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据库初始化脚本
|
||||
用于自动创建MySQL数据库、用户和表结构,并配置Redis
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import getpass
|
||||
import asyncio
|
||||
import yaml
|
||||
import pymysql
|
||||
import redis
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_CONFIG = {
|
||||
"database": {
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"username": "auto_cursor_reg",
|
||||
"password": "auto_cursor_pass",
|
||||
"database": "auto_cursor_reg",
|
||||
"pool_size": 10,
|
||||
"use_redis": True
|
||||
},
|
||||
"redis": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 6379,
|
||||
"password": "",
|
||||
"db": 0
|
||||
}
|
||||
}
|
||||
|
||||
# 数据库表结构
|
||||
EMAIL_ACCOUNTS_TABLE = '''
|
||||
CREATE TABLE IF NOT EXISTS email_accounts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT NOT NULL,
|
||||
in_use BOOLEAN DEFAULT 0,
|
||||
cursor_password VARCHAR(255),
|
||||
cursor_cookie TEXT,
|
||||
cursor_token TEXT,
|
||||
sold BOOLEAN DEFAULT 0,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
extracted BOOLEAN DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_status_inuse_sold (status, in_use, sold),
|
||||
INDEX idx_extracted (extracted, status, sold)
|
||||
)
|
||||
'''
|
||||
|
||||
|
||||
def setup_logger():
|
||||
"""设置日志"""
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="INFO")
|
||||
logger.add("init_database.log", rotation="1 MB", level="DEBUG")
|
||||
|
||||
|
||||
def get_mysql_root_credentials():
|
||||
"""获取MySQL root用户凭据"""
|
||||
print("\n===== MySQL配置 =====")
|
||||
print("请输入MySQL root用户凭据以创建数据库和用户")
|
||||
|
||||
mysql_host = input("MySQL 主机地址 [localhost]: ") or "localhost"
|
||||
mysql_port = input("MySQL 端口 [3306]: ") or "3306"
|
||||
try:
|
||||
mysql_port = int(mysql_port)
|
||||
except ValueError:
|
||||
mysql_port = 3306
|
||||
print("端口无效,使用默认端口 3306")
|
||||
|
||||
mysql_root = input("MySQL root用户名 [root]: ") or "root"
|
||||
mysql_root_password = getpass.getpass("MySQL root密码: ")
|
||||
|
||||
return {
|
||||
"host": mysql_host,
|
||||
"port": mysql_port,
|
||||
"user": mysql_root,
|
||||
"password": mysql_root_password
|
||||
}
|
||||
|
||||
|
||||
def get_app_database_config():
|
||||
"""获取应用数据库配置"""
|
||||
print("\n请设置应用程序的数据库配置:")
|
||||
|
||||
db_name = input("数据库名称 [auto_cursor_reg]: ") or "auto_cursor_reg"
|
||||
db_user = input("数据库用户名 [auto_cursor_reg]: ") or "auto_cursor_reg"
|
||||
db_pass = getpass.getpass(f"为用户 {db_user} 设置密码 [auto_cursor_pass]: ")
|
||||
if not db_pass:
|
||||
db_pass = "auto_cursor_pass"
|
||||
|
||||
return {
|
||||
"database": db_name,
|
||||
"username": db_user,
|
||||
"password": db_pass
|
||||
}
|
||||
|
||||
|
||||
def get_redis_config():
|
||||
"""获取Redis配置"""
|
||||
use_redis = input("\n是否使用Redis缓存? (y/n) [y]: ").lower() != "n"
|
||||
|
||||
if not use_redis:
|
||||
return {"use_redis": False}
|
||||
|
||||
print("\n===== Redis配置 =====")
|
||||
redis_host = input("Redis 主机地址 [127.0.0.1]: ") or "127.0.0.1"
|
||||
redis_port = input("Redis 端口 [6379]: ") or "6379"
|
||||
try:
|
||||
redis_port = int(redis_port)
|
||||
except ValueError:
|
||||
redis_port = 6379
|
||||
print("端口无效,使用默认端口 6379")
|
||||
|
||||
redis_password = getpass.getpass("Redis 密码 (如无密码请直接回车): ")
|
||||
redis_db = input("Redis 数据库索引 [0]: ") or "0"
|
||||
try:
|
||||
redis_db = int(redis_db)
|
||||
except ValueError:
|
||||
redis_db = 0
|
||||
print("数据库索引无效,使用默认值 0")
|
||||
|
||||
return {
|
||||
"use_redis": True,
|
||||
"redis": {
|
||||
"host": redis_host,
|
||||
"port": redis_port,
|
||||
"password": redis_password,
|
||||
"db": redis_db
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_mysql_connection(config):
|
||||
"""测试MySQL连接"""
|
||||
try:
|
||||
conn = pymysql.connect(**config)
|
||||
conn.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"MySQL连接失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def test_redis_connection(config):
|
||||
"""测试Redis连接"""
|
||||
try:
|
||||
r = redis.Redis(
|
||||
host=config["host"],
|
||||
port=config["port"],
|
||||
password=config["password"] if config["password"] else None,
|
||||
db=config["db"]
|
||||
)
|
||||
r.ping()
|
||||
r.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Redis连接失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def setup_mysql(root_config, app_config):
|
||||
"""设置MySQL数据库和用户"""
|
||||
logger.info("开始设置MySQL数据库...")
|
||||
|
||||
try:
|
||||
# 连接到MySQL
|
||||
conn = pymysql.connect(**root_config)
|
||||
cursor = conn.cursor()
|
||||
|
||||
db_name = app_config["database"]
|
||||
db_user = app_config["username"]
|
||||
db_pass = app_config["password"]
|
||||
|
||||
# 创建数据库
|
||||
logger.info(f"创建数据库: {db_name}")
|
||||
cursor.execute(f"CREATE DATABASE IF NOT EXISTS `{db_name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
|
||||
|
||||
# 创建用户并授权
|
||||
logger.info(f"创建用户: {db_user}")
|
||||
|
||||
# 检查用户是否已存在
|
||||
cursor.execute(f"SELECT User FROM mysql.user WHERE User = '{db_user}'")
|
||||
user_exists = cursor.fetchone()
|
||||
|
||||
if user_exists:
|
||||
logger.info(f"用户 {db_user} 已存在,更新密码")
|
||||
cursor.execute(f"ALTER USER '{db_user}'@'localhost' IDENTIFIED BY '{db_pass}'")
|
||||
cursor.execute(f"ALTER USER '{db_user}'@'%' IDENTIFIED BY '{db_pass}'")
|
||||
else:
|
||||
# 创建用户 (同时创建本地和远程连接权限)
|
||||
try:
|
||||
cursor.execute(f"CREATE USER '{db_user}'@'localhost' IDENTIFIED BY '{db_pass}'")
|
||||
cursor.execute(f"CREATE USER '{db_user}'@'%' IDENTIFIED BY '{db_pass}'")
|
||||
except pymysql.err.MySQLError as e:
|
||||
logger.warning(f"创建用户时出现警告 (可能用户已存在): {str(e)}")
|
||||
|
||||
# 授权
|
||||
cursor.execute(f"GRANT ALL PRIVILEGES ON `{db_name}`.* TO '{db_user}'@'localhost'")
|
||||
cursor.execute(f"GRANT ALL PRIVILEGES ON `{db_name}`.* TO '{db_user}'@'%'")
|
||||
cursor.execute("FLUSH PRIVILEGES")
|
||||
|
||||
# 切换到新创建的数据库
|
||||
cursor.execute(f"USE `{db_name}`")
|
||||
|
||||
# 创建表
|
||||
logger.info("创建数据表: email_accounts")
|
||||
cursor.execute(EMAIL_ACCOUNTS_TABLE)
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
logger.success("MySQL数据库设置成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"设置MySQL失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def update_config_file(db_config, redis_config=None):
|
||||
"""更新配置文件"""
|
||||
logger.info("更新配置文件...")
|
||||
|
||||
config_file = "config.yaml"
|
||||
|
||||
try:
|
||||
# 读取现有配置
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# 更新数据库配置
|
||||
config["database"].update({
|
||||
"host": db_config.get("host", "localhost"),
|
||||
"port": db_config.get("port", 3306),
|
||||
"username": db_config["username"],
|
||||
"password": db_config["password"],
|
||||
"database": db_config["database"],
|
||||
"use_redis": db_config.get("use_redis", False)
|
||||
})
|
||||
|
||||
# 如果启用Redis,更新Redis配置
|
||||
if redis_config and db_config.get("use_redis"):
|
||||
config["redis"] = redis_config
|
||||
|
||||
# 备份原配置文件
|
||||
if os.path.exists(config_file):
|
||||
os.rename(config_file, f"{config_file}.bak")
|
||||
logger.info(f"已备份原配置文件为 {config_file}.bak")
|
||||
|
||||
# 写入新配置
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
||||
|
||||
logger.success(f"配置文件已更新: {config_file}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新配置文件失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
setup_logger()
|
||||
logger.info("开始初始化数据库")
|
||||
|
||||
# 获取MySQL root凭据
|
||||
root_config = get_mysql_root_credentials()
|
||||
|
||||
# 测试MySQL连接
|
||||
if not test_mysql_connection(root_config):
|
||||
logger.error("无法连接到MySQL,请检查凭据和服务状态")
|
||||
return
|
||||
|
||||
# 获取应用数据库配置
|
||||
db_config = get_app_database_config()
|
||||
|
||||
# 获取Redis配置
|
||||
redis_info = get_redis_config()
|
||||
db_config["use_redis"] = redis_info["use_redis"]
|
||||
|
||||
# 如果启用了Redis,测试连接
|
||||
if redis_info["use_redis"]:
|
||||
redis_config = redis_info["redis"]
|
||||
if not test_redis_connection(redis_config):
|
||||
use_anyway = input("Redis连接测试失败,是否继续? (y/n) [n]: ").lower() == "y"
|
||||
if not use_anyway:
|
||||
logger.warning("用户取消了初始化")
|
||||
return
|
||||
else:
|
||||
redis_config = None
|
||||
|
||||
# 设置MySQL
|
||||
if not setup_mysql(root_config, db_config):
|
||||
return
|
||||
|
||||
# 合并配置
|
||||
final_db_config = {
|
||||
"host": root_config["host"],
|
||||
"port": root_config["port"],
|
||||
"username": db_config["username"],
|
||||
"password": db_config["password"],
|
||||
"database": db_config["database"],
|
||||
"use_redis": db_config["use_redis"]
|
||||
}
|
||||
|
||||
# 更新配置文件
|
||||
update_config_file(final_db_config, redis_config)
|
||||
|
||||
logger.success("数据库初始化完成!")
|
||||
print("\n===== 初始化完成 =====")
|
||||
print("您现在可以运行以下命令导入邮箱账号:")
|
||||
print(" python import_emails.py")
|
||||
print("\n然后运行主程序开始注册:")
|
||||
print(" python main.py")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
170
main.py
170
main.py
@@ -11,10 +11,14 @@ from core.config import Config
|
||||
from core.database import DatabaseManager
|
||||
from core.logger import setup_logger
|
||||
from register.register_worker import RegisterWorker
|
||||
from register.host_register_worker import HostRegisterWorker
|
||||
from services.email_manager import EmailManager
|
||||
from services.fetch_manager import FetchManager
|
||||
from services.proxy_pool import ProxyPool
|
||||
from services.token_pool import TokenPool
|
||||
from services.self_hosted_email import SelfHostedEmail
|
||||
from upload_account import AccountUploader
|
||||
from auto_cursor_service import AutoCursorService
|
||||
|
||||
|
||||
class CursorRegister:
|
||||
@@ -25,6 +29,8 @@ class CursorRegister:
|
||||
self.fetch_manager = FetchManager(self.config)
|
||||
self.proxy_pool = ProxyPool(self.config, self.fetch_manager)
|
||||
self.token_pool = TokenPool(self.config)
|
||||
|
||||
# 初始化常规邮箱服务
|
||||
self.email_manager = EmailManager(self.config, self.db_manager)
|
||||
self.register_worker = RegisterWorker(
|
||||
self.config,
|
||||
@@ -32,9 +38,30 @@ class CursorRegister:
|
||||
self.email_manager
|
||||
)
|
||||
|
||||
# 初始化自建邮箱服务(如果配置了)
|
||||
self.self_hosted_email = None
|
||||
self.host_register_worker = None
|
||||
if hasattr(self.config, 'self_hosted_email_config') and self.config.self_hosted_email_config:
|
||||
self.self_hosted_email = SelfHostedEmail(
|
||||
self.fetch_manager,
|
||||
self.config.self_hosted_email_config.api_base_url,
|
||||
self.config.self_hosted_email_config.api_key
|
||||
)
|
||||
self.logger.info("自建邮箱服务已初始化")
|
||||
|
||||
# 初始化自建邮箱注册工作器
|
||||
self.host_register_worker = HostRegisterWorker(
|
||||
self.config,
|
||||
self.fetch_manager,
|
||||
self.self_hosted_email
|
||||
)
|
||||
self.logger.info("自建邮箱注册工作器已初始化")
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化数据库"""
|
||||
await self.db_manager.initialize()
|
||||
# 确保EmailManager完成初始化
|
||||
await self.email_manager.initialize()
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理资源"""
|
||||
@@ -134,6 +161,105 @@ class CursorRegister:
|
||||
|
||||
return successful
|
||||
|
||||
async def batch_register_with_host(self, num: int):
|
||||
"""使用自建邮箱批量注册"""
|
||||
if not self.host_register_worker:
|
||||
self.logger.error("未配置自建邮箱注册工作器,无法继续")
|
||||
return []
|
||||
|
||||
try:
|
||||
self.logger.info(f"开始使用自建邮箱批量注册 {num} 个账号")
|
||||
|
||||
# 1. 获取token对
|
||||
token_pairs = await self.token_pool.batch_generate(num)
|
||||
if not token_pairs:
|
||||
self.logger.error("获取token失败,终止注册")
|
||||
return []
|
||||
|
||||
actual_num = len(token_pairs)
|
||||
if actual_num < num:
|
||||
self.logger.warning(f"只获取到 {actual_num} 对token,将减少注册数量")
|
||||
num = actual_num
|
||||
|
||||
# 2. 获取代理
|
||||
proxies = await self.proxy_pool.batch_get(num)
|
||||
|
||||
self.logger.debug(f"代理列表: {proxies}")
|
||||
self.logger.debug(f"尝试使用的token对数量: {len(token_pairs)}")
|
||||
|
||||
# 3. 创建注册任务
|
||||
tasks = []
|
||||
for proxy, token_pair in zip(proxies, token_pairs):
|
||||
task = self.host_register_worker.register(proxy, token_pair)
|
||||
tasks.append(task)
|
||||
|
||||
# 4. 并发执行
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 5. 处理结果
|
||||
successful = []
|
||||
failed = []
|
||||
skipped = 0
|
||||
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
self.logger.error(f"注册任务 {i+1} 失败: {str(result)}")
|
||||
failed.append(str(result))
|
||||
elif result is None:
|
||||
skipped += 1
|
||||
else:
|
||||
successful.append(result)
|
||||
# 添加详细的账号信息日志
|
||||
self.logger.info("=" * 50)
|
||||
self.logger.info(f"账号 {i+1} 注册成功:")
|
||||
self.logger.info(f"邮箱: {result['email']}")
|
||||
if 'email_password' in result and result['email_password']:
|
||||
self.logger.info(f"邮箱密码: {result['email_password']}")
|
||||
else:
|
||||
self.logger.info("邮箱密码: (无)")
|
||||
self.logger.info(f"Cursor密码: {result['cursor_password']}")
|
||||
# 只显示token的前30个字符,避免日志过长
|
||||
token = result.get('cursor_jwt', '')
|
||||
if token:
|
||||
self.logger.info(f"Token: {token[:30]}...")
|
||||
self.logger.info("=" * 50)
|
||||
|
||||
# 6. 直接上传成功注册的账号
|
||||
if successful:
|
||||
try:
|
||||
service = AutoCursorService()
|
||||
await service.initialize()
|
||||
|
||||
# 准备上传数据
|
||||
upload_data = []
|
||||
for account in successful:
|
||||
upload_item = {
|
||||
"email": account["email"],
|
||||
"password": account.get("email_password", ""), # 使用get并提供默认值
|
||||
"cursor_password": account["cursor_password"],
|
||||
"cursor_cookie": account["cursor_cookie"],
|
||||
"cursor_jwt": account.get("cursor_jwt", "")
|
||||
}
|
||||
upload_data.append(upload_item)
|
||||
|
||||
# 上传账号
|
||||
upload_result = await service.upload_accounts(upload_data)
|
||||
await service.cleanup()
|
||||
|
||||
if upload_result:
|
||||
self.logger.info(f"成功上传 {len(upload_data)} 个账号到服务器")
|
||||
else:
|
||||
self.logger.error(f"账号上传失败,请检查日志了解详细信息")
|
||||
except Exception as e:
|
||||
self.logger.error(f"上传账号时发生错误: {str(e)}")
|
||||
|
||||
self.logger.info(f"注册完成: 成功 {len(successful)}, 失败 {len(failed)}, 跳过 {skipped}")
|
||||
return successful
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"批量注册失败: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
async def main():
|
||||
register = CursorRegister()
|
||||
@@ -143,19 +269,18 @@ async def main():
|
||||
batch_size = register.config.register_config.batch_size
|
||||
total_registered = 0
|
||||
|
||||
# 直接使用自建邮箱模式,不再检查配置
|
||||
register.logger.info("使用自建邮箱模式")
|
||||
|
||||
# 确保已初始化自建邮箱服务
|
||||
if not register.host_register_worker:
|
||||
register.logger.error("自建邮箱注册工作器未初始化,请检查配置")
|
||||
return
|
||||
|
||||
# 自建邮箱模式,直接执行自建邮箱批量注册
|
||||
while True:
|
||||
# 检查是否还有可用的邮箱账号
|
||||
available_accounts = await register.email_manager.batch_get_accounts(1)
|
||||
if not available_accounts:
|
||||
register.logger.info("没有更多可用的邮箱账号,注册完成")
|
||||
break
|
||||
|
||||
# 释放检查用的账号
|
||||
await register.email_manager.update_account_status(available_accounts[0].id, 'pending')
|
||||
|
||||
# 执行批量注册
|
||||
register.logger.info(f"开始新一轮批量注册,批次大小: {batch_size}")
|
||||
results = await register.batch_register(batch_size)
|
||||
register.logger.info(f"开始新一轮自建邮箱批量注册,批次大小: {batch_size}")
|
||||
results = await register.batch_register_with_host(batch_size)
|
||||
|
||||
# 统计结果
|
||||
successful = len(results)
|
||||
@@ -163,13 +288,26 @@ async def main():
|
||||
|
||||
register.logger.info(f"当前总进度: 已注册 {total_registered} 个账号")
|
||||
|
||||
# 批次之间的间隔
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# 如果本批次注册失败率过高,暂停一段时间
|
||||
if successful < batch_size * 0.5: # 成功率低于50%
|
||||
if successful < batch_size * 0.5 and successful > 0: # 成功率低于50%但不为零
|
||||
register.logger.warning("本批次成功率过低,暂停60秒后继续")
|
||||
await asyncio.sleep(60)
|
||||
else:
|
||||
# 正常等待一个较短的时间再继续下一批
|
||||
await asyncio.sleep(5)
|
||||
elif successful == 0 and batch_size > 0: # 完全失败
|
||||
register.logger.error("本批次完全失败,可能存在系统问题,暂停120秒后继续")
|
||||
await asyncio.sleep(120)
|
||||
|
||||
# 让用户决定是否继续
|
||||
try:
|
||||
answer = input("是否继续下一批注册? (y/n): ").strip().lower()
|
||||
if answer != 'y':
|
||||
register.logger.info("用户选择停止注册")
|
||||
break
|
||||
except Exception:
|
||||
# 如果在非交互环境中运行,默认继续
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
register.logger.error(f"程序执行出错: {str(e)}")
|
||||
|
||||
179
migrate_db.py
Normal file
179
migrate_db.py
Normal file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SQLite 到 MySQL 数据迁移脚本
|
||||
用于将旧的 SQLite 数据库迁移到新的 MySQL 数据库
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sqlite3
|
||||
import sys
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import aiomysql
|
||||
import yaml
|
||||
from loguru import logger
|
||||
|
||||
|
||||
async def migrate_data():
|
||||
"""迁移数据主函数"""
|
||||
# 读取配置文件
|
||||
logger.info("读取配置文件")
|
||||
with open("config.yaml", "r", encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# 获取数据库配置
|
||||
db_config = config.get("database", {})
|
||||
sqlite_path = db_config.get("path", "cursor.db")
|
||||
mysql_config = {
|
||||
"host": db_config.get("host", "localhost"),
|
||||
"port": db_config.get("port", 3306),
|
||||
"user": db_config.get("username", "auto_cursor_reg"), # 使用正确的默认用户名
|
||||
"password": db_config.get("password", "this_password_jiaqiao"), # 使用正确的默认密码
|
||||
"db": db_config.get("database", "auto_cursor_reg"), # 使用正确的默认数据库名
|
||||
"charset": "utf8mb4"
|
||||
}
|
||||
|
||||
logger.info(f"MySQL配置: 主机={mysql_config['host']}:{mysql_config['port']}, 用户={mysql_config['user']}, 数据库={mysql_config['db']}")
|
||||
|
||||
# 连接SQLite数据库
|
||||
logger.info(f"连接SQLite数据库: {sqlite_path}")
|
||||
sqlite_conn = None
|
||||
mysql_conn = None
|
||||
|
||||
try:
|
||||
sqlite_conn = sqlite3.connect(sqlite_path)
|
||||
sqlite_conn.row_factory = sqlite3.Row # 启用字典行工厂
|
||||
except Exception as e:
|
||||
logger.error(f"无法连接SQLite数据库: {e}")
|
||||
return
|
||||
|
||||
# 连接MySQL数据库
|
||||
logger.info(f"尝试连接MySQL数据库...")
|
||||
try:
|
||||
mysql_conn = await aiomysql.connect(**mysql_config)
|
||||
logger.info("MySQL数据库连接成功")
|
||||
except Exception as e:
|
||||
logger.error(f"无法连接MySQL数据库: {e}")
|
||||
logger.error(f"请确认MySQL服务已启动且用户名密码正确")
|
||||
logger.info(f"您可能需要创建MySQL用户和数据库:")
|
||||
logger.info(f" CREATE USER '{mysql_config['user']}'@'localhost' IDENTIFIED BY '{mysql_config['password']}';")
|
||||
logger.info(f" CREATE DATABASE {mysql_config['db']} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;")
|
||||
logger.info(f" GRANT ALL PRIVILEGES ON {mysql_config['db']}.* TO '{mysql_config['user']}'@'localhost';")
|
||||
logger.info(f" FLUSH PRIVILEGES;")
|
||||
if sqlite_conn:
|
||||
sqlite_conn.close()
|
||||
return
|
||||
|
||||
try:
|
||||
# 检查email_accounts表是否存在
|
||||
logger.info("检查并创建MySQL表结构")
|
||||
async with mysql_conn.cursor() as cursor:
|
||||
await cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS email_accounts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT NOT NULL,
|
||||
in_use BOOLEAN DEFAULT 0,
|
||||
cursor_password VARCHAR(255),
|
||||
cursor_cookie TEXT,
|
||||
cursor_token TEXT,
|
||||
sold BOOLEAN DEFAULT 0,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
extracted BOOLEAN DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_status_inuse_sold (status, in_use, sold),
|
||||
INDEX idx_extracted (extracted, status, sold)
|
||||
)
|
||||
''')
|
||||
await mysql_conn.commit()
|
||||
|
||||
# 从SQLite读取数据
|
||||
logger.info("从SQLite读取数据")
|
||||
try:
|
||||
sqlite_cursor = sqlite_conn.cursor()
|
||||
sqlite_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='email_accounts'")
|
||||
if not sqlite_cursor.fetchone():
|
||||
logger.warning("SQLite数据库中不存在email_accounts表,无需迁移")
|
||||
return
|
||||
|
||||
# 获取表结构信息
|
||||
sqlite_cursor.execute("PRAGMA table_info(email_accounts)")
|
||||
columns_info = sqlite_cursor.fetchall()
|
||||
column_names = [col["name"] for col in columns_info]
|
||||
|
||||
# 读取所有数据
|
||||
sqlite_cursor.execute("SELECT * FROM email_accounts")
|
||||
rows = sqlite_cursor.fetchall()
|
||||
logger.info(f"从SQLite读取到 {len(rows)} 条数据")
|
||||
except Exception as e:
|
||||
logger.error(f"读取SQLite数据失败: {e}")
|
||||
return
|
||||
|
||||
# 迁移数据到MySQL
|
||||
if rows:
|
||||
logger.info("开始迁移数据到MySQL")
|
||||
try:
|
||||
async with mysql_conn.cursor() as cursor:
|
||||
# 检查MySQL表中是否已有数据
|
||||
await cursor.execute("SELECT COUNT(*) FROM email_accounts")
|
||||
result = await cursor.fetchone()
|
||||
if result and result[0] > 0:
|
||||
logger.warning("MySQL表中已存在数据,是否继续?(y/n)")
|
||||
response = input().strip().lower()
|
||||
if response != 'y':
|
||||
logger.info("用户取消迁移")
|
||||
return
|
||||
|
||||
# 构建插入查询
|
||||
placeholders = ", ".join(["%s"] * len(column_names))
|
||||
columns = ", ".join(column_names)
|
||||
query = f"INSERT INTO email_accounts ({columns}) VALUES ({placeholders}) ON DUPLICATE KEY UPDATE id=id"
|
||||
|
||||
# 批量插入数据
|
||||
batch_size = 100
|
||||
for i in range(0, len(rows), batch_size):
|
||||
batch = rows[i:i+batch_size]
|
||||
batch_values = []
|
||||
for row in batch:
|
||||
# 将行数据转换为列表
|
||||
row_values = [row[name] for name in column_names]
|
||||
batch_values.append(row_values)
|
||||
|
||||
await cursor.executemany(query, batch_values)
|
||||
await mysql_conn.commit()
|
||||
logger.info(f"已迁移 {min(i+batch_size, len(rows))}/{len(rows)} 条数据")
|
||||
|
||||
logger.success(f"数据迁移完成,共迁移 {len(rows)} 条数据")
|
||||
except Exception as e:
|
||||
logger.error(f"迁移数据到MySQL失败: {e}")
|
||||
else:
|
||||
logger.warning("SQLite数据库中没有数据需要迁移")
|
||||
|
||||
finally:
|
||||
# 安全关闭连接
|
||||
if sqlite_conn:
|
||||
sqlite_conn.close()
|
||||
logger.debug("SQLite连接已关闭")
|
||||
|
||||
if mysql_conn:
|
||||
await mysql_conn.close()
|
||||
logger.debug("MySQL连接已关闭")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Windows平台特殊处理,强制使用SelectorEventLoop
|
||||
if sys.platform.startswith('win'):
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
# 设置日志
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="INFO")
|
||||
logger.add("migrate.log", rotation="1 MB", level="DEBUG")
|
||||
|
||||
# 执行迁移
|
||||
logger.info("开始执行数据迁移")
|
||||
asyncio.run(migrate_data())
|
||||
logger.info("数据迁移脚本执行完毕")
|
||||
421
register/host_register_worker.py
Normal file
421
register/host_register_worker.py
Normal file
@@ -0,0 +1,421 @@
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
from typing import Optional, Tuple, Dict
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
from core.exceptions import RegisterError
|
||||
from services.fetch_manager import FetchManager
|
||||
from services.self_hosted_email import SelfHostedEmail
|
||||
from services.uuid import ULID
|
||||
|
||||
|
||||
def extract_jwt(cookie_string: str) -> str:
|
||||
"""从cookie字符串中提取JWT token"""
|
||||
try:
|
||||
return cookie_string.split(';')[0].split('=')[1].split('%3A%3A')[1]
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 提取JWT失败: {str(e)}")
|
||||
return ""
|
||||
|
||||
class FormBuilder:
|
||||
@staticmethod
|
||||
def _generate_password() -> str:
|
||||
"""生成随机密码
|
||||
规则: 12-16位,包含大小写字母、数字和特殊字符
|
||||
"""
|
||||
length = random.randint(12, 16)
|
||||
lowercase = string.ascii_lowercase
|
||||
uppercase = string.ascii_uppercase
|
||||
digits = string.digits
|
||||
special = "!@#$%^&*"
|
||||
|
||||
# 确保每种字符至少有一个
|
||||
password = [
|
||||
random.choice(lowercase),
|
||||
random.choice(uppercase),
|
||||
random.choice(digits),
|
||||
random.choice(special)
|
||||
]
|
||||
|
||||
# 填充剩余长度
|
||||
all_chars = lowercase + uppercase + digits + special
|
||||
password.extend(random.choice(all_chars) for _ in range(length - 4))
|
||||
|
||||
# 打乱顺序
|
||||
random.shuffle(password)
|
||||
return ''.join(password)
|
||||
|
||||
@staticmethod
|
||||
def _generate_name() -> tuple[str, str]:
|
||||
"""生成随机的名字和姓氏
|
||||
Returns:
|
||||
tuple: (first_name, last_name)
|
||||
"""
|
||||
first_names = ["Alex", "Sam", "Chris", "Jordan", "Taylor", "Morgan", "Casey", "Drew", "Pat", "Quinn"]
|
||||
last_names = ["Smith", "Johnson", "Brown", "Davis", "Wilson", "Moore", "Taylor", "Anderson", "Thomas", "Jackson"]
|
||||
|
||||
return (
|
||||
random.choice(first_names),
|
||||
random.choice(last_names)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_register_form(boundary: str, email: str, token: str) -> tuple[str, str]:
|
||||
"""构建注册表单数据,返回(form_data, password)"""
|
||||
password = FormBuilder._generate_password()
|
||||
first_name, last_name = FormBuilder._generate_name()
|
||||
|
||||
fields = {
|
||||
"1_state": "{\"returnTo\":\"/settings\"}",
|
||||
"1_redirect_uri": "https://cursor.com/api/auth/callback",
|
||||
"1_bot_detection_token": token,
|
||||
"1_first_name": first_name,
|
||||
"1_last_name": last_name,
|
||||
"1_email": email,
|
||||
"1_password": password,
|
||||
"1_intent": "sign-up",
|
||||
"0": "[\"$K1\"]"
|
||||
}
|
||||
|
||||
form_data = []
|
||||
for key, value in fields.items():
|
||||
form_data.append(f'--{boundary}')
|
||||
form_data.append(f'Content-Disposition: form-data; name="{key}"')
|
||||
form_data.append('')
|
||||
form_data.append(value)
|
||||
|
||||
form_data.append(f'--{boundary}--')
|
||||
return '\r\n'.join(form_data), password
|
||||
|
||||
@staticmethod
|
||||
def build_verify_form(boundary: str, email: str, token: str, code: str, pending_token: str) -> str:
|
||||
"""构建验证表单数据"""
|
||||
fields = {
|
||||
"1_pending_authentication_token": pending_token,
|
||||
"1_email": email,
|
||||
"1_state": "{\"returnTo\":\"/settings\"}",
|
||||
"1_redirect_uri": "https://cursor.com/api/auth/callback",
|
||||
"1_bot_detection_token": token,
|
||||
"1_code": code,
|
||||
"0": "[\"$K1\"]"
|
||||
}
|
||||
|
||||
form_data = []
|
||||
for key, value in fields.items():
|
||||
form_data.append(f'--{boundary}')
|
||||
form_data.append(f'Content-Disposition: form-data; name="{key}"')
|
||||
form_data.append('')
|
||||
form_data.append(value)
|
||||
|
||||
form_data.append(f'--{boundary}--')
|
||||
return '\r\n'.join(form_data)
|
||||
|
||||
|
||||
class HostRegisterWorker:
|
||||
"""自建邮箱注册工作器,使用自建邮箱完成注册流程"""
|
||||
|
||||
def __init__(self, config: Config, fetch_manager: FetchManager, self_hosted_email: SelfHostedEmail):
|
||||
self.config = config
|
||||
self.fetch_manager = fetch_manager
|
||||
self.self_hosted_email = self_hosted_email
|
||||
self.form_builder = FormBuilder()
|
||||
self.uuid = ULID()
|
||||
|
||||
async def random_delay(self):
|
||||
delay = random.uniform(*self.config.register_config.delay_range)
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
@staticmethod
|
||||
async def _extract_auth_token(response_text: str) -> str | None:
|
||||
"""从响应文本中提取pending_authentication_token"""
|
||||
res = response_text.split('\n')
|
||||
logger.debug(f"开始提取 auth_token,响应行数: {len(res)}")
|
||||
|
||||
# 检查邮箱是否可用
|
||||
for line in res:
|
||||
if '"code":"email_not_available"' in line:
|
||||
logger.error("不受支持的邮箱")
|
||||
raise RegisterError("Email is not available")
|
||||
|
||||
# 像register_worker.py中一样使用简单的路径
|
||||
try:
|
||||
for i, r in enumerate(res):
|
||||
if r.startswith('0:'):
|
||||
logger.debug(f"在第 {i+1} 行找到匹配")
|
||||
data = json.loads(r.split('0:')[1])
|
||||
# 使用完全相同的路径
|
||||
auth_data = data[1][0][0][1]["children"][1]["children"][1]["children"][1]["children"][0]
|
||||
params_str = auth_data.split('?')[1]
|
||||
params_dict = json.loads(params_str)
|
||||
token = params_dict['pending_authentication_token']
|
||||
logger.debug(f"提取成功: {token[:10]}...")
|
||||
return token
|
||||
except Exception as e:
|
||||
logger.error(f"提取token失败: {str(e)}")
|
||||
logger.debug(f"响应内容预览: {response_text[:200]}...")
|
||||
# 保存完整响应到文件以便调试
|
||||
try:
|
||||
with open('debug_response.txt', 'w', encoding='utf-8') as f:
|
||||
f.write(response_text)
|
||||
logger.debug("完整响应已保存到debug_response.txt")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 尝试备选方法 - 在整个响应文本中查找token
|
||||
try:
|
||||
import re
|
||||
match = re.search(r'pending_authentication_token["\']?\s*[:=]\s*["\']?([^"\'&,\s]+)["\']?', response_text)
|
||||
if match:
|
||||
token = match.group(1)
|
||||
logger.debug(f"使用正则表达式提取成功: {token[:10]}...")
|
||||
return token
|
||||
except Exception as e:
|
||||
logger.error(f"正则表达式提取失败: {str(e)}")
|
||||
|
||||
return None
|
||||
|
||||
async def register(self, proxy: str, token_pair: Tuple[str, str]) -> Optional[Dict]:
|
||||
"""使用自建邮箱完成注册流程"""
|
||||
if not self.self_hosted_email:
|
||||
raise RegisterError("自建邮箱服务未配置")
|
||||
|
||||
token1, token2 = token_pair
|
||||
session_id = self.uuid.generate()
|
||||
|
||||
try:
|
||||
# 从自建邮箱API获取邮箱
|
||||
email = await self.self_hosted_email.get_email()
|
||||
if not email:
|
||||
raise RegisterError("获取自建邮箱失败")
|
||||
|
||||
logger.info(f"开始使用自建邮箱注册: {email}")
|
||||
|
||||
# 第一次注册请求
|
||||
email, pending_token, cursor_password = await self._first_register(
|
||||
proxy,
|
||||
token1,
|
||||
email,
|
||||
session_id
|
||||
)
|
||||
|
||||
await self.random_delay()
|
||||
|
||||
# 从自建邮箱API获取验证码
|
||||
verification_code = await self.self_hosted_email.get_verification_code(email)
|
||||
|
||||
if not verification_code:
|
||||
logger.error(f"自建邮箱 {email} 获取验证码失败")
|
||||
raise RegisterError("获取验证码失败")
|
||||
|
||||
logger.debug(f"自建邮箱 {email} 获取到验证码: {verification_code}")
|
||||
|
||||
await self.random_delay()
|
||||
|
||||
# 验证码验证
|
||||
redirect_url = await self._verify_code(
|
||||
proxy=proxy,
|
||||
token=token2,
|
||||
code=verification_code,
|
||||
pending_token=pending_token,
|
||||
email=email,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
if not redirect_url:
|
||||
raise RegisterError("未找到重定向URL")
|
||||
|
||||
await self.random_delay()
|
||||
|
||||
# callback请求
|
||||
cookies = await self._callback(proxy, redirect_url)
|
||||
if not cookies:
|
||||
raise RegisterError("获取cookies失败")
|
||||
|
||||
# 提取JWT
|
||||
jwt_token = extract_jwt(cookies)
|
||||
|
||||
logger.success(f"自建邮箱账号 {email} 注册成功")
|
||||
return {
|
||||
'email': email,
|
||||
'email_password': '', # 添加email_password字段,可以为空字符串
|
||||
'cursor_password': cursor_password,
|
||||
'cursor_cookie': cookies,
|
||||
'cursor_jwt': jwt_token
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"自建邮箱账号注册失败: {str(e)}")
|
||||
raise RegisterError(f"注册失败: {str(e)}")
|
||||
|
||||
async def _first_register(
|
||||
self,
|
||||
proxy: str,
|
||||
token: str,
|
||||
email: str,
|
||||
session_id: str
|
||||
) -> tuple[str, str, str]:
|
||||
"""自建邮箱的第一次注册请求"""
|
||||
logger.debug(f"开始第一次注册请求 - 自建邮箱: {email}, 代理: {proxy}")
|
||||
|
||||
first_name, last_name = self.form_builder._generate_name()
|
||||
|
||||
# 在headers中定义boundary
|
||||
boundary = "----WebKitFormBoundary2rKlvTagBEhneWi3"
|
||||
headers = {
|
||||
"accept": "text/x-component",
|
||||
"next-action": "770926d8148e29539286d20e1c1548d2aff6c0b9",
|
||||
"content-type": f"multipart/form-data; boundary={boundary}",
|
||||
"origin": "https://authenticator.cursor.sh",
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-origin",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
params = {
|
||||
"first_name": first_name,
|
||||
"last_name": last_name,
|
||||
"email": email,
|
||||
"state": "%7B%22returnTo%22%3A%22%2Fsettings%22%7D",
|
||||
"redirect_uri": "https://cursor.com/api/auth/callback",
|
||||
}
|
||||
|
||||
# 构建form数据
|
||||
form_data, cursor_password = self.form_builder.build_register_form(boundary, email, token)
|
||||
|
||||
response = await self.fetch_manager.request(
|
||||
"POST",
|
||||
"https://authenticator.cursor.sh/sign-up/password",
|
||||
headers=headers,
|
||||
params=params,
|
||||
data=form_data,
|
||||
proxy=proxy
|
||||
)
|
||||
|
||||
if 'error' in response:
|
||||
raise RegisterError(f"First register request failed: {response['error']}")
|
||||
|
||||
text = response['body'].decode()
|
||||
|
||||
# 使用更简单的方法提取token,与register_worker.py相同的方式
|
||||
pending_token = await self._extract_auth_token(text)
|
||||
if not pending_token:
|
||||
raise RegisterError("Failed to extract auth token")
|
||||
|
||||
logger.debug(f"第一次请求完成 - pending_token: {pending_token[:10]}...")
|
||||
return email, pending_token, cursor_password
|
||||
|
||||
async def _verify_code(
|
||||
self,
|
||||
proxy: str,
|
||||
token: str,
|
||||
code: str,
|
||||
pending_token: str,
|
||||
email: str,
|
||||
session_id: str
|
||||
) -> str:
|
||||
"""验证码验证请求"""
|
||||
logger.debug(f"开始验证码验证 - 邮箱: {email}, 验证码: {code}")
|
||||
|
||||
boundary = "----WebKitFormBoundaryqEBf0rEYwwb9aUoF"
|
||||
headers = {
|
||||
"accept": "text/x-component",
|
||||
"content-type": f"multipart/form-data; boundary={boundary}",
|
||||
"next-action": "e75011da58d295bef5aa55740d0758a006468655",
|
||||
"origin": "https://authenticator.cursor.sh",
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-origin",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
params = {
|
||||
"email": email,
|
||||
"pending_authentication_token": pending_token,
|
||||
"state": "%7B%22returnTo%22%3A%22%2Fsettings%22%7D",
|
||||
"redirect_uri": "https://cursor.com/api/auth/callback",
|
||||
"authorization_session_id": session_id
|
||||
}
|
||||
|
||||
form_data = self.form_builder.build_verify_form(
|
||||
boundary=boundary,
|
||||
email=email,
|
||||
token=token,
|
||||
code=code,
|
||||
pending_token=pending_token,
|
||||
)
|
||||
|
||||
response = await self.fetch_manager.request(
|
||||
"POST",
|
||||
"https://authenticator.cursor.sh/email-verification",
|
||||
headers=headers,
|
||||
params=params,
|
||||
data=form_data,
|
||||
proxy=proxy
|
||||
)
|
||||
|
||||
redirect_url = response.get('headers', {}).get('x-action-redirect')
|
||||
if not redirect_url:
|
||||
logger.error(f"未找到重定向URL,响应头: {json.dumps(response.get('headers', {}))}")
|
||||
# 尝试从响应体中提取
|
||||
body = response.get('body', b'').decode()
|
||||
if 'redirect' in body.lower():
|
||||
logger.debug("尝试从响应体中提取重定向URL")
|
||||
import re
|
||||
match = re.search(r'redirect[^"\']*["\']([^"\']+)["\']', body)
|
||||
if match:
|
||||
redirect_url = match.group(1)
|
||||
logger.debug(f"从响应体提取到重定向URL: {redirect_url}")
|
||||
|
||||
if not redirect_url:
|
||||
raise RegisterError("未找到重定向URL,响应头: %s" % json.dumps(response.get('headers')))
|
||||
|
||||
return redirect_url
|
||||
|
||||
async def _callback(self, proxy: str, redirect_url: str) -> str:
|
||||
"""Callback请求"""
|
||||
logger.debug(f"开始callback请求 - URL: {redirect_url[:50]}...")
|
||||
|
||||
parsed = urlparse(redirect_url)
|
||||
code = parse_qs(parsed.query)['code'][0]
|
||||
logger.debug(f"从URL提取的code: {code[:10]}...")
|
||||
|
||||
headers = {
|
||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||
"accept-language": "zh-CN,zh;q=0.9",
|
||||
"sec-fetch-dest": "document",
|
||||
"sec-fetch-mode": "navigate",
|
||||
"sec-fetch-site": "cross-site",
|
||||
"upgrade-insecure-requests": "1",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
callback_url = "https://www.cursor.com/api/auth/callback"
|
||||
params = {
|
||||
"code": code,
|
||||
"state": "%7B%22returnTo%22%3A%22%2Fsettings%22%7D"
|
||||
}
|
||||
|
||||
response = await self.fetch_manager.request(
|
||||
"GET",
|
||||
callback_url,
|
||||
headers=headers,
|
||||
params=params,
|
||||
proxy=proxy,
|
||||
allow_redirects=False
|
||||
)
|
||||
|
||||
if 'error' in response:
|
||||
raise RegisterError(f"Callback request failed: {response['error']}")
|
||||
|
||||
cookies = response['headers'].get('set-cookie')
|
||||
if cookies:
|
||||
logger.debug("成功获取到cookies")
|
||||
else:
|
||||
logger.error("未获取到cookies")
|
||||
return cookies
|
||||
@@ -22,6 +22,13 @@ python-dateutil
|
||||
|
||||
# Database
|
||||
aiosqlite
|
||||
aiomysql>=0.1.1
|
||||
pymysql>=1.0.2
|
||||
|
||||
# Redis - 使用其中一个
|
||||
# 如果使用Python 3.12,推荐使用redis-py而不是aioredis
|
||||
redis>=4.2.0
|
||||
# aioredis>=2.0.0 # 不建议在Python 3.12上使用
|
||||
|
||||
# Logging
|
||||
loguru==0.7.2
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Dict, List, Optional
|
||||
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
import aiomysql
|
||||
|
||||
from core.config import Config
|
||||
from core.database import DatabaseManager
|
||||
@@ -34,72 +35,364 @@ class EmailManager:
|
||||
"Verify your email address",
|
||||
"Complete code challenge",
|
||||
]
|
||||
# Redis相关配置
|
||||
self.use_redis = False
|
||||
if hasattr(self.db, 'redis') and self.db.redis:
|
||||
self.use_redis = True
|
||||
logger.info("Redis可用,将使用Redis进行邮箱状态管理")
|
||||
else:
|
||||
logger.warning("Redis不可用,将使用MySQL进行邮箱状态管理")
|
||||
|
||||
# Redis键前缀
|
||||
self.redis_prefix = "emailmanager:"
|
||||
# 锁和黑名单的过期时间(秒)
|
||||
self.lock_timeout = 600 # 10分钟
|
||||
self.blacklist_timeout = 86400 * 30 # 30天
|
||||
# 黑名单初始化标记
|
||||
self.blacklist_initialized = False
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化EmailManager,确保在使用前完成必要的设置"""
|
||||
if self.use_redis:
|
||||
await self._ensure_blacklist_initialized()
|
||||
self.blacklist_initialized = True
|
||||
|
||||
async def _ensure_blacklist_initialized(self):
|
||||
"""确保黑名单已经初始化"""
|
||||
if not self.use_redis:
|
||||
return False
|
||||
|
||||
blacklist_key = f"{self.redis_prefix}blacklist:initialized"
|
||||
initialized = await self.db.redis.exists(blacklist_key)
|
||||
|
||||
if not initialized:
|
||||
logger.info("初始化Redis邮箱黑名单...")
|
||||
# 查询所有已成功或不可用的邮箱
|
||||
query = """
|
||||
SELECT DISTINCT email
|
||||
FROM email_accounts
|
||||
WHERE status = 'success' OR status = 'unavailable'
|
||||
"""
|
||||
results = await self.db.fetch_all(query)
|
||||
|
||||
if results:
|
||||
# 批量添加到黑名单
|
||||
blacklist_key = f"{self.redis_prefix}blacklist:emails"
|
||||
emails = [row['email'] for row in results]
|
||||
if emails:
|
||||
pipeline = self.db.redis.pipeline()
|
||||
for email in emails:
|
||||
pipeline.sadd(blacklist_key, email)
|
||||
pipeline.expire(blacklist_key, self.blacklist_timeout)
|
||||
await pipeline.execute()
|
||||
|
||||
logger.info(f"已将 {len(emails)} 个邮箱添加到黑名单")
|
||||
|
||||
# 标记为已初始化
|
||||
await self.db.redis.setex(f"{self.redis_prefix}blacklist:initialized", self.blacklist_timeout, "1")
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
async def is_email_blacklisted(self, email: str) -> bool:
|
||||
"""检查邮箱是否在黑名单中"""
|
||||
if not self.use_redis:
|
||||
# 回退到数据库查询
|
||||
query = """
|
||||
SELECT 1 FROM email_accounts
|
||||
WHERE email = %s AND (status = 'success' OR status = 'unavailable')
|
||||
LIMIT 1
|
||||
"""
|
||||
result = await self.db.fetch_one(query, (email,))
|
||||
return result is not None
|
||||
|
||||
# 使用Redis SET存储黑名单
|
||||
blacklist_key = f"{self.redis_prefix}blacklist:emails"
|
||||
return await self.db.redis.sismember(blacklist_key, email)
|
||||
|
||||
async def add_email_to_blacklist(self, email: str):
|
||||
"""将邮箱添加到黑名单"""
|
||||
if not self.use_redis:
|
||||
return False
|
||||
|
||||
blacklist_key = f"{self.redis_prefix}blacklist:emails"
|
||||
await self.db.redis.sadd(blacklist_key, email)
|
||||
await self.db.redis.expire(blacklist_key, self.blacklist_timeout)
|
||||
logger.debug(f"邮箱 {email} 已添加到黑名单")
|
||||
return True
|
||||
|
||||
async def lock_account(self, account_id: int) -> bool:
|
||||
"""锁定账号"""
|
||||
if not self.use_redis:
|
||||
# 如果不使用Redis,通过数据库更新来锁定
|
||||
try:
|
||||
query = """
|
||||
UPDATE email_accounts
|
||||
SET in_use = 1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = %s AND in_use = 0
|
||||
"""
|
||||
affected = await self.db.execute(query, (account_id,))
|
||||
return affected > 0
|
||||
except Exception as e:
|
||||
logger.error(f"通过数据库锁定账号 {account_id} 失败: {e}")
|
||||
return False
|
||||
|
||||
# 使用Redis实现分布式锁
|
||||
lock_key = f"{self.redis_prefix}lock:account:{account_id}"
|
||||
locked = await self.db.redis.setnx(lock_key, "1")
|
||||
if locked:
|
||||
# 锁定成功,设置过期时间
|
||||
await self.db.redis.expire(lock_key, self.lock_timeout)
|
||||
|
||||
# 同时更新数据库状态
|
||||
try:
|
||||
update_query = """
|
||||
UPDATE email_accounts
|
||||
SET in_use = 1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = %s
|
||||
"""
|
||||
await self.db.execute(update_query, (account_id,))
|
||||
except Exception as e:
|
||||
# 如果数据库更新失败,释放Redis锁
|
||||
logger.error(f"锁定账号 {account_id} 后更新数据库失败: {e}")
|
||||
await self.db.redis.delete(lock_key)
|
||||
return False
|
||||
|
||||
logger.debug(f"账号 {account_id} 已锁定")
|
||||
return locked
|
||||
|
||||
async def unlock_account(self, account_id: int) -> bool:
|
||||
"""解锁账号"""
|
||||
# 无论是否使用Redis,都更新数据库
|
||||
try:
|
||||
update_query = """
|
||||
UPDATE email_accounts
|
||||
SET in_use = 0, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = %s
|
||||
"""
|
||||
await self.db.execute(update_query, (account_id,))
|
||||
except Exception as e:
|
||||
logger.error(f"解锁账号 {account_id} 更新数据库失败: {e}")
|
||||
|
||||
if not self.use_redis:
|
||||
return True
|
||||
|
||||
# 删除Redis锁
|
||||
lock_key = f"{self.redis_prefix}lock:account:{account_id}"
|
||||
deleted = await self.db.redis.delete(lock_key)
|
||||
logger.debug(f"账号 {account_id} 锁已释放")
|
||||
return deleted > 0
|
||||
|
||||
async def batch_get_accounts(self, num: int) -> List[EmailAccount]:
|
||||
"""批量获取未使用的邮箱账号"""
|
||||
logger.info(f"尝试获取 {num} 个未使用的邮箱账号")
|
||||
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET in_use = 1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id IN (
|
||||
SELECT id FROM email_accounts
|
||||
WHERE in_use = 0 AND sold = 0 AND status = 'pending'
|
||||
LIMIT ?
|
||||
)
|
||||
RETURNING id, email, password, client_id, refresh_token
|
||||
'''
|
||||
# 如果使用Redis,确保黑名单已初始化
|
||||
if self.use_redis:
|
||||
await self._ensure_blacklist_initialized()
|
||||
|
||||
results = await self.db.fetch_all(query, (num,))
|
||||
logger.debug(f"实际获取到 {len(results)} 个账号")
|
||||
return [
|
||||
EmailAccount(
|
||||
id=row[0],
|
||||
email=row[1],
|
||||
password=row[2],
|
||||
client_id=row[3],
|
||||
refresh_token=row[4],
|
||||
# 1. 先从数据库中获取候选账号
|
||||
select_query = """
|
||||
SELECT id, email, password, client_id, refresh_token
|
||||
FROM email_accounts
|
||||
WHERE in_use = 0 AND sold = 0 AND status = 'pending'
|
||||
LIMIT %s
|
||||
"""
|
||||
# 多获取一些候选账号,防止有些被排除
|
||||
candidate_accounts = await self.db.fetch_all(select_query, (num * 2,))
|
||||
|
||||
if not candidate_accounts:
|
||||
logger.debug("没有找到符合条件的候选账号")
|
||||
return []
|
||||
|
||||
logger.debug(f"找到 {len(candidate_accounts)} 个候选账号")
|
||||
|
||||
# 2. 筛选并锁定账号
|
||||
result_accounts = []
|
||||
for account in candidate_accounts:
|
||||
# 检查邮箱是否在黑名单中
|
||||
if await self.is_email_blacklisted(account['email']):
|
||||
logger.debug(f"邮箱 {account['email']} 在黑名单中,跳过")
|
||||
continue
|
||||
|
||||
# 尝试锁定账号
|
||||
if await self.lock_account(account['id']):
|
||||
# 添加到结果列表
|
||||
result_accounts.append(EmailAccount(
|
||||
id=account['id'],
|
||||
email=account['email'],
|
||||
password=account['password'],
|
||||
client_id=account['client_id'],
|
||||
refresh_token=account['refresh_token'],
|
||||
in_use=True
|
||||
)
|
||||
for row in results
|
||||
]
|
||||
))
|
||||
|
||||
# 如果已经获取足够的账号,退出循环
|
||||
if len(result_accounts) >= num:
|
||||
break
|
||||
else:
|
||||
logger.debug(f"账号 {account['id']} 锁定失败,可能被其他进程使用")
|
||||
|
||||
logger.info(f"实际获取到 {len(result_accounts)} 个可用账号")
|
||||
|
||||
# 如果账号数量不足,尝试清理长时间锁定但未更新的账号
|
||||
if len(result_accounts) < num and len(result_accounts) < len(candidate_accounts):
|
||||
logger.warning("可用账号不足,尝试清理长时间锁定的账号")
|
||||
await self._cleanup_stuck_accounts()
|
||||
|
||||
return result_accounts
|
||||
|
||||
async def _cleanup_stuck_accounts(self):
|
||||
"""清理长时间锁定但未更新的账号"""
|
||||
try:
|
||||
# 清理超过30分钟未更新且仍标记为in_use=1的账号
|
||||
cleanup_query = """
|
||||
UPDATE email_accounts
|
||||
SET in_use = 0
|
||||
WHERE in_use = 1
|
||||
AND updated_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE)
|
||||
"""
|
||||
affected = await self.db.execute(cleanup_query)
|
||||
if affected > 0:
|
||||
logger.info(f"已清理 {affected} 个长时间锁定的账号")
|
||||
|
||||
# 如果使用Redis,同时清理对应的锁
|
||||
if self.use_redis:
|
||||
# 清理可能存在的Redis锁,但这需要知道具体的account_id
|
||||
# 这里简化处理,依赖锁的自动过期机制
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"清理账号时出错: {e}")
|
||||
|
||||
async def update_account_status(self, account_id: int, status: str):
|
||||
"""更新账号状态"""
|
||||
try:
|
||||
# 获取账号邮箱信息(用于黑名单)
|
||||
account_query = "SELECT email, status as current_status FROM email_accounts WHERE id = %s"
|
||||
account_info = await self.db.fetch_one(account_query, (account_id,))
|
||||
|
||||
if not account_info:
|
||||
logger.error(f"账号 {account_id} 不存在")
|
||||
return
|
||||
|
||||
# 检查状态变更是否合理
|
||||
current_status = account_info.get('current_status')
|
||||
if current_status == 'success' and status != 'success':
|
||||
logger.warning(f"警告: 尝试将成功账号 {account_id} 状态改为 {status},这可能是不正确的操作")
|
||||
# 如果已经是success状态,不允许降级为其他状态
|
||||
# return
|
||||
|
||||
# 更新数据库状态
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET
|
||||
status = ?,
|
||||
status = %s,
|
||||
in_use = 0,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
WHERE id = %s
|
||||
'''
|
||||
await self.db.execute(query, (status, account_id))
|
||||
logger.info(f"账号 {account_id} 状态已更新为 {status}")
|
||||
|
||||
# 如果是success或unavailable状态,添加到黑名单
|
||||
if account_info and (status == 'success' or status == 'unavailable'):
|
||||
email = account_info['email']
|
||||
logger.debug(f"将邮箱 {email} 添加到黑名单 (状态: {status})")
|
||||
await self.add_email_to_blacklist(email)
|
||||
|
||||
# 解锁账号
|
||||
await self.unlock_account(account_id)
|
||||
|
||||
# 清除数据库缓存
|
||||
if self.db.redis:
|
||||
await self.db.clear_cache("db:*email_accounts*")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新账号 {account_id} 状态为 {status} 时出错: {e}")
|
||||
# 确保无论如何都解锁账号
|
||||
try:
|
||||
await self.unlock_account(account_id)
|
||||
except Exception as unlock_error:
|
||||
logger.error(f"尝试解锁账号 {account_id} 时出错: {unlock_error}")
|
||||
raise
|
||||
|
||||
async def update_account(self, account_id: int, cursor_password: str, cursor_cookie: str, cursor_token: str):
|
||||
"""更新账号信息"""
|
||||
"""更新账号信息(注册成功)"""
|
||||
try:
|
||||
# 获取账号邮箱信息(用于黑名单)
|
||||
account_query = "SELECT email FROM email_accounts WHERE id = %s"
|
||||
account_info = await self.db.fetch_one(account_query, (account_id,))
|
||||
|
||||
# 更新数据库
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET
|
||||
cursor_password = ?,
|
||||
cursor_cookie = ?,
|
||||
cursor_token = ?,
|
||||
cursor_password = %s,
|
||||
cursor_cookie = %s,
|
||||
cursor_token = %s,
|
||||
in_use = 0,
|
||||
sold = 1,
|
||||
status = 'success',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
WHERE id = %s
|
||||
'''
|
||||
await self.db.execute(query, (cursor_password, cursor_cookie, cursor_token, account_id))
|
||||
logger.info(f"账号 {account_id} 更新为注册成功")
|
||||
|
||||
# 添加到黑名单
|
||||
if account_info:
|
||||
email = account_info['email']
|
||||
logger.debug(f"将邮箱 {email} 添加到黑名单 (注册成功)")
|
||||
await self.add_email_to_blacklist(email)
|
||||
|
||||
# 解锁账号
|
||||
await self.unlock_account(account_id)
|
||||
|
||||
# 清除数据库缓存
|
||||
if self.db.redis:
|
||||
await self.db.clear_cache("db:*email_accounts*")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新账号 {account_id} 信息时出错: {e}")
|
||||
# 确保无论如何都解锁账号
|
||||
try:
|
||||
await self.unlock_account(account_id)
|
||||
except:
|
||||
pass
|
||||
raise
|
||||
|
||||
async def release_account(self, account_id: int):
|
||||
"""释放账号"""
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET in_use = 0, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
'''
|
||||
await self.db.execute(query, (account_id,))
|
||||
try:
|
||||
await self.unlock_account(account_id)
|
||||
logger.info(f"账号 {account_id} 已释放")
|
||||
|
||||
# 清除数据库缓存
|
||||
if self.db.redis:
|
||||
await self.db.clear_cache("db:*email_accounts*")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"释放账号 {account_id} 时出错: {e}")
|
||||
raise
|
||||
|
||||
async def count_pending_accounts(self) -> int:
|
||||
"""统计可用的pending状态账号数量"""
|
||||
if self.use_redis:
|
||||
await self._ensure_blacklist_initialized()
|
||||
|
||||
query = """
|
||||
SELECT COUNT(*)
|
||||
FROM email_accounts
|
||||
WHERE status = 'pending' AND in_use = 0 AND sold = 0
|
||||
"""
|
||||
|
||||
# 注:这里不使用黑名单过滤,因为数据量可能很大,
|
||||
# 但实际获取账号时会应用黑名单过滤
|
||||
|
||||
result = await self.db.fetch_one(query)
|
||||
if result:
|
||||
return result.get("COUNT(*)", 0)
|
||||
return 0
|
||||
|
||||
async def _get_access_token(self, client_id: str, refresh_token: str) -> str:
|
||||
"""获取微软 access token"""
|
||||
@@ -236,10 +529,10 @@ class EmailManager:
|
||||
logger.debug(f"从文本中提取到验证码: {code}")
|
||||
return code
|
||||
|
||||
logger.warning(f"[{email}] 未能从邮件中提取到验证码")
|
||||
logger.debug(f"[{email}] 邮件内容预览: " + body[:200])
|
||||
logger.warning(f"未能从邮件中提取到验证码")
|
||||
logger.debug(f"邮件内容预览: " + body[:200])
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{email}] 提取验证码失败: {str(e)}")
|
||||
logger.error(f"提取验证码失败: {str(e)}")
|
||||
return None
|
||||
@@ -14,7 +14,7 @@ class ProxyPool:
|
||||
async def batch_get(self, num: int) -> List[str]:
|
||||
"""获取num个代理"""
|
||||
# 临时代理
|
||||
return ['http://127.0.0.1:3057'] * num
|
||||
return ['1ddbeae0f7a67106fd58:f72e512b10893a1d@gw.dataimpulse.com:823'] * num
|
||||
|
||||
try:
|
||||
response = await self.fetch_manager.request(
|
||||
|
||||
204
services/self_hosted_email.py
Normal file
204
services/self_hosted_email.py
Normal file
@@ -0,0 +1,204 @@
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import asyncio
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from core.exceptions import EmailError
|
||||
from services.fetch_manager import FetchManager
|
||||
|
||||
|
||||
class SelfHostedEmail:
|
||||
"""自建邮箱服务类,负责从API获取邮箱和验证码"""
|
||||
|
||||
def __init__(self, fetch_manager: FetchManager, api_base_url: str, api_key: Optional[str] = None):
|
||||
"""初始化自建邮箱服务
|
||||
|
||||
Args:
|
||||
fetch_manager: HTTP请求管理器
|
||||
api_base_url: API基础URL,例如 "https://cursorapi3.nosqli.com/"
|
||||
api_key: API密钥(可选)
|
||||
"""
|
||||
self.fetch_manager = fetch_manager
|
||||
self.api_base_url = "https://cursorapi3.nosqli.com/"
|
||||
self.api_key = "1234567890"
|
||||
|
||||
# API端点
|
||||
self.get_email_endpoint = "/admin/api.email/getEmail"
|
||||
self.get_code_endpoint = "/admin/api.email/getVerificationCode"
|
||||
|
||||
# 新的邮件获取接口
|
||||
self.new_email_api = "https://rnemail.nosqli.com/latest_email"
|
||||
|
||||
async def _make_api_request(self, endpoint: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""向API发送请求
|
||||
|
||||
Args:
|
||||
endpoint: API端点,例如 "/admin/api.email/getEmail"
|
||||
params: 请求参数
|
||||
|
||||
Returns:
|
||||
解析后的JSON响应
|
||||
|
||||
Raises:
|
||||
EmailError: 当API请求失败或响应无法解析时
|
||||
"""
|
||||
url = f"{self.api_base_url}{endpoint}"
|
||||
headers = {}
|
||||
|
||||
if self.api_key:
|
||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
|
||||
logger.debug(f"正在请求: {url}")
|
||||
response = await self.fetch_manager.request(
|
||||
"GET",
|
||||
url,
|
||||
headers=headers,
|
||||
params=params
|
||||
)
|
||||
|
||||
if 'error' in response:
|
||||
raise EmailError(f"API请求失败: {response['error']}")
|
||||
|
||||
try:
|
||||
result = json.loads(response['body'].decode())
|
||||
return result
|
||||
except Exception as e:
|
||||
raise EmailError(f"解析API响应失败: {str(e)}")
|
||||
|
||||
async def get_email(self) -> Optional[str]:
|
||||
"""从API获取一个可用邮箱
|
||||
|
||||
Returns:
|
||||
邮箱地址,失败时返回None
|
||||
"""
|
||||
try:
|
||||
result = await self._make_api_request(self.get_email_endpoint)
|
||||
|
||||
if result.get('code') != 0:
|
||||
logger.error(f"获取邮箱失败: {result.get('msg')}")
|
||||
return None
|
||||
|
||||
email = result.get('data', {}).get('email')
|
||||
if not email:
|
||||
logger.error("API未返回有效邮箱")
|
||||
return None
|
||||
|
||||
logger.info(f"获取到自建邮箱: {email}")
|
||||
return email
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取邮箱失败: {str(e)}")
|
||||
return None
|
||||
|
||||
async def get_verification_code(self, email: str) -> Optional[str]:
|
||||
"""从API获取验证码
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
验证码,失败时返回None
|
||||
"""
|
||||
# 首先尝试使用新接口获取
|
||||
code = await self._get_code_from_email_api(email)
|
||||
if code:
|
||||
return code
|
||||
|
||||
# 如果新接口失败,尝试旧接口
|
||||
logger.debug("新接口获取验证码失败,尝试使用旧接口")
|
||||
try:
|
||||
result = await self._make_api_request(
|
||||
self.get_code_endpoint,
|
||||
params={"email": email}
|
||||
)
|
||||
|
||||
if result.get('code') != 0:
|
||||
logger.error(f"获取验证码失败: {result.get('msg')}")
|
||||
return None
|
||||
|
||||
code = result.get('data', {}).get('code')
|
||||
if not code:
|
||||
logger.error("API未返回有效验证码")
|
||||
return None
|
||||
|
||||
logger.info(f"获取到验证码: {code} (邮箱: {email})")
|
||||
return code
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取验证码失败: {str(e)}")
|
||||
return None
|
||||
|
||||
async def _get_code_from_email_api(self, email: str, max_retries: int = 5, retry_delay: int = 3) -> Optional[str]:
|
||||
"""从新的邮件API获取验证码
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
max_retries: 最大重试次数
|
||||
retry_delay: 重试间隔(秒)
|
||||
|
||||
Returns:
|
||||
验证码,失败时返回None
|
||||
"""
|
||||
url = f"{self.new_email_api}?recipient={email}"
|
||||
logger.debug(f"使用新接口获取验证码: {url}")
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = await self.fetch_manager.request("GET", url)
|
||||
|
||||
if 'error' in response:
|
||||
logger.warning(f"获取邮件失败 (尝试 {attempt + 1}/{max_retries}): {response['error']}")
|
||||
else:
|
||||
email_data = json.loads(response['body'].decode())
|
||||
logger.debug(f"获取到邮件数据: {str(email_data)[:200]}...")
|
||||
|
||||
# 解析邮件内容,提取验证码
|
||||
if email_data and isinstance(email_data, dict):
|
||||
body = email_data.get('body', '')
|
||||
if 'code' in email_data:
|
||||
# 如果API直接返回code字段
|
||||
code = email_data.get('code')
|
||||
if code:
|
||||
logger.info(f"从邮件API直接获取到验证码: {code}")
|
||||
return code
|
||||
|
||||
# 从邮件主体中提取验证码
|
||||
if body:
|
||||
# 正则表达式匹配6位数字验证码
|
||||
code_match = re.search(r'code["\s:]*["\s]*(\d{6})["\s]*', body, re.IGNORECASE)
|
||||
if code_match:
|
||||
code = code_match.group(1)
|
||||
logger.info(f"从邮件内容中提取到验证码: {code}")
|
||||
return code
|
||||
|
||||
# 匹配"验证码"后的6位数字
|
||||
code_match = re.search(r'[\u4e00-\u9fa5]*验证码[\u4e00-\u9fa5]*[::]*\s*(\d{6})\b', body)
|
||||
if code_match:
|
||||
code = code_match.group(1)
|
||||
logger.info(f"从邮件内容中提取到验证码: {code}")
|
||||
return code
|
||||
|
||||
# 如果是Cursor邮件,尝试直接提取验证码格式
|
||||
if "Cursor" in body and "verify" in body:
|
||||
code_match = re.search(r'\b(\d{6})\b', body)
|
||||
if code_match:
|
||||
code = code_match.group(1)
|
||||
logger.info(f"从Cursor邮件中提取到验证码: {code}")
|
||||
return code
|
||||
|
||||
logger.warning(f"未能从邮件中提取到验证码 (尝试 {attempt + 1}/{max_retries})")
|
||||
|
||||
# 如果没有找到验证码,等待一段时间后重试
|
||||
if attempt < max_retries - 1:
|
||||
logger.debug(f"等待 {retry_delay} 秒后重试获取验证码...")
|
||||
await asyncio.sleep(retry_delay)
|
||||
except Exception as e:
|
||||
logger.error(f"获取验证码出错 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(retry_delay)
|
||||
|
||||
logger.error(f"在 {max_retries} 次尝试后未能获取到验证码")
|
||||
return None
|
||||
607
setup_environment.py
Normal file
607
setup_environment.py
Normal file
@@ -0,0 +1,607 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cursor自动化服务 - 交互式环境配置脚本
|
||||
用于配置和初始化新服务器环境、数据库和Redis
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import getpass
|
||||
import time
|
||||
import yaml
|
||||
import socket
|
||||
import random
|
||||
import string
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
|
||||
# 设置颜色输出
|
||||
class Colors:
|
||||
HEADER = '\033[95m'
|
||||
BLUE = '\033[94m'
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
RED = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
UNDERLINE = '\033[4m'
|
||||
|
||||
def print_color(text, color):
|
||||
"""输出彩色文本"""
|
||||
print(f"{color}{text}{Colors.ENDC}")
|
||||
|
||||
def print_title(title):
|
||||
"""打印标题"""
|
||||
width = 60
|
||||
print("\n" + "=" * width)
|
||||
print(f"{Colors.BOLD}{Colors.HEADER}{title.center(width)}{Colors.ENDC}")
|
||||
print("=" * width + "\n")
|
||||
|
||||
def print_step(step, description):
|
||||
"""打印步骤信息"""
|
||||
print(f"{Colors.BOLD}{Colors.BLUE}[步骤 {step}]{Colors.ENDC} {description}")
|
||||
|
||||
def print_success(message):
|
||||
"""打印成功信息"""
|
||||
print(f"{Colors.GREEN}✓ {message}{Colors.ENDC}")
|
||||
|
||||
def print_warning(message):
|
||||
"""打印警告信息"""
|
||||
print(f"{Colors.YELLOW}⚠ {message}{Colors.ENDC}")
|
||||
|
||||
def print_error(message):
|
||||
"""打印错误信息"""
|
||||
print(f"{Colors.RED}✗ {message}{Colors.ENDC}")
|
||||
|
||||
# 检测虚拟环境
|
||||
def is_in_virtualenv():
|
||||
"""检查是否在虚拟环境中运行"""
|
||||
return hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
|
||||
|
||||
def setup_virtualenv():
|
||||
"""设置虚拟环境"""
|
||||
print_title("虚拟环境检测")
|
||||
|
||||
if is_in_virtualenv():
|
||||
print_success("已在虚拟环境中运行")
|
||||
return True
|
||||
|
||||
print_warning("当前不在虚拟环境中运行")
|
||||
create_venv = input("是否创建并使用虚拟环境? (推荐) (y/n, 默认: y): ").strip().lower()
|
||||
|
||||
if create_venv == 'n':
|
||||
print_warning("跳过虚拟环境创建,将直接在系统Python环境中安装依赖")
|
||||
return True
|
||||
|
||||
venv_path = input("请输入虚拟环境路径 (默认: ./venv): ").strip() or "./venv"
|
||||
|
||||
try:
|
||||
# 检查venv模块
|
||||
try:
|
||||
import venv
|
||||
has_venv = True
|
||||
except ImportError:
|
||||
has_venv = False
|
||||
|
||||
if not has_venv:
|
||||
print_warning("Python venv模块不可用,尝试安装...")
|
||||
if sys.platform.startswith('linux'):
|
||||
print("在Linux上安装venv模块...")
|
||||
try:
|
||||
subprocess.check_call(["sudo", "apt", "install", "-y", "python3-venv", "python3-full"])
|
||||
except:
|
||||
try:
|
||||
subprocess.check_call(["sudo", "yum", "install", "-y", "python3-venv"])
|
||||
except:
|
||||
print_error("无法自动安装venv模块,请手动安装后重试")
|
||||
print_warning("Ubuntu/Debian: sudo apt install python3-venv python3-full")
|
||||
print_warning("CentOS/RHEL: sudo yum install python3-venv")
|
||||
return False
|
||||
else:
|
||||
print_error("请安装Python venv模块后重试")
|
||||
return False
|
||||
|
||||
# 创建虚拟环境
|
||||
print_step("创建", f"正在创建虚拟环境: {venv_path}")
|
||||
if sys.platform.startswith('win'):
|
||||
subprocess.check_call([sys.executable, "-m", "venv", venv_path])
|
||||
else:
|
||||
subprocess.check_call([sys.executable, "-m", "venv", venv_path])
|
||||
|
||||
# 计算激活脚本路径
|
||||
if sys.platform.startswith('win'):
|
||||
activate_script = os.path.join(venv_path, "Scripts", "activate.bat")
|
||||
python_path = os.path.join(venv_path, "Scripts", "python.exe")
|
||||
else:
|
||||
activate_script = os.path.join(venv_path, "bin", "activate")
|
||||
python_path = os.path.join(venv_path, "bin", "python")
|
||||
|
||||
print_success(f"虚拟环境创建成功: {venv_path}")
|
||||
print_warning("请退出当前程序,然后执行以下命令激活虚拟环境并重新运行:")
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
print(f"\n{Colors.BOLD}Windows:{Colors.ENDC}")
|
||||
print(f" {activate_script}")
|
||||
print(f" python setup_environment.py")
|
||||
else:
|
||||
print(f"\n{Colors.BOLD}Linux/Mac:{Colors.ENDC}")
|
||||
print(f" source {activate_script}")
|
||||
print(f" python setup_environment.py")
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"创建虚拟环境失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 尝试导入必要的库,如果不存在则安装
|
||||
required_packages = [
|
||||
"loguru",
|
||||
"pymysql",
|
||||
"aiomysql",
|
||||
"redis",
|
||||
"pyyaml",
|
||||
"cryptography" # 添加这一行
|
||||
]
|
||||
|
||||
def check_and_install_packages():
|
||||
"""检查并安装必要的Python包"""
|
||||
print_step(1, "检查并安装必要的Python包")
|
||||
|
||||
for package in required_packages:
|
||||
try:
|
||||
__import__(package.split(">=")[0])
|
||||
print_success(f"已安装: {package}")
|
||||
except ImportError:
|
||||
print_warning(f"未找到 {package},正在安装...")
|
||||
try:
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
|
||||
print_success(f"成功安装 {package}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print_error(f"安装 {package} 失败: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
# 安装后重新导入必要的库
|
||||
try:
|
||||
global yaml
|
||||
import yaml
|
||||
from loguru import logger
|
||||
print_success("所有必要的包已安装")
|
||||
except ImportError as e:
|
||||
print_error(f"无法导入必要的库: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
def generate_password(length=16):
|
||||
"""生成随机密码"""
|
||||
chars = string.ascii_letters + string.digits + "!@#$%^&*()_+-="
|
||||
return ''.join(random.choice(chars) for _ in range(length))
|
||||
|
||||
def get_default_hostname():
|
||||
"""获取默认主机名"""
|
||||
try:
|
||||
return socket.gethostname()
|
||||
except:
|
||||
return "cursor-server"
|
||||
|
||||
def read_config():
|
||||
"""读取现有配置文件"""
|
||||
config_path = "config.yaml"
|
||||
if os.path.exists(config_path):
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
print_warning(f"读取配置文件失败: {str(e)}")
|
||||
return {}
|
||||
|
||||
def configure_server_settings(existing_config):
|
||||
"""配置服务器设置"""
|
||||
print_step(2, "配置服务器标识")
|
||||
|
||||
existing_hostname = None
|
||||
if 'server_config' in existing_config and 'hostname' in existing_config['server_config']:
|
||||
existing_hostname = existing_config['server_config']['hostname']
|
||||
|
||||
default_hostname = existing_hostname or get_default_hostname()
|
||||
hostname = input(f"请输入服务器唯一标识 (默认: {default_hostname}): ").strip()
|
||||
if not hostname:
|
||||
hostname = default_hostname
|
||||
|
||||
if not 'server_config' in existing_config:
|
||||
existing_config['server_config'] = {}
|
||||
|
||||
existing_config['server_config']['hostname'] = hostname
|
||||
print_success(f"服务器标识已设置为: {hostname}")
|
||||
return existing_config
|
||||
|
||||
def test_mysql_connection(host, port, user, password, database=None):
|
||||
"""测试MySQL连接"""
|
||||
import pymysql
|
||||
try:
|
||||
conn_args = {
|
||||
'host': host,
|
||||
'port': port,
|
||||
'user': user,
|
||||
'password': password,
|
||||
'connect_timeout': 5
|
||||
}
|
||||
if database:
|
||||
conn_args['database'] = database
|
||||
|
||||
conn = pymysql.connect(**conn_args)
|
||||
conn.close()
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def configure_mysql_settings(existing_config):
|
||||
"""配置MySQL设置"""
|
||||
print_step(3, "配置MySQL数据库")
|
||||
|
||||
db_config = existing_config.get('database', {})
|
||||
|
||||
print("请输入MySQL配置信息:")
|
||||
host = input(f"主机地址 (默认: {db_config.get('host', 'localhost')}): ").strip() or db_config.get('host', 'localhost')
|
||||
port = input(f"端口 (默认: {db_config.get('port', 3306)}): ").strip() or db_config.get('port', 3306)
|
||||
try:
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
print_warning("端口必须是数字,使用默认值3306")
|
||||
port = 3306
|
||||
|
||||
# 获取root信息以创建数据库和用户
|
||||
print("\n需要MySQL root权限来创建数据库和用户:")
|
||||
root_user = input("MySQL root用户名 (默认: root): ").strip() or "root"
|
||||
root_password = getpass.getpass("MySQL root密码: ")
|
||||
|
||||
# 测试root连接
|
||||
success, error_msg = test_mysql_connection(host, port, root_user, root_password)
|
||||
if not success:
|
||||
print_error(f"无法连接到MySQL: {error_msg}")
|
||||
print_warning("请确认MySQL服务已启动且用户名密码正确")
|
||||
retry = input("是否重试MySQL配置? (y/n): ").lower()
|
||||
if retry == 'y':
|
||||
return configure_mysql_settings(existing_config)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
print_success("MySQL连接测试成功")
|
||||
|
||||
# 应用数据库和用户设置
|
||||
db_name = input(f"数据库名称 (默认: {db_config.get('database', 'auto_cursor_reg')}): ").strip() or db_config.get('database', 'auto_cursor_reg')
|
||||
db_user = input(f"应用用户名 (默认: {db_config.get('username', 'auto_cursor_reg')}): ").strip() or db_config.get('username', 'auto_cursor_reg')
|
||||
|
||||
# 为应用用户生成密码
|
||||
default_password = db_config.get('password')
|
||||
if default_password:
|
||||
use_existing = input(f"使用现有密码? (y/n, 默认: y): ").lower() != 'n'
|
||||
if use_existing:
|
||||
db_password = default_password
|
||||
else:
|
||||
suggested_password = generate_password(12)
|
||||
db_password = input(f"应用用户密码 (建议: {suggested_password}): ").strip() or suggested_password
|
||||
else:
|
||||
suggested_password = generate_password(12)
|
||||
db_password = input(f"应用用户密码 (建议: {suggested_password}): ").strip() or suggested_password
|
||||
|
||||
# 更新配置
|
||||
existing_config['database'] = {
|
||||
'host': host,
|
||||
'port': port,
|
||||
'username': db_user,
|
||||
'password': db_password,
|
||||
'database': db_name,
|
||||
'pool_size': db_config.get('pool_size', 10),
|
||||
'use_redis': db_config.get('use_redis', True)
|
||||
}
|
||||
|
||||
# 保存root信息用于创建数据库和用户
|
||||
root_info = {
|
||||
'host': host,
|
||||
'port': port,
|
||||
'user': root_user,
|
||||
'password': root_password
|
||||
}
|
||||
|
||||
return existing_config, root_info
|
||||
|
||||
def test_redis_connection(host, port, password=None, db=0):
|
||||
"""测试Redis连接"""
|
||||
try:
|
||||
import redis
|
||||
client = redis.Redis(
|
||||
host=host,
|
||||
port=port,
|
||||
password=password if password else None,
|
||||
db=db,
|
||||
socket_timeout=5
|
||||
)
|
||||
client.ping()
|
||||
client.close()
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def configure_redis_settings(existing_config):
|
||||
"""配置Redis设置"""
|
||||
print_step(4, "配置Redis缓存")
|
||||
|
||||
db_config = existing_config.get('database', {})
|
||||
redis_config = existing_config.get('redis', {})
|
||||
|
||||
use_redis = input(f"是否使用Redis缓存? (y/n, 默认: {'y' if db_config.get('use_redis', True) else 'n'}): ").lower()
|
||||
if use_redis == 'n':
|
||||
existing_config['database']['use_redis'] = False
|
||||
print_warning("Redis缓存已禁用")
|
||||
return existing_config, None
|
||||
|
||||
existing_config['database']['use_redis'] = True
|
||||
|
||||
print("请输入Redis配置信息:")
|
||||
host = input(f"主机地址 (默认: {redis_config.get('host', '127.0.0.1')}): ").strip() or redis_config.get('host', '127.0.0.1')
|
||||
port = input(f"端口 (默认: {redis_config.get('port', 6379)}): ").strip() or redis_config.get('port', 6379)
|
||||
try:
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
print_warning("端口必须是数字,使用默认值6379")
|
||||
port = 6379
|
||||
|
||||
password = input(f"密码 (默认: {'保持现有密码' if redis_config.get('password') else '无'}): ")
|
||||
if not password and 'password' in redis_config:
|
||||
password = redis_config['password']
|
||||
|
||||
db = input(f"数据库索引 (默认: {redis_config.get('db', 0)}): ").strip() or redis_config.get('db', 0)
|
||||
try:
|
||||
db = int(db)
|
||||
except ValueError:
|
||||
print_warning("数据库索引必须是数字,使用默认值0")
|
||||
db = 0
|
||||
|
||||
# 测试Redis连接
|
||||
success, error_msg = test_redis_connection(host, port, password, db)
|
||||
if not success:
|
||||
print_error(f"无法连接到Redis: {error_msg}")
|
||||
print_warning("请确认Redis服务已启动且配置正确")
|
||||
retry = input("是否重试Redis配置? (y/n): ").lower()
|
||||
if retry == 'y':
|
||||
return configure_redis_settings(existing_config)
|
||||
elif retry == 'n':
|
||||
use_anyway = input("是否仍然使用这些设置? (y/n): ").lower()
|
||||
if use_anyway != 'y':
|
||||
existing_config['database']['use_redis'] = False
|
||||
print_warning("Redis缓存已禁用")
|
||||
return existing_config, None
|
||||
else:
|
||||
print_success("Redis连接测试成功")
|
||||
|
||||
# 更新配置
|
||||
existing_config['redis'] = {
|
||||
'host': host,
|
||||
'port': port,
|
||||
'password': password,
|
||||
'db': db
|
||||
}
|
||||
|
||||
redis_info = {
|
||||
'host': host,
|
||||
'port': port,
|
||||
'password': password,
|
||||
'db': db
|
||||
}
|
||||
|
||||
return existing_config, redis_info
|
||||
|
||||
def setup_mysql_database(config, root_info):
|
||||
"""设置MySQL数据库和用户"""
|
||||
print_step(5, "创建MySQL数据库和用户")
|
||||
|
||||
db_config = config['database']
|
||||
db_name = db_config['database']
|
||||
db_user = db_config['username']
|
||||
db_password = db_config['password']
|
||||
|
||||
import pymysql
|
||||
try:
|
||||
# 连接MySQL
|
||||
conn = pymysql.connect(
|
||||
host=root_info['host'],
|
||||
port=root_info['port'],
|
||||
user=root_info['user'],
|
||||
password=root_info['password']
|
||||
)
|
||||
|
||||
with conn.cursor() as cursor:
|
||||
# 创建数据库
|
||||
print(f"创建数据库 {db_name}...")
|
||||
cursor.execute(f"CREATE DATABASE IF NOT EXISTS `{db_name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
|
||||
|
||||
# 创建用户并授权
|
||||
print(f"创建用户 {db_user}...")
|
||||
|
||||
# 检查用户是否存在
|
||||
cursor.execute(f"SELECT 1 FROM mysql.user WHERE user = %s AND host = %s", (db_user, '%'))
|
||||
user_exists = cursor.fetchone()
|
||||
|
||||
if user_exists:
|
||||
print_warning(f"用户 {db_user} 已存在,更新密码...")
|
||||
cursor.execute(f"ALTER USER '{db_user}'@'%' IDENTIFIED BY %s", (db_password,))
|
||||
else:
|
||||
cursor.execute(f"CREATE USER '{db_user}'@'%' IDENTIFIED BY %s", (db_password,))
|
||||
|
||||
# 授权
|
||||
cursor.execute(f"GRANT ALL PRIVILEGES ON `{db_name}`.* TO '{db_user}'@'%'")
|
||||
cursor.execute("FLUSH PRIVILEGES")
|
||||
|
||||
conn.close()
|
||||
print_success(f"数据库 {db_name} 和用户 {db_user} 创建成功")
|
||||
return True
|
||||
except Exception as e:
|
||||
print_error(f"设置MySQL数据库失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def create_database_tables(config):
|
||||
"""创建数据库表结构"""
|
||||
print_step(6, "创建数据库表结构")
|
||||
|
||||
db_config = config['database']
|
||||
|
||||
import pymysql
|
||||
try:
|
||||
# 连接MySQL
|
||||
conn = pymysql.connect(
|
||||
host=db_config['host'],
|
||||
port=db_config['port'],
|
||||
user=db_config['username'],
|
||||
password=db_config['password'],
|
||||
database=db_config['database']
|
||||
)
|
||||
|
||||
with conn.cursor() as cursor:
|
||||
# 创建email_accounts表
|
||||
print("创建email_accounts表...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS email_accounts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT NOT NULL,
|
||||
in_use BOOLEAN DEFAULT 0,
|
||||
cursor_password VARCHAR(255),
|
||||
cursor_cookie TEXT,
|
||||
cursor_token TEXT,
|
||||
sold BOOLEAN DEFAULT 0,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
extracted BOOLEAN DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_status_inuse_sold (status, in_use, sold),
|
||||
INDEX idx_extracted (extracted, status, sold)
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建system_settings表
|
||||
print("创建system_settings表...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS system_settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
key_name VARCHAR(50) UNIQUE NOT NULL,
|
||||
value TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# 初始化system_settings
|
||||
cursor.execute(
|
||||
"INSERT INTO system_settings (key_name, value) VALUES ('auto_service_enabled', '0') "
|
||||
"ON DUPLICATE KEY UPDATE value = value"
|
||||
)
|
||||
|
||||
conn.close()
|
||||
print_success("数据库表结构创建成功")
|
||||
return True
|
||||
except Exception as e:
|
||||
print_error(f"创建数据库表失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def update_config_file(config):
|
||||
"""更新配置文件"""
|
||||
print_step(7, "更新配置文件")
|
||||
|
||||
config_path = "config.yaml"
|
||||
backup_path = f"config.yaml.bak.{int(time.time())}"
|
||||
|
||||
# 备份现有配置
|
||||
if os.path.exists(config_path):
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as src:
|
||||
with open(backup_path, 'w', encoding='utf-8') as dst:
|
||||
dst.write(src.read())
|
||||
print_success(f"现有配置已备份为 {backup_path}")
|
||||
except Exception as e:
|
||||
print_warning(f"备份配置文件失败: {str(e)}")
|
||||
|
||||
# 写入新配置
|
||||
try:
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
||||
print_success(f"配置已更新至 {config_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print_error(f"更新配置文件失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def finalize_setup():
|
||||
"""完成设置并提供指导"""
|
||||
print_title("设置完成")
|
||||
|
||||
print(f"{Colors.GREEN}Cursor自动化服务环境配置已完成!{Colors.ENDC}")
|
||||
print("\n接下来您可以:")
|
||||
|
||||
print(f"{Colors.BOLD}1. 导入邮箱账号:{Colors.ENDC}")
|
||||
print(" python import_emails.py")
|
||||
|
||||
print(f"\n{Colors.BOLD}2. 启动服务:{Colors.ENDC}")
|
||||
print(" python start.py")
|
||||
print(" 或")
|
||||
print(" python auto_cursor_service.py")
|
||||
|
||||
print(f"\n{Colors.BOLD}3. 设置为系统服务:{Colors.ENDC}")
|
||||
print(" 请参考README文件中的系统服务设置说明")
|
||||
|
||||
print(f"\n{Colors.BOLD}如需帮助:{Colors.ENDC}")
|
||||
print(" 查看各README文件获取详细使用说明")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print_title("Cursor自动化服务环境配置")
|
||||
|
||||
# 检查Python版本
|
||||
if sys.version_info < (3, 7):
|
||||
print_error("需要Python 3.7或更高版本")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查并安装依赖
|
||||
check_and_install_packages()
|
||||
|
||||
# 读取现有配置
|
||||
existing_config = read_config()
|
||||
|
||||
# 配置服务器设置
|
||||
config = configure_server_settings(existing_config)
|
||||
|
||||
# 配置MySQL
|
||||
config, root_info = configure_mysql_settings(config)
|
||||
|
||||
# 配置Redis
|
||||
config, redis_info = configure_redis_settings(config)
|
||||
|
||||
# 设置MySQL数据库和用户
|
||||
if not setup_mysql_database(config, root_info):
|
||||
retry = input("MySQL设置失败,是否继续? (y/n): ").lower()
|
||||
if retry != 'y':
|
||||
sys.exit(1)
|
||||
|
||||
# 创建数据库表
|
||||
if not create_database_tables(config):
|
||||
retry = input("创建表失败,是否继续? (y/n): ").lower()
|
||||
if retry != 'y':
|
||||
sys.exit(1)
|
||||
|
||||
# 更新配置文件
|
||||
if not update_config_file(config):
|
||||
retry = input("更新配置文件失败,是否继续? (y/n): ").lower()
|
||||
if retry != 'y':
|
||||
sys.exit(1)
|
||||
|
||||
# 完成设置
|
||||
finalize_setup()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n设置已取消")
|
||||
sys.exit(1)
|
||||
225
setup_patch.py
Normal file
225
setup_patch.py
Normal file
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cursor自动化服务 - 设置脚本补丁
|
||||
修复MySQL连接和添加虚拟环境支持
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
# 彩色输出
|
||||
class Colors:
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
RED = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
|
||||
def print_title(title):
|
||||
"""打印标题"""
|
||||
print(f"\n{Colors.BOLD}{Colors.GREEN}{'='*60}{Colors.ENDC}")
|
||||
print(f"{Colors.BOLD}{Colors.GREEN}{title.center(60)}{Colors.ENDC}")
|
||||
print(f"{Colors.BOLD}{Colors.GREEN}{'='*60}{Colors.ENDC}\n")
|
||||
|
||||
def print_success(message):
|
||||
"""打印成功信息"""
|
||||
print(f"{Colors.GREEN}✓ {message}{Colors.ENDC}")
|
||||
|
||||
def print_warning(message):
|
||||
"""打印警告信息"""
|
||||
print(f"{Colors.YELLOW}⚠ {message}{Colors.ENDC}")
|
||||
|
||||
def print_error(message):
|
||||
"""打印错误信息"""
|
||||
print(f"{Colors.RED}✗ {message}{Colors.ENDC}")
|
||||
|
||||
def is_in_virtualenv():
|
||||
"""检查是否在虚拟环境中运行"""
|
||||
return hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
|
||||
|
||||
def setup_virtualenv():
|
||||
"""设置虚拟环境"""
|
||||
print_title("虚拟环境设置")
|
||||
|
||||
if is_in_virtualenv():
|
||||
print_success("已在虚拟环境中运行")
|
||||
return True
|
||||
|
||||
print_warning("当前不在虚拟环境中运行")
|
||||
create_venv = input("是否创建并使用虚拟环境? (推荐) (y/n, 默认: y): ").strip().lower()
|
||||
|
||||
if create_venv == 'n':
|
||||
print_warning("跳过虚拟环境创建,将直接在系统Python环境中安装依赖")
|
||||
return True
|
||||
|
||||
venv_path = input("请输入虚拟环境路径 (默认: ./venv): ").strip() or "./venv"
|
||||
|
||||
try:
|
||||
# 检查venv模块
|
||||
try:
|
||||
subprocess.check_call([sys.executable, "-m", "venv", "--help"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
has_venv = True
|
||||
except (subprocess.SubprocessError, ImportError):
|
||||
has_venv = False
|
||||
|
||||
if not has_venv:
|
||||
print_warning("Python venv模块不可用,尝试安装...")
|
||||
if sys.platform.startswith('linux'):
|
||||
print("在Linux上安装venv模块...")
|
||||
try:
|
||||
subprocess.check_call(["sudo", "apt", "install", "-y", "python3-venv", "python3-full"], stdout=subprocess.DEVNULL)
|
||||
print_success("venv模块安装成功")
|
||||
except:
|
||||
try:
|
||||
subprocess.check_call(["sudo", "yum", "install", "-y", "python3-venv"], stdout=subprocess.DEVNULL)
|
||||
print_success("venv模块安装成功")
|
||||
except:
|
||||
print_error("无法自动安装venv模块,请手动安装后重试")
|
||||
print_warning("Ubuntu/Debian: sudo apt install python3-venv python3-full")
|
||||
print_warning("CentOS/RHEL: sudo yum install python3-venv")
|
||||
return False
|
||||
else:
|
||||
print_error("请安装Python venv模块后重试")
|
||||
return False
|
||||
|
||||
# 创建虚拟环境
|
||||
print("\n正在创建虚拟环境...")
|
||||
subprocess.check_call([sys.executable, "-m", "venv", venv_path])
|
||||
|
||||
# 计算激活脚本路径
|
||||
if sys.platform.startswith('win'):
|
||||
activate_script = os.path.join(venv_path, "Scripts", "activate.bat")
|
||||
pip_path = os.path.join(venv_path, "Scripts", "pip.exe")
|
||||
python_path = os.path.join(venv_path, "Scripts", "python.exe")
|
||||
else:
|
||||
activate_script = os.path.join(venv_path, "bin", "activate")
|
||||
pip_path = os.path.join(venv_path, "bin", "pip")
|
||||
python_path = os.path.join(venv_path, "bin", "python")
|
||||
|
||||
print_success(f"虚拟环境创建成功: {venv_path}")
|
||||
|
||||
# 安装依赖
|
||||
print("\n正在安装依赖包...")
|
||||
if os.path.exists("requirements.txt"):
|
||||
try:
|
||||
if sys.platform.startswith('win'):
|
||||
subprocess.check_call([pip_path, "install", "-r", "requirements.txt"])
|
||||
else:
|
||||
script = f"""#!/bin/bash
|
||||
source "{activate_script}"
|
||||
pip install -r requirements.txt
|
||||
pip install cryptography
|
||||
echo "依赖安装完成,请运行以下命令启动设置向导:"
|
||||
echo "source {activate_script}"
|
||||
echo "python setup_environment.py"
|
||||
"""
|
||||
with open("setup_venv.sh", "w") as f:
|
||||
f.write(script)
|
||||
os.chmod("setup_venv.sh", 0o755)
|
||||
print_success("已创建安装脚本: setup_venv.sh")
|
||||
print("\n请运行以下命令完成安装并启动设置向导:")
|
||||
print(f" bash ./setup_venv.sh")
|
||||
except Exception as e:
|
||||
print_error(f"安装依赖失败: {str(e)}")
|
||||
print("\n请手动运行以下命令:")
|
||||
print(f" source {activate_script}")
|
||||
print(f" pip install -r requirements.txt")
|
||||
print(f" pip install cryptography")
|
||||
print(f" python setup_environment.py")
|
||||
else:
|
||||
print_warning("未找到requirements.txt文件")
|
||||
print("\n请手动运行以下命令:")
|
||||
print(f" source {activate_script}")
|
||||
print(f" pip install loguru pymysql aiomysql redis pyyaml cryptography")
|
||||
print(f" python setup_environment.py")
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"创建虚拟环境失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def patch_setup_script():
|
||||
"""修补setup_environment.py脚本"""
|
||||
print_title("修补设置脚本")
|
||||
|
||||
if not os.path.exists("setup_environment.py"):
|
||||
print_error("未找到setup_environment.py文件")
|
||||
return False
|
||||
|
||||
# 备份原始文件
|
||||
backup_file = f"setup_environment.py.bak.{int(os.path.getmtime('setup_environment.py'))}"
|
||||
try:
|
||||
shutil.copy2("setup_environment.py", backup_file)
|
||||
print_success(f"已备份原始文件: {backup_file}")
|
||||
except Exception as e:
|
||||
print_error(f"备份文件失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 修改required_packages列表
|
||||
try:
|
||||
with open("setup_environment.py", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 添加cryptography包
|
||||
if "cryptography" not in content:
|
||||
content = content.replace(
|
||||
'required_packages = [\n "loguru",\n "pymysql",\n "aiomysql",\n "redis",\n "pyyaml"',
|
||||
'required_packages = [\n "loguru",\n "pymysql",\n "aiomysql",\n "redis",\n "pyyaml",\n "cryptography"'
|
||||
)
|
||||
print_success("已添加cryptography包到依赖列表")
|
||||
|
||||
# 修复MySQL字符串格式化问题
|
||||
if "CREATE USER '{db_user}'@'%'" in content:
|
||||
content = content.replace(
|
||||
"cursor.execute(f\"CREATE USER '{db_user}'@'%' IDENTIFIED BY %s\", (db_password,))",
|
||||
"cursor.execute(\"CREATE USER %s@'%%' IDENTIFIED BY %s\", (db_user, db_password))"
|
||||
)
|
||||
print_success("已修复MySQL用户创建语句的格式化问题")
|
||||
|
||||
if "ALTER USER '{db_user}'@'%'" in content:
|
||||
content = content.replace(
|
||||
"cursor.execute(f\"ALTER USER '{db_user}'@'%' IDENTIFIED BY %s\", (db_password,))",
|
||||
"cursor.execute(\"ALTER USER %s@'%%' IDENTIFIED BY %s\", (db_user, db_password))"
|
||||
)
|
||||
print_success("已修复MySQL用户更新语句的格式化问题")
|
||||
|
||||
# 写回文件
|
||||
with open("setup_environment.py", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print_success("设置脚本修补完成")
|
||||
return True
|
||||
except Exception as e:
|
||||
print_error(f"修补文件失败: {str(e)}")
|
||||
try:
|
||||
# 恢复备份
|
||||
shutil.copy2(backup_file, "setup_environment.py")
|
||||
print_warning("已恢复原始文件")
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print_title("Cursor自动化服务 - 设置补丁工具")
|
||||
|
||||
# 设置虚拟环境
|
||||
if not setup_virtualenv():
|
||||
return
|
||||
|
||||
# 修补设置脚本
|
||||
if not patch_setup_script():
|
||||
return
|
||||
|
||||
print_title("补丁应用完成")
|
||||
print("现在您可以安全地运行设置向导:")
|
||||
print(" python setup_environment.py")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n操作已取消")
|
||||
sys.exit(1)
|
||||
311
start.py
Normal file
311
start.py
Normal file
@@ -0,0 +1,311 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cursor自动化服务统一启动脚本
|
||||
- 检查数据库标记
|
||||
- 启动自动化服务或独立功能
|
||||
- 集中管理各项服务功能
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Optional, List
|
||||
|
||||
from loguru import logger
|
||||
|
||||
# Windows平台特殊处理,强制使用SelectorEventLoop
|
||||
if sys.platform.startswith("win"):
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
from core.config import Config
|
||||
from core.database import DatabaseManager
|
||||
|
||||
|
||||
# 配置日志
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="INFO")
|
||||
logger.add(
|
||||
"start.log",
|
||||
rotation="10 MB",
|
||||
retention="10 days",
|
||||
level="DEBUG",
|
||||
compression="zip"
|
||||
)
|
||||
|
||||
|
||||
class CursorServiceManager:
|
||||
def __init__(self):
|
||||
self.config = Config.from_yaml()
|
||||
self.db_manager = DatabaseManager(self.config)
|
||||
self.service_process = None
|
||||
self.upload_process = None
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化数据库连接"""
|
||||
logger.info("初始化数据库连接")
|
||||
await self.db_manager.initialize()
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理资源"""
|
||||
logger.info("清理资源")
|
||||
if self.service_process and self.service_process.poll() is None:
|
||||
logger.info("终止自动化服务进程")
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.service_process.pid)])
|
||||
else:
|
||||
self.service_process.terminate()
|
||||
self.service_process.wait(timeout=5)
|
||||
except Exception as e:
|
||||
logger.error(f"终止进程时出错: {e}")
|
||||
|
||||
if self.upload_process and self.upload_process.poll() is None:
|
||||
logger.info("终止上传进程")
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.upload_process.pid)])
|
||||
else:
|
||||
self.upload_process.terminate()
|
||||
self.upload_process.wait(timeout=5)
|
||||
except Exception as e:
|
||||
logger.error(f"终止进程时出错: {e}")
|
||||
|
||||
await self.db_manager.cleanup()
|
||||
|
||||
async def check_service_flag(self) -> bool:
|
||||
"""检查数据库中是否存在启动标记"""
|
||||
try:
|
||||
# 创建系统设置表(如果不存在)
|
||||
await self.db_manager.execute("""
|
||||
CREATE TABLE IF NOT EXISTS system_settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
key_name VARCHAR(50) UNIQUE NOT NULL,
|
||||
value TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# 检查是否存在自动服务启动标记
|
||||
result = await self.db_manager.fetch_one(
|
||||
"SELECT value FROM system_settings WHERE key_name = 'auto_service_enabled'"
|
||||
)
|
||||
|
||||
if result:
|
||||
return result['value'] == '1'
|
||||
else:
|
||||
# 如果不存在记录,创建默认记录(不启用)
|
||||
await self.db_manager.execute(
|
||||
"INSERT INTO system_settings (key_name, value) VALUES ('auto_service_enabled', '0')"
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"检查服务标记时出错: {e}")
|
||||
return False
|
||||
|
||||
async def set_service_flag(self, enabled: bool):
|
||||
"""设置服务启动标记"""
|
||||
try:
|
||||
value = '1' if enabled else '0'
|
||||
await self.db_manager.execute(
|
||||
"INSERT INTO system_settings (key_name, value) VALUES ('auto_service_enabled', %s) "
|
||||
"ON DUPLICATE KEY UPDATE value = %s",
|
||||
(value, value)
|
||||
)
|
||||
logger.success(f"自动服务标记已{'启用' if enabled else '禁用'}")
|
||||
except Exception as e:
|
||||
logger.error(f"设置服务标记时出错: {e}")
|
||||
|
||||
def start_auto_service(self):
|
||||
"""启动自动化服务"""
|
||||
if self.service_process and self.service_process.poll() is None:
|
||||
logger.info("自动化服务已在运行中")
|
||||
return
|
||||
|
||||
logger.info("启动自动化服务")
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
self.service_process = subprocess.Popen(
|
||||
["python", "auto_cursor_service.py"],
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
)
|
||||
else:
|
||||
self.service_process = subprocess.Popen(
|
||||
["python3", "auto_cursor_service.py"]
|
||||
)
|
||||
|
||||
logger.info(f"自动化服务已启动,PID: {self.service_process.pid}")
|
||||
except Exception as e:
|
||||
logger.error(f"启动自动化服务时出错: {e}")
|
||||
self.service_process = None
|
||||
|
||||
def stop_auto_service(self):
|
||||
"""停止自动化服务"""
|
||||
if not self.service_process or self.service_process.poll() is not None:
|
||||
logger.info("自动化服务未在运行")
|
||||
self.service_process = None
|
||||
return
|
||||
|
||||
logger.info("停止自动化服务")
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.service_process.pid)])
|
||||
else:
|
||||
self.service_process.terminate()
|
||||
self.service_process.wait(timeout=5)
|
||||
|
||||
logger.info("自动化服务已停止")
|
||||
except Exception as e:
|
||||
logger.error(f"停止自动化服务时出错: {e}")
|
||||
|
||||
self.service_process = None
|
||||
|
||||
def start_upload(self):
|
||||
"""启动账号上传"""
|
||||
logger.info("开始上传账号")
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
self.upload_process = subprocess.Popen(
|
||||
["python", "upload_account.py"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
else:
|
||||
self.upload_process = subprocess.Popen(
|
||||
["python3", "upload_account.py"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
# 等待进程完成
|
||||
stdout, stderr = self.upload_process.communicate()
|
||||
|
||||
if self.upload_process.returncode != 0:
|
||||
logger.error(f"上传账号进程异常终止,退出码: {self.upload_process.returncode}")
|
||||
if stderr:
|
||||
logger.error(f"错误输出: {stderr.decode('utf-8', errors='ignore')}")
|
||||
else:
|
||||
logger.info("账号上传完成")
|
||||
if stdout:
|
||||
# 只记录最后几行输出
|
||||
output_lines = stdout.decode('utf-8', errors='ignore').strip().split('\n')
|
||||
for line in output_lines[-5:]:
|
||||
logger.info(f"Upload: {line}")
|
||||
except Exception as e:
|
||||
logger.error(f"执行账号上传脚本时出错: {e}")
|
||||
|
||||
self.upload_process = None
|
||||
|
||||
def start_register(self):
|
||||
"""启动注册进程"""
|
||||
logger.info("启动注册进程")
|
||||
try:
|
||||
# 使用subprocess启动main.py
|
||||
if sys.platform.startswith("win"):
|
||||
register_process = subprocess.Popen(
|
||||
["python", "main.py"],
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
)
|
||||
else:
|
||||
register_process = subprocess.Popen(
|
||||
["python3", "main.py"]
|
||||
)
|
||||
|
||||
logger.info(f"注册进程已启动,PID: {register_process.pid}")
|
||||
return register_process
|
||||
except Exception as e:
|
||||
logger.error(f"启动注册进程时出错: {e}")
|
||||
return None
|
||||
|
||||
async def run_service_check(self):
|
||||
"""检查并启动/停止自动服务"""
|
||||
enabled = await self.check_service_flag()
|
||||
|
||||
if enabled:
|
||||
if not self.service_process or self.service_process.poll() is not None:
|
||||
logger.info("自动服务标记已启用,但服务未运行,现在启动")
|
||||
self.start_auto_service()
|
||||
else:
|
||||
if self.service_process and self.service_process.poll() is None:
|
||||
logger.info("自动服务标记已禁用,但服务仍在运行,现在停止")
|
||||
self.stop_auto_service()
|
||||
|
||||
return enabled
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description="Cursor自动化服务管理工具")
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--enable", action="store_true", help="启用自动服务")
|
||||
group.add_argument("--disable", action="store_true", help="禁用自动服务")
|
||||
group.add_argument("--status", action="store_true", help="查看自动服务状态")
|
||||
group.add_argument("--upload", action="store_true", help="手动上传账号")
|
||||
group.add_argument("--register", action="store_true", help="仅运行注册进程")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
manager = CursorServiceManager()
|
||||
await manager.initialize()
|
||||
|
||||
try:
|
||||
if args.enable:
|
||||
await manager.set_service_flag(True)
|
||||
manager.start_auto_service()
|
||||
|
||||
elif args.disable:
|
||||
await manager.set_service_flag(False)
|
||||
manager.stop_auto_service()
|
||||
|
||||
elif args.status:
|
||||
enabled = await manager.check_service_flag()
|
||||
print(f"自动服务状态: {'已启用' if enabled else '已禁用'}")
|
||||
|
||||
# 检查进程是否在运行
|
||||
if manager.service_process and manager.service_process.poll() is None:
|
||||
print(f"自动服务进程: 运行中 (PID: {manager.service_process.pid})")
|
||||
else:
|
||||
print("自动服务进程: 未运行")
|
||||
|
||||
elif args.upload:
|
||||
manager.start_upload()
|
||||
|
||||
elif args.register:
|
||||
register_process = manager.start_register()
|
||||
if register_process:
|
||||
print(f"注册进程已启动,PID: {register_process.pid}")
|
||||
print("按Ctrl+C结束...")
|
||||
try:
|
||||
# 等待进程结束
|
||||
register_process.wait()
|
||||
except KeyboardInterrupt:
|
||||
print("正在终止注册进程...")
|
||||
if sys.platform.startswith("win"):
|
||||
subprocess.run(["taskkill", "/F", "/T", "/PID", str(register_process.pid)])
|
||||
else:
|
||||
register_process.terminate()
|
||||
|
||||
else:
|
||||
# 默认行为:检查并根据标记启动/停止服务
|
||||
enabled = await manager.run_service_check()
|
||||
print(f"自动服务状态: {'已启用' if enabled else '已禁用'}")
|
||||
|
||||
if enabled:
|
||||
print("自动服务已启动,按Ctrl+C停止...")
|
||||
try:
|
||||
# 保持进程运行,等待用户中断
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("正在停止...")
|
||||
|
||||
finally:
|
||||
await manager.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
118
upgrade_database.py
Normal file
118
upgrade_database.py
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据库升级脚本
|
||||
在已有的数据库中添加新字段和索引
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import yaml
|
||||
import traceback
|
||||
from loguru import logger
|
||||
|
||||
# Windows平台特殊处理,强制使用SelectorEventLoop
|
||||
if sys.platform.startswith('win'):
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
from core.config import Config
|
||||
from core.database import DatabaseManager
|
||||
|
||||
|
||||
async def add_extracted_field(db_manager):
|
||||
"""向email_accounts表添加extracted字段和索引"""
|
||||
logger.info("正在检查extracted字段...")
|
||||
|
||||
# 检查extracted字段是否存在
|
||||
check_query = """
|
||||
SELECT COUNT(*) as field_exists
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'email_accounts'
|
||||
AND COLUMN_NAME = 'extracted'
|
||||
"""
|
||||
|
||||
result = await db_manager.fetch_one(check_query)
|
||||
field_exists = result['field_exists'] > 0 if result else False
|
||||
|
||||
if field_exists:
|
||||
logger.info("extracted字段已存在,无需添加")
|
||||
else:
|
||||
logger.info("添加extracted字段...")
|
||||
add_field_query = """
|
||||
ALTER TABLE email_accounts
|
||||
ADD COLUMN extracted BOOLEAN DEFAULT 0
|
||||
"""
|
||||
await db_manager.execute(add_field_query)
|
||||
logger.success("成功添加extracted字段")
|
||||
|
||||
# 检查索引是否存在
|
||||
check_index_query = """
|
||||
SELECT COUNT(*) as index_exists
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'email_accounts'
|
||||
AND INDEX_NAME = 'idx_extracted'
|
||||
"""
|
||||
|
||||
result = await db_manager.fetch_one(check_index_query)
|
||||
index_exists = result['index_exists'] > 0 if result else False
|
||||
|
||||
if index_exists:
|
||||
logger.info("idx_extracted索引已存在,无需添加")
|
||||
else:
|
||||
logger.info("添加idx_extracted索引...")
|
||||
add_index_query = """
|
||||
ALTER TABLE email_accounts
|
||||
ADD INDEX idx_extracted (extracted, status, sold)
|
||||
"""
|
||||
await db_manager.execute(add_index_query)
|
||||
logger.success("成功添加idx_extracted索引")
|
||||
|
||||
logger.info("数据库升级完成")
|
||||
|
||||
# 设置所有注册成功且已售出的账号的extracted状态
|
||||
update_query = """
|
||||
UPDATE email_accounts
|
||||
SET extracted = 0
|
||||
WHERE status = 'success' AND sold = 1 AND extracted IS NULL
|
||||
"""
|
||||
affected = await db_manager.execute(update_query)
|
||||
logger.info(f"已更新 {affected} 条记录的extracted状态")
|
||||
|
||||
|
||||
async def upgrade_database():
|
||||
"""升级数据库结构"""
|
||||
logger.info("开始数据库升级...")
|
||||
|
||||
try:
|
||||
# 加载配置
|
||||
config = Config.from_yaml()
|
||||
|
||||
# 创建数据库管理器并初始化
|
||||
db_manager = DatabaseManager(config)
|
||||
await db_manager.initialize()
|
||||
|
||||
try:
|
||||
# 添加extracted字段和索引
|
||||
await add_extracted_field(db_manager)
|
||||
|
||||
logger.success("数据库升级成功完成")
|
||||
|
||||
finally:
|
||||
# 清理资源
|
||||
await db_manager.cleanup()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"数据库升级失败: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 设置日志
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="INFO")
|
||||
logger.add("upgrade_database.log", rotation="1 MB", level="DEBUG")
|
||||
|
||||
# 执行升级
|
||||
logger.info("启动数据库升级")
|
||||
asyncio.run(upgrade_database())
|
||||
logger.info("数据库升级脚本执行完毕")
|
||||
455
upload_account.py
Normal file
455
upload_account.py
Normal file
@@ -0,0 +1,455 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import json
|
||||
from typing import List, Dict, Any
|
||||
import aiohttp
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
from core.database import DatabaseManager
|
||||
from core.logger import setup_logger
|
||||
|
||||
|
||||
class AccountUploader:
|
||||
def __init__(self):
|
||||
self.config = Config.from_yaml()
|
||||
self.logger = setup_logger(self.config)
|
||||
self.db_manager = DatabaseManager(self.config)
|
||||
self.api_url = "https://cursorapi.nosqli.com/admin/api.AutoCursor/commonadd"
|
||||
self.batch_size = 100
|
||||
|
||||
# 获取hostname
|
||||
self.hostname = getattr(self.config, "hostname", None)
|
||||
if not self.hostname:
|
||||
# 尝试从config.yaml中的server_config获取
|
||||
server_config = getattr(self.config, "server_config", None)
|
||||
if server_config:
|
||||
self.hostname = getattr(server_config, "hostname", "unknown")
|
||||
else:
|
||||
self.hostname = "unknown"
|
||||
logger.warning(f"未在配置中找到hostname,使用默认值: {self.hostname}")
|
||||
|
||||
# 添加代理设置
|
||||
self.proxy = None
|
||||
if hasattr(self.config, "proxy_config") and hasattr(self.config.proxy_config, "api_proxy"):
|
||||
self.proxy = self.config.proxy_config.api_proxy
|
||||
logger.info(f"使用API代理: {self.proxy}")
|
||||
self.use_proxy = self.proxy is not None
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化数据库"""
|
||||
await self.db_manager.initialize()
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理资源"""
|
||||
await self.db_manager.cleanup()
|
||||
|
||||
async def get_success_accounts(self, limit: int = 100, last_id: int = 0) -> List[Dict[str, Any]]:
|
||||
"""获取状态为success且未提取的账号"""
|
||||
query = """
|
||||
SELECT
|
||||
id, email, password, cursor_password, cursor_cookie, cursor_token
|
||||
FROM email_accounts
|
||||
WHERE status = 'success' AND sold = 1 AND extracted = 0
|
||||
AND id > %s
|
||||
ORDER BY id ASC
|
||||
LIMIT %s
|
||||
"""
|
||||
rows = await self.db_manager.fetch_all(query, (last_id, limit))
|
||||
|
||||
accounts = []
|
||||
for row in rows:
|
||||
account = {
|
||||
"id": row["id"],
|
||||
"email": row["email"],
|
||||
"password": row["password"],
|
||||
"cursor_password": row["cursor_password"],
|
||||
"cursor_cookie": row["cursor_cookie"],
|
||||
"cursor_token": row["cursor_token"]
|
||||
}
|
||||
accounts.append(account)
|
||||
|
||||
return accounts
|
||||
|
||||
async def count_success_accounts(self) -> int:
|
||||
"""统计未提取的成功账号数量"""
|
||||
query = "SELECT COUNT(*) FROM email_accounts WHERE status = 'success' AND sold = 1 AND extracted = 0"
|
||||
result = await self.db_manager.fetch_one(query)
|
||||
if result:
|
||||
count = result.get("COUNT(*)", 0)
|
||||
return count
|
||||
return 0
|
||||
|
||||
async def mark_as_extracted(self, account_ids: List[int]) -> bool:
|
||||
"""将账号标记为已提取"""
|
||||
if not account_ids:
|
||||
return True
|
||||
|
||||
try:
|
||||
placeholders = ', '.join(['%s' for _ in account_ids])
|
||||
|
||||
query = f"""
|
||||
UPDATE email_accounts
|
||||
SET
|
||||
extracted = 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id IN ({placeholders})
|
||||
"""
|
||||
|
||||
await self.db_manager.execute(query, tuple(account_ids))
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"标记账号为已提取时出错: {e}")
|
||||
return False
|
||||
|
||||
async def upload_accounts(self, accounts: List[Dict[str, Any]]) -> bool:
|
||||
"""上传账号到API"""
|
||||
if not accounts:
|
||||
return True
|
||||
|
||||
try:
|
||||
# 准备上传数据
|
||||
upload_data = []
|
||||
for account in accounts:
|
||||
# 根据API需要的格式构建账号项
|
||||
upload_item = {
|
||||
"email": account["email"],
|
||||
"password": account["password"], # 同时提供password字段
|
||||
"email_password": account["password"], # 同时提供email_password字段
|
||||
"cursor_email": account["email"],
|
||||
"cursor_password": account["cursor_password"],
|
||||
"cookie": account["cursor_cookie"] or "", # 确保不为None
|
||||
"token": account.get("cursor_token", ""),
|
||||
"hostname": self.hostname # 添加hostname参数
|
||||
}
|
||||
# 确保所有必须字段都有值
|
||||
for key, value in upload_item.items():
|
||||
if value is None:
|
||||
upload_item[key] = "" # 将None替换为空字符串
|
||||
|
||||
upload_data.append(upload_item)
|
||||
|
||||
# 打印上传数据的结构(去掉长字符串的详细内容)
|
||||
debug_data = []
|
||||
for item in upload_data[:2]: # 只打印前2个账号作为示例
|
||||
debug_item = item.copy()
|
||||
if "cookie" in debug_item and debug_item["cookie"]:
|
||||
debug_item["cookie"] = debug_item["cookie"][:20] + "..." if len(debug_item["cookie"]) > 20 else debug_item["cookie"]
|
||||
if "token" in debug_item and debug_item["token"]:
|
||||
debug_item["token"] = debug_item["token"][:20] + "..." if len(debug_item["token"]) > 20 else debug_item["token"]
|
||||
debug_data.append(debug_item)
|
||||
|
||||
self.logger.debug(f"准备上传数据示例: {json.dumps(debug_data, ensure_ascii=False)}")
|
||||
self.logger.debug(f"API URL: {self.api_url}")
|
||||
self.logger.info(f"上传账号,服务器标识: {self.hostname}")
|
||||
|
||||
# 发送请求
|
||||
# 使用代理创建ClientSession
|
||||
connector = aiohttp.TCPConnector(ssl=False) # 禁用SSL验证以防代理问题
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
# 添加超时设置
|
||||
timeout = aiohttp.ClientTimeout(total=60) # 增加超时时间
|
||||
|
||||
# 准备代理配置
|
||||
proxy = self.proxy if self.use_proxy else None
|
||||
if self.use_proxy:
|
||||
self.logger.debug(f"通过代理发送请求: {self.proxy.split('@')[1] if '@' in self.proxy else self.proxy}")
|
||||
else:
|
||||
self.logger.debug("不使用代理,直接连接API")
|
||||
|
||||
# 根据API错误信息,确保发送的是账号数组
|
||||
self.logger.debug(f"发送账号数组,共 {len(upload_data)} 条记录")
|
||||
|
||||
try:
|
||||
# 直接发送数组格式,使用代理
|
||||
async with session.post(self.api_url, json=upload_data, timeout=timeout, proxy=proxy) as response:
|
||||
response_text = await response.text()
|
||||
self.logger.debug(f"API响应状态码: {response.status}")
|
||||
self.logger.debug(f"API响应内容: {response_text}")
|
||||
|
||||
if response.status != 200:
|
||||
self.logger.error(f"API响应错误 - 状态码: {response.status}")
|
||||
return True # 即使HTTP错误也继续处理下一批
|
||||
|
||||
# 解析响应
|
||||
try:
|
||||
result = json.loads(response_text)
|
||||
except json.JSONDecodeError:
|
||||
self.logger.error(f"响应不是有效的JSON: {response_text}")
|
||||
return True # JSON解析错误也继续处理下一批
|
||||
|
||||
# 判断上传是否成功 - 修改判断逻辑
|
||||
# API返回code为0表示成功
|
||||
if result.get("code") == 0:
|
||||
# 检查data中的成功计数
|
||||
success_count = result.get("data", {}).get("success", 0)
|
||||
failed_count = result.get("data", {}).get("failed", 0)
|
||||
|
||||
self.logger.info(f"API返回结果: 成功: {success_count}, 失败: {failed_count}")
|
||||
|
||||
if success_count > 0:
|
||||
self.logger.success(f"成功上传 {success_count} 个账号")
|
||||
return True
|
||||
else:
|
||||
# 检查详细错误信息
|
||||
details = result.get("data", {}).get("details", [])
|
||||
if details:
|
||||
for detail in details[:3]: # 只显示前三个错误
|
||||
self.logger.error(f"账号 {detail.get('email')} 上传失败: {detail.get('message')}")
|
||||
# 输出更详细的错误信息,帮助诊断
|
||||
self.logger.debug(f"错误账号详情: {json.dumps(detail, ensure_ascii=False)}")
|
||||
|
||||
self.logger.error(f"账号上传全部失败: {result.get('msg', '未知错误')}")
|
||||
return True # 即使全部失败也继续处理下一批
|
||||
else:
|
||||
self.logger.error(f"上传失败: {result.get('msg', '未知错误')}")
|
||||
|
||||
# 检查是否有详细错误信息
|
||||
if 'data' in result:
|
||||
self.logger.error(f"错误详情: {json.dumps(result['data'], ensure_ascii=False)}")
|
||||
|
||||
return True # API返回错误也继续处理下一批
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
self.logger.error(f"HTTP请求错误: {str(e)}")
|
||||
return True # 网络错误也继续处理下一批
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"上传账号时出错: {str(e)}")
|
||||
import traceback
|
||||
self.logger.error(f"错误堆栈: {traceback.format_exc()}")
|
||||
return True # 其他错误也继续处理下一批
|
||||
|
||||
async def process_accounts(self, max_batches: int = 0) -> int:
|
||||
"""处理账号上传,max_batches=0表示处理所有批次"""
|
||||
total_count = await self.count_success_accounts()
|
||||
|
||||
if total_count == 0:
|
||||
self.logger.info("没有找到待上传的账号")
|
||||
return 0
|
||||
|
||||
self.logger.info(f"找到 {total_count} 个待上传账号")
|
||||
|
||||
processed_count = 0
|
||||
batch_count = 0
|
||||
failed_batches = 0 # 记录失败的批次数
|
||||
last_id = 0 # 记录最后处理的ID
|
||||
|
||||
while True:
|
||||
# 检查是否达到最大批次
|
||||
if max_batches > 0 and batch_count >= max_batches:
|
||||
self.logger.info(f"已达到指定的最大批次数 {max_batches}")
|
||||
break
|
||||
|
||||
# 获取一批账号
|
||||
accounts = await self.get_success_accounts(self.batch_size, last_id)
|
||||
|
||||
if not accounts:
|
||||
self.logger.info("没有更多账号可上传")
|
||||
break
|
||||
|
||||
batch_count += 1
|
||||
current_batch_size = len(accounts)
|
||||
self.logger.info(f"处理第 {batch_count} 批 - {current_batch_size} 个账号")
|
||||
|
||||
# 上传账号
|
||||
account_ids = [account["id"] for account in accounts]
|
||||
if last_id < max(account_ids, default=0):
|
||||
last_id = max(account_ids) # 更新最后处理的ID
|
||||
|
||||
upload_success = await self.upload_accounts(accounts)
|
||||
|
||||
if upload_success:
|
||||
# 标记为已提取
|
||||
mark_success = await self.mark_as_extracted(account_ids)
|
||||
if mark_success:
|
||||
processed_count += current_batch_size
|
||||
self.logger.info(f"成功处理 {current_batch_size} 个账号,已标记为已提取")
|
||||
else:
|
||||
failed_batches += 1
|
||||
self.logger.error(f"标记账号为已提取失败,批次 {batch_count}")
|
||||
else:
|
||||
failed_batches += 1
|
||||
self.logger.error(f"上传账号失败,批次 {batch_count}")
|
||||
|
||||
# 避免API请求过于频繁
|
||||
await asyncio.sleep(1)
|
||||
|
||||
self.logger.info(f"账号处理完成: 总共 {processed_count} 个, 失败批次 {failed_batches}")
|
||||
return processed_count
|
||||
|
||||
async def test_api_connection(self) -> bool:
|
||||
"""测试API连接是否正常"""
|
||||
self.logger.info(f"正在测试API连接: {self.api_url}")
|
||||
self.logger.info(f"使用代理: {'使用代理' if self.use_proxy else '不使用代理'}")
|
||||
|
||||
try:
|
||||
# 准备一个简单的测试数据
|
||||
test_data = [
|
||||
{
|
||||
"email": "test@example.com",
|
||||
"password": "test_password",
|
||||
"email_password": "test_password", # 同时提供两个字段
|
||||
"cursor_email": "test@example.com",
|
||||
"cursor_password": "test_cursor_password",
|
||||
"cookie": "test_cookie",
|
||||
"token": "test_token"
|
||||
}
|
||||
]
|
||||
|
||||
# 使用代理创建ClientSession
|
||||
connector = aiohttp.TCPConnector(ssl=False) # 禁用SSL验证以防代理问题
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
# 准备代理配置
|
||||
proxy = self.proxy if self.use_proxy else None
|
||||
|
||||
# 尝试HEAD请求检查服务器是否可达
|
||||
try:
|
||||
self.logger.debug("尝试HEAD请求...")
|
||||
async with session.head(self.api_url, timeout=aiohttp.ClientTimeout(total=10), proxy=proxy) as response:
|
||||
self.logger.debug(f"HEAD响应状态码: {response.status}")
|
||||
if response.status >= 400:
|
||||
self.logger.error(f"API服务器响应错误: {response.status}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.warning(f"HEAD请求失败: {str(e)},尝试GET请求...")
|
||||
|
||||
# 尝试GET请求
|
||||
try:
|
||||
base_url = self.api_url.split('/admin')[0]
|
||||
ping_url = f"{base_url}/ping"
|
||||
self.logger.debug(f"尝试GET请求检查服务健康: {ping_url}")
|
||||
async with session.get(ping_url, timeout=aiohttp.ClientTimeout(total=10), proxy=proxy) as response:
|
||||
self.logger.debug(f"GET响应状态码: {response.status}")
|
||||
if response.status == 200:
|
||||
response_text = await response.text()
|
||||
self.logger.debug(f"GET响应内容: {response_text}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.warning(f"GET请求失败: {str(e)}")
|
||||
|
||||
# 尝试OPTIONS请求获取允许的HTTP方法
|
||||
try:
|
||||
self.logger.debug("尝试OPTIONS请求...")
|
||||
async with session.options(self.api_url, timeout=aiohttp.ClientTimeout(total=10), proxy=proxy) as response:
|
||||
self.logger.debug(f"OPTIONS响应状态码: {response.status}")
|
||||
if response.status == 200:
|
||||
allowed_methods = response.headers.get('Allow', '')
|
||||
self.logger.debug(f"允许的HTTP方法: {allowed_methods}")
|
||||
return 'POST' in allowed_methods
|
||||
except Exception as e:
|
||||
self.logger.warning(f"OPTIONS请求失败: {str(e)}")
|
||||
|
||||
# 最后尝试一个小请求
|
||||
self.logger.debug("尝试轻量级POST请求...")
|
||||
try:
|
||||
# 不同格式的请求体
|
||||
formats = [
|
||||
{"test": "connection"},
|
||||
[{"test": "connection"}],
|
||||
"test"
|
||||
]
|
||||
|
||||
for i, payload in enumerate(formats):
|
||||
try:
|
||||
async with session.post(self.api_url, json=payload, timeout=aiohttp.ClientTimeout(total=10), proxy=proxy) as response:
|
||||
self.logger.debug(f"POST格式{i+1}响应状态码: {response.status}")
|
||||
response_text = await response.text()
|
||||
self.logger.debug(f"POST格式{i+1}响应内容: {response_text[:200]}...")
|
||||
|
||||
# 即使返回错误也是说明服务器可达
|
||||
if response.status != 0:
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.debug(f"POST格式{i+1}失败: {str(e)}")
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"测试POST请求失败: {str(e)}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"API连接测试失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def set_proxy_usage(self, use_proxy: bool):
|
||||
"""设置是否使用代理"""
|
||||
self.use_proxy = use_proxy
|
||||
if use_proxy:
|
||||
self.logger.info(f"已启用HTTP代理: {self.proxy.split('@')[1] if '@' in self.proxy else self.proxy}")
|
||||
else:
|
||||
self.logger.info("已禁用HTTP代理,将直接连接")
|
||||
|
||||
|
||||
async def main():
|
||||
# 解析命令行参数
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="上传成功账号到API并标记为已提取")
|
||||
parser.add_argument("--batches", type=int, default=0, help="处理的最大批次数,0表示处理所有批次")
|
||||
parser.add_argument("--batch-size", type=int, default=100, help="每批处理的账号数量")
|
||||
parser.add_argument("--dry-run", action="store_true", help="预览模式,不实际上传和更新")
|
||||
parser.add_argument("--test-api", action="store_true", help="测试API连接")
|
||||
parser.add_argument("--no-proxy", action="store_true", help="不使用代理直接连接")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 初始化
|
||||
uploader = AccountUploader()
|
||||
await uploader.initialize()
|
||||
|
||||
if args.batch_size > 0:
|
||||
uploader.batch_size = args.batch_size
|
||||
|
||||
# 设置是否使用代理
|
||||
uploader.set_proxy_usage(not args.no_proxy)
|
||||
|
||||
try:
|
||||
# 测试API连接
|
||||
if args.test_api:
|
||||
logger.info("开始测试API连接...")
|
||||
is_connected = await uploader.test_api_connection()
|
||||
if is_connected:
|
||||
logger.success("API连接测试成功!")
|
||||
else:
|
||||
logger.error("API连接测试失败!")
|
||||
return
|
||||
|
||||
# 预览模式
|
||||
if args.dry_run:
|
||||
total_count = await uploader.count_success_accounts()
|
||||
if total_count == 0:
|
||||
logger.info("没有找到待上传的账号")
|
||||
return
|
||||
|
||||
logger.info(f"预览模式:找到 {total_count} 个待上传账号")
|
||||
|
||||
# 获取示例账号
|
||||
accounts = await uploader.get_success_accounts(min(5, total_count))
|
||||
logger.info(f"示例账号 ({len(accounts)}/{total_count}):")
|
||||
|
||||
for account in accounts:
|
||||
logger.info(f"ID: {account['id']}, 邮箱: {account['email']}, Cursor密码: {account['cursor_password']}")
|
||||
|
||||
batch_count = (total_count + uploader.batch_size - 1) // uploader.batch_size
|
||||
logger.info(f"预计分 {batch_count} 批上传,每批 {uploader.batch_size} 个账号")
|
||||
return
|
||||
|
||||
# 实际处理
|
||||
processed = await uploader.process_accounts(args.batches)
|
||||
logger.success(f"处理完成,共上传 {processed} 个账号")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"程序执行出错: {str(e)}")
|
||||
finally:
|
||||
await uploader.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Windows平台特殊处理,强制使用SelectorEventLoop
|
||||
if sys.platform.startswith('win'):
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user