20 Commits

Author SHA1 Message Date
huangzhenpc
74770fb3dd 保存现有功能 增加域名和添加时间关联 2025-04-07 15:32:42 +08:00
huangzhenpc
8fc5a04903 保存现有功能 增加域名和添加时间关联 2025-04-07 15:26:55 +08:00
huangzhenpc
92f7c0f3e0 保存现有功能 增加域名和添加时间关联 2025-04-07 15:23:56 +08:00
huangzhenpc
85c3095e98 保存现有功能 增加域名和添加时间关联 2025-04-07 15:13:07 +08:00
huangzhenpc
69bf430525 保存现有功能 增加域名和添加时间关联 2025-04-07 15:08:49 +08:00
huangzhenpc
de057ae183 保存现有功能 增加域名和添加时间关联 2025-04-07 14:55:33 +08:00
huangzhenpc
901c8c95e1 自建邮箱版本 2025-04-07 13:14:29 +08:00
huangzhenpc
9d00c0b58e 保存现有功能 增加域名和添加时间关联 2025-04-02 10:45:19 +08:00
huangzhenpc
5919b7e657 增加初始化安装引导交互 2025-04-02 10:15:41 +08:00
huangzhenpc
d16f6bdc62 ssss 2025-04-02 09:28:25 +08:00
huangzhenpc
31fe73f998 sdsad 2025-04-01 18:38:16 +08:00
huangzhenpc
221d7d79f1 sdsad 2025-04-01 18:16:02 +08:00
huangzhenpc
6dad187156 xx 2025-04-01 17:58:08 +08:00
huangzhenpc
058f172aba xx 2025-04-01 17:44:38 +08:00
huangzhenpc
2390592bb2 xx 2025-04-01 17:39:22 +08:00
huangzhenpc
6d618d36ac xx 2025-04-01 17:25:45 +08:00
huangzhenpc
46ee69d2f8 xx 2025-04-01 17:22:39 +08:00
huangzhenpc
067dfc84a1 xx 2025-04-01 16:29:01 +08:00
huangzhenpc
b20094adfc 优化自动化 2025-04-01 16:25:08 +08:00
huangzhenpc
7dad9f6b2f 准备创建mysqlv1分支的提交 2025-04-01 15:43:27 +08:00
30 changed files with 5590 additions and 195 deletions

166
AUTO_SERVICE_README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
```
将此批处理文件添加到启动文件夹。

View 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
View 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())

View File

@@ -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
View 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

View File

@@ -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)
# 创建 captcha 配置对象
captcha_data = data['captcha']
captcha_config = CaptchaConfig(
provider=captcha_data['provider'],
capsolver=CapsolverConfig(**captcha_data['capsolver']),
yescaptcha=YesCaptchaConfig(**captcha_data['yescaptcha'])
)
# 创建 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.get('captcha', {})
captcha_config = None
if captcha_data:
captcha_config = CaptchaConfig(
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
)

View File

@@ -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('''
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,
sold BOOLEAN DEFAULT 0,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
await db.commit()
# 初始化连接池
for i in range(self._pool_size):
conn = await aiosqlite.connect(self.db_path)
self._pool.append(conn)
# 创建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 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)
)
''')
# 初始化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()
try:
yield conn
finally:
if len(self._pool) < self._pool_size:
self._pool.append(conn)
else:
await conn.close()
if self._pool is None:
raise Exception("数据库连接池未初始化")
async with self._pool.acquire() as conn:
try:
yield conn
finally:
pass # 连接会自动返回池中
async def execute(self, query: str, params: tuple = ()) -> Any:
"""执行SQL语句"""
async with self.get_connection() as conn:
cursor = await conn.execute(query, params)
await conn.commit()
return cursor.lastrowid
logger.debug(f"执行SQL: {query}, 参数: {params}")
try:
async with self.get_connection() as conn:
async with conn.cursor() as cursor:
await cursor.execute(query, params)
# 对于INSERT语句返回最后插入的ID
if query.strip().upper().startswith("INSERT"):
return cursor.lastrowid
# 对于UPDATE/DELETE语句返回影响的行数
return cursor.rowcount
except Exception as e:
logger.error(f"SQL执行失败: {query}, 参数: {params}, 错误: {str(e)}")
raise
async def fetch_one(self, query: str, params: tuple = ()) -> Optional[tuple]:
async def fetch_one(self, query: str, params: tuple = ()) -> Optional[Dict]:
"""查询单条记录"""
async with self.get_connection() as conn:
cursor = await conn.execute(query, params)
return await cursor.fetchone()
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)
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[tuple]:
async def fetch_all(self, query: str, params: tuple = ()) -> List[Dict]:
"""查询多条记录"""
async with self.get_connection() as conn:
cursor = await conn.execute(query, params)
return await cursor.fetchall()
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
View File

@@ -0,0 +1 @@

2
debug_response.txt Normal file
View 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
View 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)

View File

@@ -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
)
''')
# 读取文件并导入数据
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()}")
# 确保数据库连接已初始化
if not db_manager._pool:
await db_manager.initialize()
# 读取文件并导入数据
count = 0
duplicate_count = 0
error_count = 0
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} 个邮箱账号")
await db.commit()
logger.success(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():
config = Config.from_yaml()
await import_emails(config, "email.txt")
try:
# 加载配置
config = Config.from_yaml()
# 初始化数据库管理器
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
View 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
View File

@@ -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
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("使用自建邮箱模式")
# 确保已初始化自建邮箱服务
if not register.host_register_worker:
register.logger.error("自建邮箱注册工作器未初始化,请检查配置")
return
# 执行批量注册
register.logger.info(f"开始新一轮批量注册,批次大小: {batch_size}")
results = await register.batch_register(batch_size)
# 自建邮箱模式,直接执行自建邮箱批量注册
while True:
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
View 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("数据迁移脚本执行完毕")

View 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

View File

@@ -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

View File

@@ -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} 个未使用的邮箱账号")
# 如果使用Redis确保黑名单已初始化
if self.use_redis:
await self._ensure_blacklist_initialized()
# 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
))
# 如果已经获取足够的账号,退出循环
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
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
'''
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],
in_use=True
)
for row in results
]
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):
"""更新账号状态"""
query = '''
UPDATE email_accounts
SET
status = ?,
in_use = 0,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
'''
await self.db.execute(query, (status, account_id))
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 = %s,
in_use = 0,
updated_at = CURRENT_TIMESTAMP
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):
"""更新账号信息"""
query = '''
UPDATE email_accounts
SET
cursor_password = ?,
cursor_cookie = ?,
cursor_token = ?,
in_use = 0,
sold = 1,
status = 'success',
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
'''
await self.db.execute(query, (cursor_password, cursor_cookie, cursor_token, account_id))
"""更新账号信息(注册成功)"""
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 = %s,
cursor_cookie = %s,
cursor_token = %s,
in_use = 0,
sold = 1,
status = 'success',
updated_at = CURRENT_TIMESTAMP
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

View File

@@ -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(

View 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
View 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
View 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
View 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
View 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
View 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())