初次同步
This commit is contained in:
parent
eb4fd2bd8a
commit
c0ff696d78
62
.env.example
Normal file
62
.env.example
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# 浏览器是否无头模式,为True时为无头模式(无界面),为False时为有头模式(有界面)
|
||||||
|
BROWSER_HEADLESS=True
|
||||||
|
# 浏览器用户代理
|
||||||
|
BROWSER_USER_AGENT="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
|
||||||
|
# 是否使用动态ua池
|
||||||
|
DYNAMIC_USERAGENT=False
|
||||||
|
# Windows 浏览器路径
|
||||||
|
#BROWSER_PATH="C:\Users\Administrator\AppData\Local\Google\Chrome\Bin\chrome.exe"
|
||||||
|
# 是否启用浏览器代理
|
||||||
|
USE_PROXY=False
|
||||||
|
# 代理类型
|
||||||
|
PROXY_TYPE=http
|
||||||
|
# 代理ip
|
||||||
|
PROXY_HOST=127.0.0.1
|
||||||
|
# 代理端口号
|
||||||
|
PROXY_PORT=7897
|
||||||
|
# 代理超时时间
|
||||||
|
PROXY_TIMEOUT=10
|
||||||
|
|
||||||
|
|
||||||
|
# 临时邮箱类型 可选值:tempemail, zmail
|
||||||
|
EMAIL_TYPE=tempemail
|
||||||
|
# 多个域名使用逗号分隔
|
||||||
|
EMAIL_DOMAINS=xxx.xx
|
||||||
|
|
||||||
|
# 临时邮箱用户名
|
||||||
|
EMAIL_USERNAME=xxx
|
||||||
|
# 临时邮箱PIN码(如果需要)
|
||||||
|
EMAIL_PIN=
|
||||||
|
#EMAIL_CODE_TYPE=INPUT #验证码获取方式INPUT 或者 API
|
||||||
|
|
||||||
|
# ===== ZMail配置 =====
|
||||||
|
# ZMail API地址
|
||||||
|
EMAIL_API=https://xxxxxxxxxxxxxxx
|
||||||
|
# 是否启用邮箱API代理
|
||||||
|
EMAIL_PROXY_ENABLED=True
|
||||||
|
# 邮箱API代理地址
|
||||||
|
EMAIL_PROXY_ADDRESS=http://ip:port
|
||||||
|
|
||||||
|
# ===== 账号管理配置 =====
|
||||||
|
# 系统最大已激活的账号数量,如果达到这个数量,则停止注册
|
||||||
|
# so 要么在页面维护好当前已激活的账号,要么在页面删除账号,或者直接增大该值
|
||||||
|
MAX_ACCOUNTS=10
|
||||||
|
|
||||||
|
# 数据库URL
|
||||||
|
DATABASE_URL="sqlite+aiosqlite:///./accounts.db"
|
||||||
|
|
||||||
|
# ===== API服务配置 =====
|
||||||
|
# API服务监听主机地址,0.0.0.0 允许非本机访问
|
||||||
|
API_HOST="0.0.0.0"
|
||||||
|
# API服务端口号
|
||||||
|
API_PORT=8000
|
||||||
|
# 是否启用调试模式
|
||||||
|
API_DEBUG=True
|
||||||
|
# API服务工作进程数量(Windows下建议使用1)
|
||||||
|
API_WORKERS=1
|
||||||
|
# 是否启用UI
|
||||||
|
ENABLE_UI=True
|
||||||
|
|
||||||
|
# Cursor main.js 主文件路径
|
||||||
|
# windows用户部分安装时是自定义目录安装的,需要修改该配置
|
||||||
|
#CURSOR_PATH="D:\devtools\cursor"
|
32
.env11
Normal file
32
.env11
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
BROWSER_HEADLESS=True
|
||||||
|
DYNAMIC_USERAGENT=True
|
||||||
|
|
||||||
|
# 多个域名使用逗号分隔
|
||||||
|
EMAIL_DOMAINS=bjh93fxs.shop
|
||||||
|
|
||||||
|
# 临时邮箱用户名
|
||||||
|
EMAIL_USERNAME=zbofano
|
||||||
|
# 临时邮箱PIN码(如果需要)
|
||||||
|
EMAIL_PIN=
|
||||||
|
|
||||||
|
# 数据库URL
|
||||||
|
DATABASE_URL="sqlite+aiosqlite:///./accounts.db"
|
||||||
|
|
||||||
|
# ===== API服务配置 =====
|
||||||
|
# API服务监听主机地址,0.0.0.0 允许非本机访问
|
||||||
|
API_HOST="0.0.0.0"
|
||||||
|
# API服务端口号
|
||||||
|
API_PORT=8000
|
||||||
|
# 是否启用UI
|
||||||
|
ENABLE_UI=True
|
||||||
|
# 最大注册账号数量
|
||||||
|
MAX_ACCOUNTS=1
|
||||||
|
# windows用户部分安装时是自定义目录安装的,需要修改该配置
|
||||||
|
CURSOR_PATH=C:\Usersbkino\AppData\Local\Programs\cursor
|
||||||
|
BROWSER_PATH=C:\Program Files\Google\Chrome\Application\chrome.exe
|
||||||
|
EMAIL_CODE_TYPE=AUTO
|
||||||
|
|
||||||
|
USE_PROXY=False
|
||||||
|
PROXY_TYPE=http
|
||||||
|
PROXY_TIMEOUT=10
|
||||||
|
RESTART_TIMESTAMP=1743262912
|
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Python 编译文件和缓存
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# 虚拟环境
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv/
|
||||||
|
.env/
|
||||||
|
.ENV/
|
||||||
|
|
||||||
|
# 日志文件
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# 本地配置文件
|
||||||
|
.env
|
||||||
|
|
||||||
|
# 数据库文件
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# IDE 文件
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# 临时文件
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# 其他可能的敏感文件
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
credentials.json
|
277
Project_Overview.md
Normal file
277
Project_Overview.md
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
# Cursor Auto Register 项目总览
|
||||||
|
|
||||||
|
**项目简介**: 本项目是一个开源的自动化 Cursor Pro 账户注册和管理工具,旨在为学习和技术交流提供参考。
|
||||||
|
|
||||||
|
**主要目的**: 本项目旨在简化 Cursor Pro 账户的获取流程,并提供 API 接口供二次开发或集成。
|
||||||
|
|
||||||
|
**参考项目**:
|
||||||
|
* [chengazhen/cursor-auto-free](https://github.com/chengazhen/cursor-auto-free)
|
||||||
|
* [cursor-account-api](https://github.com/Elawen-Carl/cursor-account-api)
|
||||||
|
|
||||||
|
**免责声明**: 本项目仅供学习和测试使用,使用风险自负。建议参考项目根目录下的 `README.md` 文件获取更详细的免责声明和许可信息。
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
* 自动化账户注册与激活:能够自动完成 Cursor 账户的注册流程。
|
||||||
|
* 提供 RESTful API 进行账户管理:包括创建、查询(支持分页、搜索、排序)、随机获取、状态更新、数据导入/导出等功能。
|
||||||
|
* 提供简单的 Web UI:用于查看系统状态、账户列表、配置信息,并进行基本操作。
|
||||||
|
* 集成临时邮箱服务:自动化获取注册过程中所需的邮件验证码。
|
||||||
|
* 支持配置项管理:允许用户通过 `.env` 文件或 API 修改如最大账户数、API 服务参数、浏览器行为等配置。
|
||||||
|
* 账户数据持久化存储:使用 SQLite 数据库 (`accounts.db`) 保存账户信息及使用记录。
|
||||||
|
* 详细的日志记录:记录系统运行状态、注册过程、错误信息等,便于追踪和调试。
|
||||||
|
* 后台任务管理:支持启动、停止和监控后台的自动注册任务。
|
||||||
|
* 系统辅助功能:如重置机器ID、服务重启等。
|
||||||
|
|
||||||
|
## 技术架构概览
|
||||||
|
|
||||||
|
* **后端**: 采用 Python 语言和 FastAPI 异步 Web 框架构建,提供高效的 API 服务。
|
||||||
|
* **数据库**: 使用 SQLite 进行数据存储,通过 `aiosqlite` 库实现异步数据库操作,确保在高并发场景下的性能。账户信息和相关记录存储在项目根目录的 `accounts.db` 文件中。
|
||||||
|
* **前端**: 提供一个基础的 HTML 前端界面 (`index.html`),结合 `static/` 目录下的静态资源 (CSS, JavaScript),为用户提供基本的可视化操作和信息展示。
|
||||||
|
* **核心依赖与工具**:
|
||||||
|
* 编程语言: Python 3.10+
|
||||||
|
* Web 框架: FastAPI, Uvicorn (ASGI 服务器)
|
||||||
|
* 数据库ORM/Driver: SQLAlchemy (异步模式), aiosqlite
|
||||||
|
* 浏览器自动化: 可能通过 Selenium 或 Playwright (封装在 `browser_utils.py` 中) 实现与浏览器的交互。
|
||||||
|
* 邮件服务: 依赖外部临时邮箱服务 (如 tempmail.plus) 获取验证码,通常需要用户配置 Cloudflare 邮件路由规则进行转发。
|
||||||
|
* 人机验证处理: 包含针对 Cloudflare Turnstile 等验证码的应对逻辑 (体现在 `turnstilePatch/` 目录及相关代码中)。
|
||||||
|
* **配置管理**: 通过根目录的 `.env` 文件进行环境变量配置,由 `config.py` 读取和管理。
|
||||||
|
* **部署**:
|
||||||
|
* 本地运行: 可直接通过 `uvicorn api:app --host <host> --port <port> --reload` 命令启动 (具体命令依据 `README.md` 或启动脚本)。
|
||||||
|
* 容器化: 项目提供了 `dockerfile`,支持使用 Docker 进行构建和部署,方便环境隔离和迁移。
|
||||||
|
|
||||||
|
## 主要模块/组件详解
|
||||||
|
|
||||||
|
### `api.py` (FastAPI 应用层)
|
||||||
|
项目的核心入口和控制中心。基于 FastAPI 构建,定义了所有的 HTTP API 端点,负责:
|
||||||
|
* 接收和验证客户端请求。
|
||||||
|
* 路由请求到相应的业务逻辑处理函数。
|
||||||
|
* 调用其他模块(如 `database.py`, `cursor_pro_keep_alive.py`)完成具体操作。
|
||||||
|
* 格式化并返回响应给客户端。
|
||||||
|
* 实现诸如账户管理、注册任务控制、系统配置查询与更新、UI服务、API文档(Swagger/ReDoc)等功能。
|
||||||
|
* 管理应用的生命周期事件(如启动时初始化数据库)。
|
||||||
|
|
||||||
|
### `cursor_pro_keep_alive.py` (核心注册逻辑)
|
||||||
|
此模块封装了 Cursor 账户自动化注册和激活的核心业务流程。其主要职责包括:
|
||||||
|
* 执行完整的账户创建步骤,从访问注册页面到最终激活账户。
|
||||||
|
* 周期性地被 `api.py` 中的后台任务调用,以维持设定的账户数量。
|
||||||
|
* 协调调用 `get_email_code.py` 来获取邮箱地址和验证码。
|
||||||
|
* 利用 `browser_utils.py` 进行浏览器自动化操作(如填写表单、点击链接)。
|
||||||
|
* 处理注册过程中可能遇到的 Cloudflare Turnstile 等人机验证(可能调用 `turnstilePatch/` 中的逻辑)。
|
||||||
|
* 在注册成功或失败后,更新账户状态并记录相关信息。
|
||||||
|
|
||||||
|
### `database.py` (数据持久化层)
|
||||||
|
负责所有与数据库 (`accounts.db`) 相关的操作。
|
||||||
|
* 定义数据模型,如 `AccountModel` (存储账户信息) 和 `AccountUsageRecordModel` (存储账户使用记录)。
|
||||||
|
* 提供数据库初始化函数 `init_db()`,在应用启动时创建表结构。
|
||||||
|
* 封装了异步的 CRUD (创建、读取、更新、删除) 操作接口,供 `api.py` 等模块调用,以管理账户数据。
|
||||||
|
* 使用 SQLAlchemy Core 和 `aiosqlite` 实现与 SQLite 的异步交互。
|
||||||
|
|
||||||
|
### `get_email_code.py` (邮件处理模块)
|
||||||
|
专用于处理邮件相关的任务,特别是自动化获取注册过程中所需的邮件验证码。
|
||||||
|
* 与用户配置的临时邮箱服务 (如 tempmail.plus) API 进行交互。
|
||||||
|
* 获取可用的临时邮箱地址。
|
||||||
|
* 监控邮箱,等待并提取 Cursor 发送的验证码邮件内容。
|
||||||
|
|
||||||
|
### `config.py` 与 `.env` (配置模块)
|
||||||
|
管理项目的各项配置参数。
|
||||||
|
* `.env`: 纯文本文件,用于存储用户特定的配置值(如 API 密钥、邮箱账户、数据库路径、服务器端口等),不应提交到版本控制。项目提供了 `.env.example` 作为模板。
|
||||||
|
* `config.py`: Python 脚本,定义所有可配置参数的名称、数据类型、默认值,并从 `.env` 文件或环境变量中加载实际配置值。这些配置项在整个应用中被其他模块引用。
|
||||||
|
|
||||||
|
### `tokenManager/` (Token 管理模块)
|
||||||
|
此目录 (特别是 `tokenManager/cursor.py` 中的 `Cursor` 类) 封装了与 Cursor 服务端进行认证和 token 管理的逻辑。
|
||||||
|
* 可能包括获取新的用户 token、刷新即将过期的 token、验证 token 有效性等功能。
|
||||||
|
* 在账户注册成功后,或在需要代表用户与 Cursor API 交互时被调用。
|
||||||
|
|
||||||
|
### `browser_utils.py` (浏览器工具)
|
||||||
|
提供一系列用于控制和自动化浏览器行为的辅助函数。
|
||||||
|
* 封装了与浏览器驱动 (如 ChromeDriver for Selenium) 的交互。
|
||||||
|
* 功能可能包括:启动浏览器(有头或无头模式)、打开指定 URL、查找页面元素、模拟用户输入(填写表单)、点击按钮、执行 JavaScript 脚本等。
|
||||||
|
* 主要被 `cursor_pro_keep_alive.py` 在自动化注册流程中使用。
|
||||||
|
|
||||||
|
### `turnstilePatch/` (人机验证处理)
|
||||||
|
此目录下的代码专注于处理 Cloudflare Turnstile 等类型的人机验证挑战。
|
||||||
|
* 可能包含识别验证码、与第三方验证码识别服务API交互、或尝试其他绕过验证的策略。
|
||||||
|
* 在自动化注册过程中,当遇到此类验证时被调用。
|
||||||
|
|
||||||
|
### `logger.py` (日志系统)
|
||||||
|
提供标准化的日志记录功能。
|
||||||
|
* 配置日志的格式、级别 (INFO, ERROR, DEBUG 等) 和输出目标 (如控制台、日志文件 `app.log`, `api.log`)。
|
||||||
|
* 供项目中所有其他模块调用,以记录关键操作、程序流程、错误信息和调试信息。
|
||||||
|
|
||||||
|
### `index.html` 与 `static/` (前端展现层)
|
||||||
|
构成项目的用户界面。
|
||||||
|
* `index.html`: 单页应用的主 HTML 文件,定义了页面的基本骨架。
|
||||||
|
* `static/`: 存放 CSS 样式表、JavaScript 脚本、图片等静态资源,用于美化界面和实现前端交互逻辑。
|
||||||
|
* 前端通过调用 `api.py` 提供的 API 来获取数据、展示信息和提交用户操作。
|
||||||
|
|
||||||
|
### `reset_machine.py` (机器ID重置模块)
|
||||||
|
提供了一个特定的功能,用于重置或修改被 Cursor 服务识别的机器 ID。
|
||||||
|
* 这可能用于解决因同一设备注册过多账户而被限制的问题。
|
||||||
|
* 通常通过 `api.py` 中的特定 API 端点 (`/reset-machine`) 触发。
|
||||||
|
|
||||||
|
## 核心工作流程示例
|
||||||
|
|
||||||
|
### 新账户自动注册流程
|
||||||
|
1. **任务触发**: `api.py` 中的后台任务 (`run_registration`) 按照预设的间隔 (`REGISTRATION_INTERVAL`) 检查当前激活账户数量是否低于 `MAX_ACCOUNTS`。
|
||||||
|
2. **启动注册**: 如果需要新账户,任务调用 `cursor_pro_keep_alive.main()` 函数。
|
||||||
|
3. **获取邮箱**: `cursor_pro_keep_alive.py` 调用 `get_email_code.py`,后者与临时邮箱服务交互,获取一个临时邮箱地址。
|
||||||
|
4. **浏览器操作与表单填写**: `cursor_pro_keep_alive.py` 使用 `browser_utils.py` 控制浏览器(可能是无头模式)打开 Cursor 注册页面,并填入获取的邮箱地址及生成的密码等信息。
|
||||||
|
5. **处理人机验证 (如果出现)**: 如果注册页面出现 Cloudflare Turnstile 等人机验证,`cursor_pro_keep_alive.py` 可能会调用 `turnstilePatch/` 中的逻辑尝试解决。
|
||||||
|
6. **提交注册与接收验证码**: 提交注册信息后,`cursor_pro_keep_alive.py` 再次通过 `get_email_code.py` 监控临时邮箱,等待并提取 Cursor 发送的验证邮件中的激活链接或验证码。
|
||||||
|
7. **账户激活**: 使用获取到的验证码或激活链接,通过 `browser_utils.py` 完成账户激活步骤。
|
||||||
|
8. **获取 Token (如果适用)**: 激活后,可能通过 `tokenManager/cursor.py` 获取与该账户关联的 Cursor token。
|
||||||
|
9. **数据入库**: 注册成功后,将账户的邮箱、密码、token(如有)、状态等信息通过 `database.py` 提供的接口存入 `accounts.db` 数据库。
|
||||||
|
10. **日志记录**: 整个过程中的关键步骤、成功或失败信息都会通过 `logger.py` 记录下来。
|
||||||
|
|
||||||
|
### 用户通过 API 获取随机账户流程
|
||||||
|
1. **API 请求**: 用户或外部应用向 `api.py` 发送 `GET` 请求到 `/account/random` 端点。
|
||||||
|
2. **请求处理**: `api.py` 中的对应路由处理函数接收到该请求。
|
||||||
|
3. **数据库查询**: 该处理函数调用 `database.py` 中封装的数据库查询方法,从 `accounts` 表中随机选择一个状态为 "active" (或其他可用状态) 的账户记录。
|
||||||
|
4. **数据格式化**: `database.py` 返回查询结果给 `api.py`。`api.py` 可能需要将原始数据模型转换为 Pydantic 定义的响应模型,确保返回数据的结构和类型正确。
|
||||||
|
5. **API 响应**: `api.py` 将包含随机账户信息的 JSON 数据作为 HTTP 响应返回给请求方。
|
||||||
|
6. **日志记录**: API 调用信息(如请求路径、时间、结果等)可能会被 `logger.py` 记录。
|
||||||
|
|
||||||
|
## 环境要求与部署
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
* **Python**: 版本 3.10 或更高。
|
||||||
|
* **pip**: Python 包管理器,用于安装项目依赖。
|
||||||
|
* **依赖包**: 所有必需的 Python 库均在 `requirements.txt` 文件中列出。可以通过 `pip install -r requirements.txt` 命令一键安装。
|
||||||
|
* **外部服务**:
|
||||||
|
* 临时邮箱服务 (如 tempmail.plus) 并正确配置 Cloudflare DNS 及邮件路由规则,以便 `get_email_code.py` 能够接收验证码。
|
||||||
|
* 网络连接:用于访问 Cursor 网站、临时邮箱服务以及可能的验证码服务。
|
||||||
|
|
||||||
|
### 配置
|
||||||
|
项目的核心配置通过位于根目录下的 `.env` 文件进行管理。在首次运行前,需要根据 `.env.example` 文件创建一个 `.env` 文件,并填入实际的配置值。关键配置项包括:
|
||||||
|
* `EMAIL_DOMAINS`: 用于接收邮件的域名(需配合Cloudflare)。
|
||||||
|
* `EMAIL_USERNAME`: 临时邮箱服务提供的用户名/邮箱前缀。
|
||||||
|
* `EMAIL_PIN`: 临时邮箱服务可能需要的 PIN 码。
|
||||||
|
* `DATABASE_URL`: SQLite 数据库文件的路径,默认为 `sqlite+aiosqlite:///./accounts.db`。
|
||||||
|
* `API_HOST`: API 服务监听的主机地址 (如 `0.0.0.0` 允许外部访问)。
|
||||||
|
* `API_PORT`: API 服务监听的端口号 (如 `8000`)。
|
||||||
|
* `ENABLE_UI`: 是否启用 Web UI (True/False)。
|
||||||
|
* `MAX_ACCOUNTS`: 系统维护的最大激活账户数量。
|
||||||
|
* `CURSOR_PATH` (可选): Cursor 客户端的安装路径,某些功能可能需要。
|
||||||
|
* 浏览器相关配置 (如 `BROWSER_HEADLESS`, `BROWSER_PATH`等)。
|
||||||
|
* 代理配置 (如 `USE_PROXY`, `PROXY_HOST`, `PROXY_PORT`等)。
|
||||||
|
|
||||||
|
### 运行与部署
|
||||||
|
|
||||||
|
**1. 本地开发/直接运行:**
|
||||||
|
a. 确保已安装 Python 3.10+ 和 pip。
|
||||||
|
b. 克隆项目代码到本地。
|
||||||
|
c. 在项目根目录下,复制 `.env.example` 为 `.env`,并修改其中的配置项。
|
||||||
|
d. 安装依赖:`pip install -r requirements.txt`
|
||||||
|
e. 启动应用:通常使用 Uvicorn 作为 ASGI 服务器来运行 FastAPI 应用。命令如下:
|
||||||
|
```bash
|
||||||
|
uvicorn api:app --host <API_HOST_VALUE> --port <API_PORT_VALUE> --reload
|
||||||
|
```
|
||||||
|
将 `<API_HOST_VALUE>` 和 `<API_PORT_VALUE>`替换为 `.env` 中配置的值 (例如 `0.0.0.0` 和 `8000`)。`--reload` 参数可以在代码变更时自动重启服务,适合开发环境。
|
||||||
|
项目根目录下的 `启动服务器.bat` (Windows) 可能封装了此命令。
|
||||||
|
|
||||||
|
**2. Docker 部署:**
|
||||||
|
项目提供了 `dockerfile`,可以用于构建 Docker 镜像并进行容器化部署。
|
||||||
|
a. 确保已安装 Docker。
|
||||||
|
b. 在项目根目录下,构建 Docker 镜像:
|
||||||
|
```bash
|
||||||
|
docker build -t cursor-auto-register .
|
||||||
|
```
|
||||||
|
c. 运行 Docker 容器 (需要将 `.env` 文件或环境变量传递给容器):
|
||||||
|
```bash
|
||||||
|
docker run -d --env-file .env -p <HOST_PORT>:<CONTAINER_PORT> --name cursor_register_app cursor-auto-register
|
||||||
|
```
|
||||||
|
将 `<HOST_PORT>` 替换为希望在宿主机上映射的端口,`<CONTAINER_PORT>` 替换为容器内应用监听的端口 (即 `.env` 中配置的 `API_PORT`)。`-d` 参数使容器在后台运行。确保 `.env` 文件在执行 `docker run` 命令的上下文中可访问,或者使用其他方式 (如 `-e` 参数) 传递环境变量。
|
||||||
|
|
||||||
|
## 使用与API
|
||||||
|
|
||||||
|
### Web UI
|
||||||
|
如果 `ENABLE_UI` 配置为 `True`,在服务成功启动后,可以通过浏览器访问部署的地址来使用 Web UI。默认情况下,如果服务运行在本地且端口为 `8000`,则 UI 地址为:
|
||||||
|
`http://localhost:8000/` 或 `http://127.0.0.1:8000/`
|
||||||
|
|
||||||
|
Web UI 通常提供以下功能:
|
||||||
|
* 查看当前系统状态和注册任务信息。
|
||||||
|
* 展示已注册的账户列表及其状态。
|
||||||
|
* 查看和修改部分系统配置。
|
||||||
|
* 手动触发某些操作。
|
||||||
|
|
||||||
|
### 主要 API 端点
|
||||||
|
项目通过 FastAPI 提供了一系列 RESTful API 端点,便于程序化交互和集成。以下是一些核心端点示例:
|
||||||
|
|
||||||
|
* **`GET /health`**:
|
||||||
|
* 描述:检查 API 服务的健康状况。
|
||||||
|
* 响应:返回服务运行状态。
|
||||||
|
|
||||||
|
* **`GET /accounts`**:
|
||||||
|
* 描述:获取账户列表,支持分页、搜索和排序。
|
||||||
|
* 参数(Query):`page`, `per_page`, `search`, `sort_by`, `order`。
|
||||||
|
* 响应:包含账户列表及分页信息的 JSON 数据。
|
||||||
|
|
||||||
|
* **`GET /account/random`**:
|
||||||
|
* 描述:随机获取一个当前状态为可用的账户。
|
||||||
|
* 响应:包含单个账户详细信息的 JSON 数据。
|
||||||
|
|
||||||
|
* **`POST /account`**:
|
||||||
|
* 描述:手动创建一个新的账户记录(注意:这通常区别于后台的自动注册任务,具体用途需参考实现,可能用于导入已有账户或特定测试)。
|
||||||
|
* 请求体:包含账户信息的 JSON 对象 (如 `email`, `password`, `token`)。
|
||||||
|
* 响应:操作结果及创建的账户信息。
|
||||||
|
|
||||||
|
* **`DELETE /account/{email}` 或 `DELETE /account/id/{id}`**:
|
||||||
|
* 描述:删除指定邮箱或ID的账户。可能支持软删除或硬删除。
|
||||||
|
* 响应:操作结果。
|
||||||
|
|
||||||
|
* **`PUT /account/id/{id}/status`**:
|
||||||
|
* 描述:更新指定ID账户的状态。
|
||||||
|
* 请求体:包含新状态的 JSON 对象 (如 `{"status": "inactive"}`)。
|
||||||
|
* 响应:操作结果及更新后的账户信息。
|
||||||
|
|
||||||
|
* **`GET /registration/start`**:
|
||||||
|
* 描述:启动后台的自动注册任务(如果尚未运行)。
|
||||||
|
* 响应:任务启动状态。
|
||||||
|
|
||||||
|
* **`GET /registration/stop`**:
|
||||||
|
* 描述:停止后台的自动注册任务。
|
||||||
|
* 响应:任务停止状态。
|
||||||
|
|
||||||
|
* **`GET /registration/status`**:
|
||||||
|
* 描述:获取当前后台自动注册任务的详细状态(如是否运行、上次运行时间、成功/失败次数等)。
|
||||||
|
* 响应:包含任务状态信息的 JSON 数据。
|
||||||
|
|
||||||
|
* **`GET /config`**:
|
||||||
|
* 描述:获取当前系统的配置信息。
|
||||||
|
* 响应:包含所有可配置项及其当前值的 JSON 数据。
|
||||||
|
|
||||||
|
* **`POST /config`**:
|
||||||
|
* 描述:更新系统配置项。
|
||||||
|
* 请求体:包含要修改的配置项及其新值的 JSON 对象。
|
||||||
|
* 响应:操作结果。
|
||||||
|
|
||||||
|
### API 文档
|
||||||
|
服务启动后,FastAPI 会自动生成交互式的 API 文档:
|
||||||
|
* **Swagger UI**: 可通过访问 `/docs` 路径 (例如 `http://localhost:8000/docs`) 查看和测试 API。
|
||||||
|
* **ReDoc**: 可通过访问 `/redoc` 路径 (例如 `http://localhost:8000/redoc`) 获取另一种格式的 API 文档。
|
||||||
|
|
||||||
|
这些文档详细列出了所有可用的 API 端点、请求参数、请求体结构、响应格式以及示例。
|
||||||
|
|
||||||
|
## 注意事项与免责声明
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
1. **配置准确性**: 请务必确保 `.env` 文件中的各项配置(尤其是 `EMAIL_DOMAINS`, `EMAIL_USERNAME`, 以及 Cloudflare 相关设置)正确无误,错误的配置可能导致注册失败或功能异常。
|
||||||
|
2. **依赖服务**: 本项目的许多核心功能(如邮件验证码获取)依赖于第三方服务(如临时邮箱平台、Cloudflare)。这些服务的稳定性、可用性或政策变更可能会直接影响本项目的正常运行。
|
||||||
|
3. **Cloudflare 设置**: 使用自定义域名接收邮件时,必须正确配置 Cloudflare 的 DNS 解析以及邮件路由 (Email Routing) 规则,将特定邮件地址或 Catch-all 地址转发到你实际使用的临时邮箱服务能够接收的地址。
|
||||||
|
4. **资源权限**: 确保程序运行时对 `accounts.db` 数据库文件以及 `app.log`, `api.log` 等日志文件有足够的读写权限。
|
||||||
|
5. **并发与限制**: 自动化注册大量账户可能会触发目标网站(Cursor)的速率限制、更严格的人机验证或账户封禁策略。请合理配置 `MAX_ACCOUNTS` 和 `REGISTRATION_INTERVAL`,避免滥用。
|
||||||
|
6. **浏览器与驱动**: 如果项目使用 Selenium/Playwright 等浏览器自动化工具,请确保对应的浏览器已安装,并且浏览器驱动版本与浏览器版本兼容。
|
||||||
|
7. **法律与合规**: 用户应自行了解并遵守 Cursor 的服务条款。滥用本工具可能导致违反相关条款。
|
||||||
|
|
||||||
|
### 免责声明
|
||||||
|
本项目 (`Cursor Auto Register`) 严格仅供个人学习、教育和技术研究目的使用。严禁将本项目用于任何商业用途或任何可能违反适用法律法规的活动。
|
||||||
|
|
||||||
|
开发者不对用户如何使用本项目承担任何责任。用户必须独立承担因使用或无法使用本项目所导致的一切直接或间接风险和后果,包括但不限于:
|
||||||
|
* 账户被目标服务(如 Cursor)限制或封禁。
|
||||||
|
* 违反目标服务的使用条款。
|
||||||
|
* 数据丢失或损坏。
|
||||||
|
* 任何经济损失或其他形式的损害。
|
||||||
|
|
||||||
|
通过下载、复制、修改或使用本项目的任何部分,即表示用户已阅读、理解并同意上述所有条款和条件。如果用户不同意这些条款,请勿使用本项目。
|
||||||
|
|
||||||
|
开发者保留随时修改或终止本项目的权利,恕不另行通知。
|
167
api_module_documentation.md
Normal file
167
api_module_documentation.md
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# Module Documentation: api.py
|
||||||
|
|
||||||
|
## 1. 概述 (Overview)
|
||||||
|
|
||||||
|
`api.py` 文件实现了一个基于 FastAPI 的 Web 应用,其核心目的是提供一套 API 接口来管理 Cursor IDE 的账号。此应用不仅支持账号的增删改查、导入导出等基本管理功能,还集成了一个自动化的后台任务来注册新账号,直到达到预设的上限。此外,它还能跟踪账号的使用情况、管理系统配置,并提供与本地 Cursor 客户端集成的功能,如更新认证 Token 和重置机器ID。
|
||||||
|
|
||||||
|
主要技术栈包括:
|
||||||
|
* **FastAPI**: 用于构建高性能 API。
|
||||||
|
* **SQLAlchemy**: 作为 ORM 与数据库进行异步交互。
|
||||||
|
* **Pydantic**: 用于数据校验和 API 的请求/响应模型定义。
|
||||||
|
* **Uvicorn**: 作为 ASGI 服务器运行应用。
|
||||||
|
* **Asyncio**: 支持异步操作和并发任务,特别是后台注册进程。
|
||||||
|
|
||||||
|
## 2. 核心功能 (Core Features)
|
||||||
|
|
||||||
|
### 2.1. 账号管理 (Account Management)
|
||||||
|
该模块提供了全面的账号管理功能:
|
||||||
|
- **创建账号**: 通过 `POST /account` 端点,可以添加新的 Cursor 账号信息到数据库。
|
||||||
|
- **读取账号**:
|
||||||
|
- `GET /accounts`: 获取所有账号列表,支持分页、按邮件模糊搜索以及按字段(如创建时间、ID、邮箱)升序或降序排序。
|
||||||
|
- `GET /account/random`: 随机获取一个当前可用的账号及其 Token。
|
||||||
|
- `GET /account/{email}/usage`: 查询指定邮箱账号的详细使用情况(如剩余额度、天数)并更新数据库记录。
|
||||||
|
- **更新账号状态**: 通过 `PUT /account/id/{id}/status` 端点,可以修改指定 ID 账号的状态(如 active, disabled, deleted)。
|
||||||
|
- **删除账号**:
|
||||||
|
- `DELETE /account/{email}`: 根据邮箱停用(逻辑删除)或永久删除(物理删除,需 `hard_delete=True`)账号。
|
||||||
|
- `DELETE /account/id/{id}`: 根据账号 ID 停用或永久删除账号。
|
||||||
|
- **导入/导出账号**:
|
||||||
|
- `POST /accounts/import`: 从上传的 JSON 文件批量导入或更新账号信息。
|
||||||
|
- `GET /accounts/export`: 将所有账号信息导出为 JSON 文件。
|
||||||
|
|
||||||
|
### 2.2. 自动注册 (Automated Registration)
|
||||||
|
应用包含一个后台服务,用于自动注册新的 Cursor 账号:
|
||||||
|
- **后台任务**: `run_registration` 异步函数是注册逻辑的核心,它会循环尝试注册新账号。
|
||||||
|
- **启动与停止**:
|
||||||
|
- `GET /registration/start`: 手动启动后台注册任务。如果任务已运行或账号已满,会返回相应状态。
|
||||||
|
- `GET /registration/stop`: 手动停止当前运行的注册任务。
|
||||||
|
- **状态查询**: `GET /registration/status` 提供注册任务的当前状态,包括是否正在运行、上次运行时间、下次运行时间、成功/失败次数统计以及当前账号数量与最大限制。
|
||||||
|
- **监控模式**: 当数据库中激活的账号数量达到 `MAX_ACCOUNTS` 设定的上限时,注册任务会暂停实际注册操作,进入监控模式,并定期检查账号数量,一旦账号数量低于上限则自动恢复注册。
|
||||||
|
|
||||||
|
### 2.3. 账号使用情况 (Account Usage)
|
||||||
|
跟踪和管理账号的实际使用量:
|
||||||
|
- **查询所有账号使用情况**: `GET /usage` 并发查询并返回所有已存账号的剩余额度和试用天数,结果会进行缓存以提高性能。
|
||||||
|
- **查询单个账号使用情况**: `GET /account/{email}/usage` 查询特定邮箱账号的余额和剩余天数,并据此更新数据库中的记录。
|
||||||
|
- **更新所有账号使用情况**: `POST /update_all_usage` 批量更新所有账号的余量信息到数据库。
|
||||||
|
- **记录账号使用行为**: `GET /account/{id}/usage-records` 获取指定账号的历史使用记录(如IP地址、User-Agent、使用时间)。
|
||||||
|
|
||||||
|
### 2.4. 系统配置管理 (System Configuration)
|
||||||
|
允许动态管理应用的配置参数(通常存储在 `.env` 文件中):
|
||||||
|
- **读取配置**: `GET /config` 返回当前系统配置信息,如浏览器设置、邮箱服务详情、代理设置等。
|
||||||
|
- **更新配置**: `POST /config` 允许通过 API 请求更新 `.env` 文件中的配置项,并重新加载。
|
||||||
|
|
||||||
|
### 2.5. Cursor 客户端集成 (Cursor Client Integration)
|
||||||
|
提供与本地安装的 Cursor IDE 进行交互的功能:
|
||||||
|
- **使用账号 Token 更新认证**: `POST /account/use-token/{id}` 使用指定账号的 Access Token 来更新本地 Cursor IDE 的认证配置。可以选择是否同时重置机器ID。
|
||||||
|
- **重置机器 ID**: `GET /reset-machine` 调用脚本重置本地 Cursor IDE 的机器识别码。
|
||||||
|
|
||||||
|
### 2.6. Web UI 与 API 文档 (Web UI & API Documentation)
|
||||||
|
- **静态首页服务**: `GET /` 提供一个简单的 `index.html` 页面作为应用的 Web UI 入口。
|
||||||
|
- **Swagger UI**: `GET /docs` 提供交互式的 API 文档界面。
|
||||||
|
- **ReDoc**: `GET /redoc` 提供另一种风格的 API 文档。
|
||||||
|
|
||||||
|
## 3. 架构与关键组件 (Architecture & Key Components)
|
||||||
|
|
||||||
|
### 3.1. FastAPI 应用实例 (`app`)
|
||||||
|
- **初始化**: `app = FastAPI(...)` 创建主应用实例,配置了标题、描述、版本、文档URL以及 `lifespan` 管理器。
|
||||||
|
- **中间件**: `app.add_middleware(CORSMiddleware, ...)` 添加了 CORS 中间件以支持跨域请求。
|
||||||
|
- **生命周期事件**: `@asynccontextmanager async def lifespan(app: FastAPI): ...` 定义了应用的生命周期事件。在应用启动时,会调用 `init_db()` 初始化数据库。
|
||||||
|
|
||||||
|
### 3.2. 数据库交互 (Database Interaction)
|
||||||
|
- **`database.py` (外部模块)**: 此模块(未直接提供,但从导入推断)负责数据库的连接管理 (`get_session`),定义数据模型 (`AccountModel`, `AccountUsageRecordModel`),以及数据库初始化逻辑 (`init_db`)。
|
||||||
|
- **SQLAlchemy ORM**: 应用通过 SQLAlchemy Core 的查询构造语法(如 `select()`, `delete()`, `func.count()`)和异步会话 (`async with get_session() as session:`) 与数据库进行交互。
|
||||||
|
|
||||||
|
### 3.3. 路由与端点 (Routing & Endpoints)
|
||||||
|
- API 端点通过 FastAPI 的装饰器(如 `@app.get`, `@app.post`, `@app.put`, `@app.delete`)绑定到相应的异步处理函数。
|
||||||
|
- 端点通过 `tags` 参数在 API 文档中进行分组,主要包括: `General`, `Accounts`, `Registration`, `Config`, `System`。
|
||||||
|
|
||||||
|
### 3.4. 后台注册任务 (`run_registration`)
|
||||||
|
- 这是一个核心的异步函数,通过 `asyncio.create_task(run_registration())` 在 `/registration/start` 端点被调用时启动。
|
||||||
|
- 任务在一个无限循环中运行(受 `registration_status["is_running"]` 控制),依次执行:检查当前激活账号数是否小于 `MAX_ACCOUNTS`,调用 `register_account`(一个外部同步函数,通过 `run_in_executor` 异步执行)进行实际注册,处理成功/失败结果,更新统计信息,然后根据 `REGISTRATION_INTERVAL` 等待下一轮。
|
||||||
|
|
||||||
|
### 3.5. 全局状态管理 (Global State Management)
|
||||||
|
- **`registration_status` (字典)**: 用于实时跟踪和存储自动注册任务的各种状态信息,如是否正在运行 (`is_running`)、上次运行时间 (`last_run`)、上次状态 (`last_status`)、下次计划运行时间 (`next_run`),以及运行次数、成功和失败的统计。
|
||||||
|
- **`background_tasks` (字典)**: 主要用于存储 `run_registration` 任务的 `asyncio.Task` 实例,键为 `"registration_task"`。这允许其他部分代码检查任务是否正在运行或取消任务。
|
||||||
|
|
||||||
|
### 3.6. 配置管理 (Configuration Management)
|
||||||
|
- **`config.py` (外部模块)**: 定义了应用的一些关键配置常量,如 `MAX_ACCOUNTS`(最大账号数)、`REGISTRATION_INTERVAL`(注册间隔时间)、`API_HOST`、`API_PORT` 等。
|
||||||
|
- **`.env` 文件**: 应用启动时及配置更新后,会通过 `python-dotenv` 库的 `load_dotenv()` 函数从项目根目录下的 `.env` 文件加载环境变量。这些变量通常包含敏感信息或环境特定配置。
|
||||||
|
|
||||||
|
### 3.7. 日志系统 (Logging)
|
||||||
|
- **`logger.py` (外部模块)**: 应用从该模块导入 `info` 和 `error` 函数,用于在控制台或日志文件(取决于 `logger.py` 的配置)中记录不同级别的事件信息和错误详情,包括堆栈跟踪。
|
||||||
|
|
||||||
|
### 3.8. 辅助函数与类 (Utility Functions & Classes)
|
||||||
|
- **数据库查询辅助函数**: `get_active_account_count()` 和 `get_account_count()` 用于高效查询数据库中符合特定条件的账号数量。
|
||||||
|
- **`Cursor` 类 (from `tokenManager.cursor`)**: 封装了与 Cursor 服务进行交互的逻辑,例如 `get_remaining_balance()` 和 `get_trial_remaining_days()` 方法用于查询账号的额度和试用期。
|
||||||
|
- **`CursorAuthManager` 类 (from `cursor_auth_manager`)**: 负责更新本地 Cursor IDE 的认证文件。
|
||||||
|
- **`CursorShadowPatcher` 类 (from `cursor_shadow_patcher`)**: 负责重置本地 Cursor IDE 的机器 ID。
|
||||||
|
|
||||||
|
## 4. 数据模型 (Data Models - Pydantic)
|
||||||
|
|
||||||
|
Pydantic 模型在 FastAPI 中扮演着数据校验、序列化和文档生成的关键角色:
|
||||||
|
- **`Account`**: 定义了一个 Cursor 账号的主要属性,如 `email`, `password`, `token`, `user`, `usage_limit`, `created_at`, `status`, `id`。它用于创建账号 (`POST /account`) 的请求体,并在多个响应中作为数据结构。`Config.from_attributes = True` 允许从 ORM 对象创建 Pydantic 模型实例。
|
||||||
|
- **`AccountResponse`**: 一个通用的响应模型,封装了操作是否成功 (`success`)、返回的数据 (`data`,通常是 `Account` 模型或其列表)以及可选的消息 (`message`)。
|
||||||
|
- **`StatusUpdate`**: 用于 `PUT /account/id/{id}/status` 端点的请求体,包含一个新的 `status` 字符串。
|
||||||
|
- **`ConfigModel`**: 定义了 `POST /config` 端点接受的配置项及其类型,例如 `BROWSER_HEADLESS`, `MAX_ACCOUNTS`, `EMAIL_DOMAINS`, 代理相关设置等。
|
||||||
|
|
||||||
|
这些模型确保了 API 接收的数据符合预期格式,并且响应数据也以结构化的方式返回。
|
||||||
|
|
||||||
|
## 5. 错误处理 (Error Handling)
|
||||||
|
|
||||||
|
应用实现了全局异常处理机制:
|
||||||
|
- **`@app.exception_handler(HTTPException)`**: 捕获 FastAPI 自身或其他地方抛出的 `HTTPException`。它会记录错误并通过 `JSONResponse` 返回一个包含 `success: False` 和错误详情(`exc.detail`)的 JSON 响应,状态码与原始异常一致。
|
||||||
|
- **`@app.exception_handler(Exception)`**: 捕获所有其他未被处理的 Python 异常。这确保了即使发生意外错误,应用也不会崩溃,而是返回一个标准的 500 内部服务器错误响应,其中包含 `success: False` 和通用错误消息。在调试模式下(`app.debug` 为 True),响应中可能还会包含异常的详细信息。
|
||||||
|
|
||||||
|
这种集中的错误处理方式有助于提供一致的 API 错误响应格式。
|
||||||
|
|
||||||
|
## 6. 依赖与环境 (Dependencies & Environment)
|
||||||
|
|
||||||
|
### 主要依赖:
|
||||||
|
- **FastAPI**: Web 框架。
|
||||||
|
- **Uvicorn**: ASGI 服务器。
|
||||||
|
- **SQLAlchemy**: ORM,用于数据库异步操作 (需要配合相应的数据库驱动,如 `asyncpg` for PostgreSQL, `aiomysql` for MySQL)。
|
||||||
|
- **Pydantic**: 数据验证和模型定义。
|
||||||
|
- **python-dotenv**: 加载 `.env` 文件中的环境变量。
|
||||||
|
- **requests**: (推断) `tokenManager.cursor.Cursor` 类可能使用此库进行 HTTP 请求以查询 Cursor API。
|
||||||
|
- **concurrent.futures**: 用于在 `check_usage` 接口中并发执行账号状态检查。
|
||||||
|
|
||||||
|
### 运行环境:
|
||||||
|
- **Python**: 版本 3.7+ (基于 FastAPI 和 `async/await` 语法)。
|
||||||
|
- **`.env` 文件**: 必须在项目根目录下存在,并包含所有必要的配置变量 (如数据库连接信息、API 密钥、邮箱配置、代理配置等)。`get_config` 和 `update_config` 端点直接与此文件交互。
|
||||||
|
- **数据库服务**: 需要一个 SQLAlchemy 支持的数据库正在运行,并在 `.env` 中配置好连接参数。
|
||||||
|
- **网络访问**: 自动注册和账号使用情况查询功能需要网络访问权限,以连接到 Cursor 服务和可能的邮件服务。
|
||||||
|
|
||||||
|
## 7. 部署与运行 (Deployment & Execution)
|
||||||
|
|
||||||
|
该 FastAPI 应用通过 Uvicorn ASGI 服务器运行。文件末尾的 `if __name__ == "__main__":` 块提供了一个直接运行应用的入口点:
|
||||||
|
```python
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(
|
||||||
|
"api:app",
|
||||||
|
host=API_HOST,
|
||||||
|
port=API_PORT,
|
||||||
|
reload=API_DEBUG,
|
||||||
|
access_log=True,
|
||||||
|
log_level="error",
|
||||||
|
workers=API_WORKERS,
|
||||||
|
loop="asyncio",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
- `"api:app"`: 指示 Uvicorn 加载 `api.py` 文件中的 `app` FastAPI 实例。
|
||||||
|
- `host=API_HOST`, `port=API_PORT`: 从 `config.py` (或 `.env`) 加载监听的主机和端口。
|
||||||
|
- `reload=API_DEBUG`: 如果 `API_DEBUG` 为 True,则启用热重载功能,代码更改时服务器会自动重启。
|
||||||
|
- `access_log=True`: 启用访问日志。
|
||||||
|
- `log_level="error"`: 设置 Uvicorn 的日志级别。
|
||||||
|
- `workers=API_WORKERS`: 配置工作进程数量。对于生产环境,可以根据服务器核心数调整。
|
||||||
|
- `loop="asyncio"`: 明确指定使用 asyncio 事件循环。
|
||||||
|
|
||||||
|
## 8. 注意事项 (Considerations)
|
||||||
|
|
||||||
|
- **并发安全**: 虽然 FastAPI 和 asyncio 处理并发请求,但对全局变量 `registration_status` 和 `background_tasks` 的直接修改可能需要审慎处理,以避免潜在的竞争条件,尤其是在多 worker 配置下(尽管 Python 的 GIL 使得纯 Python 代码的并发问题不如其他语言复杂)。
|
||||||
|
- **安全性**:
|
||||||
|
- **敏感信息**: `.env` 文件中存储了大量敏感配置。需要确保此文件的权限得到妥善管理。
|
||||||
|
- **密码存储**: 如果 `AccountModel` 直接存储明文密码,这是严重的安全风险。密码应始终进行哈希处理 (如使用 bcrypt 或 Argon2) 后再存储。
|
||||||
|
- **API 认证/授权**: 当前 API 似乎没有实现任何认证或授权机制来保护其端点。在生产环境中,应考虑添加如 OAuth2 或 API 密钥等机制来限制对敏感操作的访问。
|
||||||
|
- **配置热重载**: `/restart` 接口通过修改 `.env` 文件中的一个时间戳变量来尝试触发 Uvicorn 的热重载。这种方法的有效性依赖于 Uvicorn 的 `--reload` 选项及其文件监控机制,可能并非在所有情况下都可靠或即时。
|
||||||
|
- **外部依赖**: 自动注册功能依赖于外部的 `register_account` 函数和 `tokenManager.cursor` 模块。这些外部组件的稳定性和行为直接影响本应用的功能。
|
||||||
|
- **错误处理细节**: 虽然有全局异常处理,但某些函数内部的 `try-except` 块可能会捕获并记录错误,然后继续执行或返回特定响应,这需要仔细审查以确保所有错误路径都得到妥善处理。
|
||||||
|
- **数据库迁移**: 随着 `AccountModel` 或 `AccountUsageRecordModel` 的演变,需要一个数据库迁移策略 (如使用 Alembic) 来管理数据库模式的变更。
|
137
browser_utils.py
Normal file
137
browser_utils.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
from DrissionPage import ChromiumOptions, Chromium
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from logger import info, warning, error
|
||||||
|
from config import (
|
||||||
|
BROWSER_USER_AGENT,
|
||||||
|
BROWSER_PATH,
|
||||||
|
BROWSER_HEADLESS,
|
||||||
|
DYNAMIC_USERAGENT,
|
||||||
|
USE_PROXY,
|
||||||
|
PROXY_TYPE,
|
||||||
|
PROXY_HOST,
|
||||||
|
PROXY_PORT,
|
||||||
|
PROXY_USERNAME,
|
||||||
|
PROXY_PASSWORD,
|
||||||
|
PROXY_TIMEOUT
|
||||||
|
)
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
from fake_useragent import UserAgent
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_user_agent():
|
||||||
|
ua = UserAgent()
|
||||||
|
return ua.random
|
||||||
|
|
||||||
|
class BrowserManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.browser = None
|
||||||
|
|
||||||
|
def init_browser(self):
|
||||||
|
try:
|
||||||
|
info("正在初始化浏览器...")
|
||||||
|
co = ChromiumOptions()
|
||||||
|
|
||||||
|
# 如果配置了特定的浏览器路径,则使用
|
||||||
|
if BROWSER_PATH and os.path.exists(BROWSER_PATH):
|
||||||
|
co.set_browser_path(BROWSER_PATH)
|
||||||
|
info(f"使用自定义浏览器路径: {BROWSER_PATH}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
extension_path = self._get_extension_path()
|
||||||
|
co.add_extension(extension_path)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
error(f"加载插件失败,警告: {e}")
|
||||||
|
|
||||||
|
# 设置User-Agent
|
||||||
|
if DYNAMIC_USERAGENT:
|
||||||
|
# 随机选择一个User-Agent
|
||||||
|
user_agent = get_random_user_agent()
|
||||||
|
info(f"使用动态User-Agent: {user_agent}")
|
||||||
|
co.set_user_agent(user_agent)
|
||||||
|
elif BROWSER_USER_AGENT:
|
||||||
|
info(f"使用固定User-Agent: {BROWSER_USER_AGENT}")
|
||||||
|
co.set_user_agent(BROWSER_USER_AGENT)
|
||||||
|
else:
|
||||||
|
info("不配置User-Agent")
|
||||||
|
|
||||||
|
co.set_pref("credentials_enable_service", False)
|
||||||
|
co.set_argument("--hide-crash-restore-bubble")
|
||||||
|
# 禁用自动化特征(关键参数)
|
||||||
|
co.set_argument("--disable-blink-features=AutomationControlled")
|
||||||
|
co.set_argument("--disable-features=AutomationControlled")
|
||||||
|
co.set_argument("--disable-automation-extension")
|
||||||
|
|
||||||
|
# 随机化指纹参数
|
||||||
|
co.set_pref("webgl.vendor", "NVIDIA Corporation")
|
||||||
|
co.set_pref(
|
||||||
|
"webgl.renderer",
|
||||||
|
"ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0)",
|
||||||
|
)
|
||||||
|
co.set_pref("navigator.plugins.length", 5)
|
||||||
|
co.set_pref("navigator.hardwareConcurrency", 8)
|
||||||
|
|
||||||
|
# 覆盖自动化特征(关键)
|
||||||
|
co.set_pref("dom.webdriver.enabled", False)
|
||||||
|
co.set_pref("useAutomationExtension", False)
|
||||||
|
|
||||||
|
# 设置时区参数
|
||||||
|
co.set_argument("--timezone=Asia/Shanghai")
|
||||||
|
co.set_pref("timezone.override", "Asia/Shanghai")
|
||||||
|
|
||||||
|
# 设置更真实的屏幕参数
|
||||||
|
co.set_pref("screen.width", 1920)
|
||||||
|
co.set_pref("screen.height", 1080)
|
||||||
|
co.set_pref("screen.pixelDepth", 24)
|
||||||
|
co.auto_port()
|
||||||
|
|
||||||
|
# 生产环境使用无头模式
|
||||||
|
co.headless(BROWSER_HEADLESS)
|
||||||
|
|
||||||
|
# Mac 系统特殊处理
|
||||||
|
if sys.platform == "darwin" or sys.platform == "linux":
|
||||||
|
co.set_argument("--no-sandbox")
|
||||||
|
co.set_argument("--disable-gpu")
|
||||||
|
|
||||||
|
# 添加代理设置
|
||||||
|
if USE_PROXY and PROXY_HOST and PROXY_PORT:
|
||||||
|
proxy_string = f"{PROXY_TYPE}://"
|
||||||
|
|
||||||
|
# 如果有认证信息
|
||||||
|
if PROXY_USERNAME and PROXY_PASSWORD:
|
||||||
|
proxy_string += f"{PROXY_USERNAME}:{PROXY_PASSWORD}@"
|
||||||
|
|
||||||
|
proxy_string += f"{PROXY_HOST}:{PROXY_PORT}"
|
||||||
|
|
||||||
|
info(f"使用代理: {PROXY_TYPE} {proxy_string}")
|
||||||
|
co.set_argument(f'--proxy-server={proxy_string}')
|
||||||
|
|
||||||
|
self.browser = Chromium(co)
|
||||||
|
info("浏览器初始化成功")
|
||||||
|
except Exception as e:
|
||||||
|
error(f"浏览器初始化失败: {str(e)}")
|
||||||
|
return self.browser
|
||||||
|
|
||||||
|
def _get_extension_path(self):
|
||||||
|
"""获取插件路径"""
|
||||||
|
root_dir = os.getcwd()
|
||||||
|
extension_path = os.path.join(root_dir, "turnstilePatch")
|
||||||
|
|
||||||
|
if hasattr(sys, "_MEIPASS"):
|
||||||
|
extension_path = os.path.join(sys._MEIPASS, "turnstilePatch")
|
||||||
|
|
||||||
|
if not os.path.exists(extension_path):
|
||||||
|
raise FileNotFoundError(f"插件不存在: {extension_path}")
|
||||||
|
info(f"插件路径: {extension_path}")
|
||||||
|
return extension_path
|
||||||
|
|
||||||
|
def quit(self):
|
||||||
|
info("正在关闭浏览器...")
|
||||||
|
try:
|
||||||
|
if self.browser:
|
||||||
|
self.browser.quit()
|
||||||
|
info("浏览器已关闭")
|
||||||
|
except Exception as e:
|
||||||
|
error(f"关闭浏览器出错: {str(e)}")
|
113
config.py
Normal file
113
config.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# 加载.env文件中的环境变量
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# ===== 日志配置 =====
|
||||||
|
# 日志级别:DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||||||
|
# 日志格式:时间戳 - 日志级别 - 消息内容
|
||||||
|
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
# 日志日期格式:年-月-日 时:分:秒
|
||||||
|
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
# ===== API服务配置 =====
|
||||||
|
# API服务监听主机地址
|
||||||
|
API_HOST = os.getenv("API_HOST", "127.0.0.1")
|
||||||
|
# API服务端口号
|
||||||
|
API_PORT = int(os.getenv("API_PORT", 8000))
|
||||||
|
# 是否启用调试模式
|
||||||
|
API_DEBUG = os.getenv("API_DEBUG", "false").lower() == "true"
|
||||||
|
# API服务工作进程数量(Windows下建议使用1)
|
||||||
|
API_WORKERS = int(os.getenv("API_WORKERS", 1))
|
||||||
|
|
||||||
|
# ===== 账号管理配置 =====
|
||||||
|
# 系统最大已激活的账号数量
|
||||||
|
MAX_ACCOUNTS = int(os.getenv("MAX_ACCOUNTS", 10))
|
||||||
|
# 每次注册间隔时间(秒)
|
||||||
|
REGISTRATION_INTERVAL = int(os.getenv("REGISTRATION_INTERVAL", 60))
|
||||||
|
# 注册失败时的最大重试次数
|
||||||
|
REGISTRATION_MAX_RETRIES = int(os.getenv("REGISTRATION_MAX_RETRIES", 3))
|
||||||
|
# 注册重试间隔时间(秒)
|
||||||
|
REGISTRATION_RETRY_INTERVAL = int(os.getenv("REGISTRATION_RETRY_INTERVAL", 5))
|
||||||
|
|
||||||
|
# ===== 浏览器配置 =====
|
||||||
|
# 是否以无头模式运行浏览器(不显示界面)
|
||||||
|
BROWSER_HEADLESS = os.getenv("BROWSER_HEADLESS", "true").lower() == "true"
|
||||||
|
# 浏览器可执行文件路径(为空则使用默认路径)
|
||||||
|
BROWSER_PATH = os.getenv("BROWSER_PATH", None)
|
||||||
|
# 浏览器下载文件保存路径
|
||||||
|
BROWSER_DOWNLOAD_PATH = os.getenv("BROWSER_DOWNLOAD_PATH", None)
|
||||||
|
# 是否使用动态ua池
|
||||||
|
DYNAMIC_USERAGENT = os.getenv("DYNAMIC_USERAGENT", "false").lower() == "true"
|
||||||
|
# 浏览器User-Agent
|
||||||
|
BROWSER_USER_AGENT = os.getenv("BROWSER_USER_AGENT", None)
|
||||||
|
|
||||||
|
# ===== Cursor URL配置 =====
|
||||||
|
# Cursor登录页面URL
|
||||||
|
LOGIN_URL = "https://authenticator.cursor.sh"
|
||||||
|
# Cursor注册页面URL
|
||||||
|
SIGN_UP_URL = "https://authenticator.cursor.sh/sign-up"
|
||||||
|
# Cursor设置页面URL
|
||||||
|
SETTINGS_URL = "https://www.cursor.com/settings"
|
||||||
|
|
||||||
|
# ===== 邮箱配置 =====
|
||||||
|
# 邮箱验证码获取方式
|
||||||
|
EMAIL_CODE_TYPE = os.getenv("EMAIL_CODE_TYPE", "AUTO")
|
||||||
|
# 邮箱类型
|
||||||
|
EMAIL_TYPE = os.getenv("EMAIL_TYPE", "tempemail")
|
||||||
|
# 临时邮箱用户名
|
||||||
|
EMAIL_USERNAME = os.getenv("EMAIL_USERNAME", "xxx")
|
||||||
|
# 临时邮箱域名
|
||||||
|
EMAIL_DOMAIN = os.getenv("EMAIL_DOMAIN", "mailto.plus")
|
||||||
|
# 临时邮箱PIN码(如果需要)
|
||||||
|
EMAIL_PIN = os.getenv("EMAIL_PIN", "")
|
||||||
|
# 可用于注册的邮箱域名列表(逗号分隔)
|
||||||
|
EMAIL_DOMAINS = [
|
||||||
|
domain.strip() for domain in os.getenv("EMAIL_DOMAINS", "xxx.xx").split(",")
|
||||||
|
]
|
||||||
|
# ZMail API地址
|
||||||
|
EMAIL_API = os.getenv("EMAIL_API", "")
|
||||||
|
# 是否启用邮箱API代理
|
||||||
|
EMAIL_PROXY_ENABLED = os.getenv("EMAIL_PROXY_ENABLED", "false").lower() == "true"
|
||||||
|
# 邮箱API代理地址
|
||||||
|
EMAIL_PROXY_ADDRESS = os.getenv("EMAIL_PROXY_ADDRESS", "")
|
||||||
|
# 邮件验证码获取最大重试次数
|
||||||
|
EMAIL_VERIFICATION_RETRIES = int(os.getenv("EMAIL_VERIFICATION_RETRIES", 5))
|
||||||
|
# 邮件验证码获取重试间隔(秒)
|
||||||
|
EMAIL_VERIFICATION_WAIT = int(os.getenv("EMAIL_VERIFICATION_WAIT", 5))
|
||||||
|
|
||||||
|
# ===== 数据库配置 =====
|
||||||
|
# 数据库文件名
|
||||||
|
DB_NAME = "accounts.db"
|
||||||
|
# 根据操作系统生成适当的数据库文件路径
|
||||||
|
if os.name == "nt": # Windows
|
||||||
|
DB_PATH = os.path.join(os.getcwd(), DB_NAME)
|
||||||
|
DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}"
|
||||||
|
else: # Linux/Unix
|
||||||
|
DB_PATH = os.path.join("/app", DB_NAME)
|
||||||
|
DATABASE_URL = f"sqlite+aiosqlite:{DB_PATH}"
|
||||||
|
|
||||||
|
# 允许通过环境变量覆盖默认的数据库URL
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", DATABASE_URL)
|
||||||
|
|
||||||
|
# ===== Cursor main.js 配置 =====
|
||||||
|
# Cursor 主文件路径
|
||||||
|
CURSOR_PATH = os.getenv("CURSOR_PATH", None)
|
||||||
|
|
||||||
|
# ===== 代理配置 =====
|
||||||
|
# 是否启用代理
|
||||||
|
USE_PROXY = os.getenv("USE_PROXY", "False").lower() == "true"
|
||||||
|
# 代理类型
|
||||||
|
PROXY_TYPE = os.getenv("PROXY_TYPE", "http")
|
||||||
|
# 代理服务器地址
|
||||||
|
PROXY_HOST = os.getenv("PROXY_HOST", "")
|
||||||
|
# 代理服务器端口
|
||||||
|
PROXY_PORT = os.getenv("PROXY_PORT", "")
|
||||||
|
# 代理服务器用户名
|
||||||
|
PROXY_USERNAME = os.getenv("PROXY_USERNAME", "")
|
||||||
|
# 代理服务器密码
|
||||||
|
PROXY_PASSWORD = os.getenv("PROXY_PASSWORD", "")
|
||||||
|
# 代理服务器超时时间
|
||||||
|
PROXY_TIMEOUT = int(os.getenv("PROXY_TIMEOUT", "10"))
|
87
cursor_auth_manager.py
Normal file
87
cursor_auth_manager.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class CursorAuthManager:
|
||||||
|
"""Cursor认证信息管理器"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 判断操作系统
|
||||||
|
if sys.platform == "win32": # Windows
|
||||||
|
appdata = os.getenv("APPDATA")
|
||||||
|
if appdata is None:
|
||||||
|
raise EnvironmentError("APPDATA 环境变量未设置")
|
||||||
|
self.db_path = os.path.join(
|
||||||
|
appdata, "Cursor", "User", "globalStorage", "state.vscdb"
|
||||||
|
)
|
||||||
|
elif sys.platform == "darwin": # macOS
|
||||||
|
self.db_path = os.path.abspath(
|
||||||
|
os.path.expanduser(
|
||||||
|
"~/Library/Application Support/Cursor/User/globalStorage/state.vscdb"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif sys.platform == "linux": # Linux 和其他类Unix系统
|
||||||
|
self.db_path = os.path.abspath(
|
||||||
|
os.path.expanduser("~/.config/Cursor/User/globalStorage/state.vscdb")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f"不支持的操作系统: {sys.platform}")
|
||||||
|
|
||||||
|
def update_auth(self, email=None, access_token=None, refresh_token=None):
|
||||||
|
"""
|
||||||
|
更新Cursor的认证信息
|
||||||
|
:param email: 新的邮箱地址
|
||||||
|
:param access_token: 新的访问令牌
|
||||||
|
:param refresh_token: 新的刷新令牌
|
||||||
|
:return: bool 是否成功更新
|
||||||
|
"""
|
||||||
|
updates = []
|
||||||
|
# 登录状态
|
||||||
|
updates.append(("cursorAuth/cachedSignUpType", "Auth_0"))
|
||||||
|
|
||||||
|
if email is not None:
|
||||||
|
updates.append(("cursorAuth/cachedEmail", email))
|
||||||
|
if access_token is not None:
|
||||||
|
updates.append(("cursorAuth/accessToken", access_token))
|
||||||
|
if refresh_token is not None:
|
||||||
|
updates.append(("cursorAuth/refreshToken", refresh_token))
|
||||||
|
|
||||||
|
if not updates:
|
||||||
|
print("没有提供任何要更新的值")
|
||||||
|
return False
|
||||||
|
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
for key, value in updates:
|
||||||
|
# 如果没有更新任何行,说明key不存在,执行插入
|
||||||
|
# 检查 accessToken 是否存在
|
||||||
|
check_query = "SELECT COUNT(*) FROM itemTable WHERE key = ?"
|
||||||
|
cursor.execute(check_query, (key,))
|
||||||
|
if cursor.fetchone()[0] == 0:
|
||||||
|
insert_query = "INSERT INTO itemTable (key, value) VALUES (?, ?)"
|
||||||
|
cursor.execute(insert_query, (key, value))
|
||||||
|
else:
|
||||||
|
update_query = "UPDATE itemTable SET value = ? WHERE key = ?"
|
||||||
|
cursor.execute(update_query, (value, key))
|
||||||
|
|
||||||
|
if cursor.rowcount > 0:
|
||||||
|
print(f"成功更新 {key.split('/')[-1]} : {value}")
|
||||||
|
else:
|
||||||
|
print(f"未找到 {key.split('/')[-1]} 或值未变化")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print("数据库错误:", str(e))
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print("发生错误:", str(e))
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
578
cursor_pro_keep_alive.py
Normal file
578
cursor_pro_keep_alive.py
Normal file
@ -0,0 +1,578 @@
|
|||||||
|
import sys
|
||||||
|
import psutil
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
from logger import info, warning, error
|
||||||
|
import traceback
|
||||||
|
from config import (
|
||||||
|
LOGIN_URL,
|
||||||
|
SIGN_UP_URL,
|
||||||
|
SETTINGS_URL,
|
||||||
|
EMAIL_DOMAINS,
|
||||||
|
REGISTRATION_MAX_RETRIES,
|
||||||
|
EMAIL_TYPE,
|
||||||
|
EMAIL_CODE_TYPE
|
||||||
|
)
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
import uuid
|
||||||
|
import requests
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
if sys.stdout.encoding != "utf-8":
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
if sys.stderr.encoding != "utf-8":
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
from browser_utils import BrowserManager
|
||||||
|
from get_email_code import EmailVerificationHandler
|
||||||
|
|
||||||
|
from datetime import datetime # 添加这行导入
|
||||||
|
|
||||||
|
TOTAL_USAGE = 0
|
||||||
|
|
||||||
|
|
||||||
|
def handle_turnstile(tab, max_retries: int = 5, retry_interval: tuple = (1, 2)) -> bool:
|
||||||
|
"""
|
||||||
|
处理Turnstile验证
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tab: 浏览器标签对象
|
||||||
|
max_retries: 最大重试次数
|
||||||
|
retry_interval: 重试间隔范围(最小值, 最大值)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 验证是否成功
|
||||||
|
"""
|
||||||
|
info("=============正在检测 Turnstile 验证=============")
|
||||||
|
|
||||||
|
# 初始化重试计数器
|
||||||
|
retry_count = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 在最大重试次数内循环尝试验证
|
||||||
|
while retry_count < max_retries:
|
||||||
|
# 增加重试计数
|
||||||
|
retry_count += 1
|
||||||
|
info(f"正在进行 Turnstile 第 {retry_count} 次验证中...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 检查页面状态,但不直接返回,先检查是否有Turnstile验证需要处理
|
||||||
|
page_ready = False
|
||||||
|
if tab.ele("@name=password"):
|
||||||
|
info("检测到密码输入页面,检查是否有验证需要处理...")
|
||||||
|
page_ready = True
|
||||||
|
elif tab.ele("@data-index=0"):
|
||||||
|
info("检测到验证码输入页面,检查是否有验证需要处理...")
|
||||||
|
page_ready = True
|
||||||
|
elif tab.ele("Account Settings"):
|
||||||
|
info("检测到账户设置页面,检查是否有验证需要处理...")
|
||||||
|
page_ready = True
|
||||||
|
|
||||||
|
# 初始化元素变量
|
||||||
|
element = None
|
||||||
|
try:
|
||||||
|
# 尝试通过层级结构定位到Turnstile验证框的容器元素
|
||||||
|
element = (
|
||||||
|
tab.ele(".main-content") # 找到 .main-content 元素
|
||||||
|
.ele("tag:div") # 找到第一个子 div
|
||||||
|
.ele("tag:div") # 找到第二个子 div
|
||||||
|
.ele("tag:div") # 找到第三个子 div
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# 如果无法通过第一种方式找到元素,忽略异常继续执行
|
||||||
|
pass
|
||||||
|
|
||||||
|
challenge_check = None
|
||||||
|
if element:
|
||||||
|
# 如果找到了容器元素,则在其中定位验证框的输入元素
|
||||||
|
try:
|
||||||
|
challenge_check = (
|
||||||
|
element
|
||||||
|
.shadow_root.ele("tag:iframe") # 找到shadow DOM中的iframe
|
||||||
|
.ele("tag:body") # 找到iframe中的body
|
||||||
|
.sr("tag:input") # 找到body中的input元素
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# 如果没有找到容器元素,则尝试另一种方式定位验证框
|
||||||
|
try:
|
||||||
|
challenge_check = (
|
||||||
|
tab.ele("@id=cf-turnstile", timeout=2) # 通过id直接找到turnstile元素
|
||||||
|
.child() # 获取其子元素
|
||||||
|
.shadow_root.ele("tag:iframe") # 找到shadow DOM中的iframe
|
||||||
|
.ele("tag:body") # 找到iframe中的body
|
||||||
|
.sr("tag:input") # 找到body中的input元素
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if challenge_check:
|
||||||
|
# 如果找到了验证输入元素,记录日志
|
||||||
|
info("检测到 Turnstile 验证,正在处理...")
|
||||||
|
# 点击前随机延迟,模拟人工操作
|
||||||
|
time.sleep(random.uniform(1, 3))
|
||||||
|
# 点击验证元素触发验证
|
||||||
|
challenge_check.click()
|
||||||
|
# 等待验证处理
|
||||||
|
time.sleep(2)
|
||||||
|
info("Turnstile 验证通过")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
info("未检测到 Turnstile 验证")
|
||||||
|
|
||||||
|
# 如果页面已准备好且没有验证需要处理,则可以返回
|
||||||
|
if page_ready:
|
||||||
|
info("页面已准备好,没有检测到需要处理的验证")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 记录当前尝试失败的详细信息
|
||||||
|
info(f"当前验证尝试失败: {str(e)}")
|
||||||
|
|
||||||
|
# 在下一次尝试前随机延迟
|
||||||
|
time.sleep(random.uniform(*retry_interval))
|
||||||
|
|
||||||
|
# 超过最大重试次数,验证失败
|
||||||
|
error(f"Turnstile 验证次数超过最大限制 {max_retries},退出")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 捕获整个验证过程中的异常
|
||||||
|
error_msg = f"Turnstile 验证失败: {str(e)}"
|
||||||
|
error(error_msg)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_cursor_session_token(tab, max_attempts: int = 3, retry_interval: int = 2) -> Optional[Tuple[str, str]]:
|
||||||
|
"""
|
||||||
|
获取Cursor会话token
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tab: 浏览器标签页对象
|
||||||
|
max_attempts: 最大尝试次数
|
||||||
|
retry_interval: 重试间隔(秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[str, str] | None: 成功返回(userId, accessToken)元组,失败返回None
|
||||||
|
"""
|
||||||
|
info("开始获取会话令牌")
|
||||||
|
|
||||||
|
# 首先尝试使用UUID深度登录方式
|
||||||
|
info("尝试使用深度登录方式获取token")
|
||||||
|
|
||||||
|
def _generate_pkce_pair():
|
||||||
|
"""生成PKCE验证对"""
|
||||||
|
code_verifier = secrets.token_urlsafe(43)
|
||||||
|
code_challenge_digest = hashlib.sha256(code_verifier.encode('utf-8')).digest()
|
||||||
|
code_challenge = base64.urlsafe_b64encode(code_challenge_digest).decode('utf-8').rstrip('=')
|
||||||
|
return code_verifier, code_challenge
|
||||||
|
|
||||||
|
attempts = 0
|
||||||
|
while attempts < max_attempts:
|
||||||
|
try:
|
||||||
|
verifier, challenge = _generate_pkce_pair()
|
||||||
|
id = uuid.uuid4()
|
||||||
|
client_login_url = f"https://www.cursor.com/cn/loginDeepControl?challenge={challenge}&uuid={id}&mode=login"
|
||||||
|
|
||||||
|
info(f"访问深度登录URL: {client_login_url}")
|
||||||
|
tab.get(client_login_url)
|
||||||
|
# save_screenshot(tab, f"deeplogin_attempt_{attempts}")
|
||||||
|
|
||||||
|
if tab.ele("xpath=//span[contains(text(), 'Yes, Log In')]", timeout=5):
|
||||||
|
info("点击确认登录按钮")
|
||||||
|
tab.ele("xpath=//span[contains(text(), 'Yes, Log In')]").click()
|
||||||
|
time.sleep(1.5)
|
||||||
|
|
||||||
|
auth_poll_url = f"https://api2.cursor.sh/auth/poll?uuid={id}&verifier={verifier}"
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/0.48.6 Chrome/132.0.6834.210 Electron/34.3.4 Safari/537.36",
|
||||||
|
"Accept": "*/*"
|
||||||
|
}
|
||||||
|
|
||||||
|
info(f"轮询认证状态: {auth_poll_url}")
|
||||||
|
response = requests.get(auth_poll_url, headers=headers, timeout=5)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
accessToken = data.get("accessToken", None)
|
||||||
|
authId = data.get("authId", "")
|
||||||
|
|
||||||
|
if accessToken:
|
||||||
|
userId = ""
|
||||||
|
if len(authId.split("|")) > 1:
|
||||||
|
userId = authId.split("|")[1]
|
||||||
|
|
||||||
|
info("成功获取账号token和userId")
|
||||||
|
return accessToken, userId
|
||||||
|
else:
|
||||||
|
error(f"API请求失败,状态码: {response.status_code}")
|
||||||
|
else:
|
||||||
|
warning("未找到登录确认按钮")
|
||||||
|
|
||||||
|
attempts += 1
|
||||||
|
if attempts < max_attempts:
|
||||||
|
wait_time = retry_interval * attempts # 逐步增加等待时间
|
||||||
|
warning(f"第 {attempts} 次尝试未获取到token,{wait_time}秒后重试...")
|
||||||
|
# save_screenshot(tab, f"token_attempt_{attempts}")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error(f"深度登录获取token失败: {str(e)}")
|
||||||
|
attempts += 1
|
||||||
|
# save_screenshot(tab, f"token_error_{attempts}")
|
||||||
|
if attempts < max_attempts:
|
||||||
|
wait_time = retry_interval * attempts
|
||||||
|
warning(f"将在 {wait_time} 秒后重试...")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_up_account(browser, tab, account_info):
|
||||||
|
info("=============开始注册账号=============")
|
||||||
|
info(
|
||||||
|
f"账号信息: 邮箱: {account_info['email']}, 密码: {account_info['password']}, 姓名: {account_info['first_name']} {account_info['last_name']}"
|
||||||
|
)
|
||||||
|
if EMAIL_TYPE == "zmail":
|
||||||
|
EmailVerificationHandler.create_zmail_email(account_info)
|
||||||
|
tab.get(SIGN_UP_URL)
|
||||||
|
|
||||||
|
tab.wait(2)
|
||||||
|
|
||||||
|
if tab.ele("@name=cf-turnstile-response"):
|
||||||
|
error("开屏就是检测啊,大佬你的IP或UA需要换一下了啊,有问题了...要等一下")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if tab.ele("@name=first_name"):
|
||||||
|
info("=============正在填写个人信息=============")
|
||||||
|
tab.actions.click("@name=first_name").input(account_info["first_name"])
|
||||||
|
info(f"已输入名字: {account_info['first_name']}")
|
||||||
|
time.sleep(random.uniform(1, 3))
|
||||||
|
|
||||||
|
tab.actions.click("@name=last_name").input(account_info["last_name"])
|
||||||
|
info(f"已输入姓氏: {account_info['last_name']}")
|
||||||
|
time.sleep(random.uniform(1, 3))
|
||||||
|
|
||||||
|
tab.actions.click("@name=email").input(account_info["email"])
|
||||||
|
info(f"已输入邮箱: {account_info['email']}")
|
||||||
|
time.sleep(random.uniform(1, 3))
|
||||||
|
|
||||||
|
info("=============提交个人信息=============")
|
||||||
|
tab.actions.click("@type=submit")
|
||||||
|
time.sleep(random.uniform(0.2, 1))
|
||||||
|
if (
|
||||||
|
tab.ele("verify the user is human. Please try again.")
|
||||||
|
or tab.ele("Can't verify the user is human. Please try again.")
|
||||||
|
or tab.ele("Can't verify the user is human. Please try again.")
|
||||||
|
):
|
||||||
|
info("检测到turnstile验证失败,(IP问题、UA问题、域名问题)...正在重试...")
|
||||||
|
return "EMAIL_USED"
|
||||||
|
except Exception as e:
|
||||||
|
info(f"填写个人信息失败: {str(e)}")
|
||||||
|
return "ERROR"
|
||||||
|
|
||||||
|
handle_turnstile(tab)
|
||||||
|
|
||||||
|
if tab.ele("verify the user is human. Please try again.") or tab.ele(
|
||||||
|
"Can't verify the user is human. Please try again."
|
||||||
|
):
|
||||||
|
info("检测到turnstile验证失败,正在重试...")
|
||||||
|
return "EMAIL_USED"
|
||||||
|
|
||||||
|
try:
|
||||||
|
if tab.ele("@name=password"):
|
||||||
|
info(f"设置密码:{account_info['password']}")
|
||||||
|
tab.ele("@name=password").input(account_info["password"])
|
||||||
|
time.sleep(random.uniform(1, 2))
|
||||||
|
|
||||||
|
info("提交密码...")
|
||||||
|
tab.ele("@type=submit").click()
|
||||||
|
info("密码设置成功,等待系统响应....")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
info(f"密码设置失败: {str(e)}")
|
||||||
|
return "ERROR"
|
||||||
|
|
||||||
|
info("处理最终验证...")
|
||||||
|
handle_turnstile(tab)
|
||||||
|
|
||||||
|
if tab.ele("This email is not available."):
|
||||||
|
info("邮箱已被使用")
|
||||||
|
return "EMAIL_USED"
|
||||||
|
|
||||||
|
if tab.ele("Sign up is restricted."):
|
||||||
|
info("注册限制")
|
||||||
|
return "SIGNUP_RESTRICTED"
|
||||||
|
|
||||||
|
# 创建邮件处理器
|
||||||
|
email_handler = EmailVerificationHandler()
|
||||||
|
i = 0
|
||||||
|
while i < 5:
|
||||||
|
try:
|
||||||
|
time.sleep(random.uniform(0.2, 1))
|
||||||
|
if tab.ele("Account Settings"):
|
||||||
|
info("注册成功,已进入账号设置页面")
|
||||||
|
break
|
||||||
|
if tab.ele("@data-index=0"):
|
||||||
|
info("等待输入验证码...")
|
||||||
|
# 切换到邮箱标签页
|
||||||
|
code = email_handler.get_verification_code(
|
||||||
|
source_email=account_info["email"]
|
||||||
|
)
|
||||||
|
if code is None:
|
||||||
|
info("未获取到验证码...系统异常,正在退出....")
|
||||||
|
return "EMAIL_GET_CODE_FAILED"
|
||||||
|
info(f"输入验证码: {code}")
|
||||||
|
i = 0
|
||||||
|
for digit in code:
|
||||||
|
tab.ele(f"@data-index={i}").input(digit)
|
||||||
|
time.sleep(random.uniform(0.3, 0.6))
|
||||||
|
i += 1
|
||||||
|
info("验证码输入完成")
|
||||||
|
time.sleep(random.uniform(3, 5))
|
||||||
|
|
||||||
|
# 在验证码输入完成后检测是否出现了Turnstile验证
|
||||||
|
info("检查是否出现了Turnstile验证...")
|
||||||
|
try:
|
||||||
|
turnstile_element = tab.ele("@id=cf-turnstile", timeout=3)
|
||||||
|
if turnstile_element:
|
||||||
|
info("检测到验证码输入后出现Turnstile验证,正在处理...")
|
||||||
|
handle_turnstile(tab)
|
||||||
|
except:
|
||||||
|
info("未检测到Turnstile验证,继续下一步")
|
||||||
|
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
info(f"验证码处理失败: {str(e)}")
|
||||||
|
return "ERROR"
|
||||||
|
|
||||||
|
info("完成最终验证...")
|
||||||
|
handle_turnstile(tab)
|
||||||
|
time.sleep(random.uniform(3, 5))
|
||||||
|
info("账号注册流程完成")
|
||||||
|
return "SUCCESS"
|
||||||
|
|
||||||
|
|
||||||
|
class EmailGenerator:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
# 将密码生成移到这里,避免类定义时执行随机密码生成
|
||||||
|
self.default_first_name = self.generate_random_name()
|
||||||
|
self.default_last_name = self.generate_random_name()
|
||||||
|
|
||||||
|
# 从配置文件获取域名配置
|
||||||
|
self.domains = EMAIL_DOMAINS
|
||||||
|
info(f"当前可用域名: {self.domains}")
|
||||||
|
|
||||||
|
self.email = None
|
||||||
|
self.password = None
|
||||||
|
|
||||||
|
def generate_random_password(self, length=12):
|
||||||
|
"""生成随机密码 - 改进密码生成算法,确保包含各类字符"""
|
||||||
|
chars = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
upper_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
digits = "0123456789"
|
||||||
|
special = "!@#$%^&*"
|
||||||
|
|
||||||
|
# 确保密码包含至少一个大写字母、一个数字和一个特殊字符
|
||||||
|
password = [
|
||||||
|
random.choice(chars),
|
||||||
|
random.choice(upper_chars),
|
||||||
|
random.choice(digits),
|
||||||
|
random.choice(special),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 添加剩余随机字符
|
||||||
|
password.extend(
|
||||||
|
random.choices(chars + upper_chars + digits + special, k=length - 4)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 打乱密码顺序
|
||||||
|
random.shuffle(password)
|
||||||
|
return "".join(password)
|
||||||
|
|
||||||
|
def generate_random_name(self, length=6):
|
||||||
|
"""生成随机用户名"""
|
||||||
|
first_letter = random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
rest_letters = "".join(
|
||||||
|
random.choices("abcdefghijklmnopqrstuvwxyz", k=length - 1)
|
||||||
|
)
|
||||||
|
return first_letter + rest_letters
|
||||||
|
|
||||||
|
def generate_email(self, length=8):
|
||||||
|
"""生成随机邮箱地址,使用随机域名"""
|
||||||
|
random_str = "".join(
|
||||||
|
random.choices("abcdefghijklmnopqrstuvwxyz1234567890", k=length)
|
||||||
|
)
|
||||||
|
timestamp = str(int(time.time()))[-4:] # 使用时间戳后4位
|
||||||
|
# 随机选择一个域名
|
||||||
|
domain = random.choice(self.domains)
|
||||||
|
return f"{random_str}@{domain}"
|
||||||
|
|
||||||
|
def get_account_info(self):
|
||||||
|
"""获取账号信息,确保每次调用都生成新的邮箱和密码"""
|
||||||
|
self.email = self.generate_email()
|
||||||
|
self.password = self.generate_random_password()
|
||||||
|
return {
|
||||||
|
"email": self.email,
|
||||||
|
"password": self.password,
|
||||||
|
"first_name": self.default_first_name.capitalize(),
|
||||||
|
"last_name": self.default_last_name.capitalize(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _save_account_info(self, user, token, total_usage):
|
||||||
|
try:
|
||||||
|
from database import get_session, AccountModel
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
async def save_to_db():
|
||||||
|
info(f"开始保存账号信息: {self.email}")
|
||||||
|
async with get_session() as session:
|
||||||
|
# 检查账号是否已存在
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(AccountModel).where(AccountModel.email == self.email)
|
||||||
|
)
|
||||||
|
existing_account = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if existing_account:
|
||||||
|
info(f"更新现有账号信息 (ID: {existing_account.id})")
|
||||||
|
existing_account.token = token
|
||||||
|
existing_account.user = user
|
||||||
|
existing_account.password = self.password
|
||||||
|
existing_account.usage_limit = str(total_usage)
|
||||||
|
# 如果账号状态是删除,更新为活跃
|
||||||
|
if existing_account.status == "deleted":
|
||||||
|
existing_account.status = "active"
|
||||||
|
# 不更新id,保留原始id值
|
||||||
|
else:
|
||||||
|
info("创建新账号记录")
|
||||||
|
# 生成毫秒级时间戳作为id
|
||||||
|
timestamp_ms = int(time.time() * 1000)
|
||||||
|
account = AccountModel(
|
||||||
|
email=self.email,
|
||||||
|
password=self.password,
|
||||||
|
token=token,
|
||||||
|
user=user,
|
||||||
|
usage_limit=str(total_usage),
|
||||||
|
created_at=datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
|
status="active", # 设置默认状态为活跃
|
||||||
|
id=timestamp_ms, # 设置毫秒时间戳id
|
||||||
|
)
|
||||||
|
session.add(account)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
info(f"账号 {self.email} 信息保存成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return asyncio.run(save_to_db())
|
||||||
|
except Exception as e:
|
||||||
|
info(f"保存账号信息失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_and_exit(browser_manager=None, exit_code=0):
|
||||||
|
"""清理资源并退出程序"""
|
||||||
|
try:
|
||||||
|
if browser_manager:
|
||||||
|
info("正在关闭浏览器")
|
||||||
|
if hasattr(browser_manager, "browser"):
|
||||||
|
browser_manager.browser.quit()
|
||||||
|
|
||||||
|
current_process = psutil.Process()
|
||||||
|
children = current_process.children(recursive=True)
|
||||||
|
for child in children:
|
||||||
|
try:
|
||||||
|
child.terminate()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
info("程序正常退出")
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
info(f"清理退出时发生错误: {str(e)}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
browser_manager = None
|
||||||
|
max_retries = REGISTRATION_MAX_RETRIES # 从配置文件获取
|
||||||
|
current_retry = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
email_handler = EmailVerificationHandler()
|
||||||
|
if email_handler.check():
|
||||||
|
info('邮箱服务连接正常,开始注册!')
|
||||||
|
else:
|
||||||
|
if EMAIL_CODE_TYPE == "API":
|
||||||
|
error('邮箱服务连接失败,并且验证码为API获取,结束注册!')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
info('邮箱服务连接失败,并且验证码为手动输入,等待输入验证码...')
|
||||||
|
|
||||||
|
email_generator = EmailGenerator()
|
||||||
|
browser_manager = BrowserManager()
|
||||||
|
browser = browser_manager.init_browser()
|
||||||
|
while current_retry < max_retries:
|
||||||
|
try:
|
||||||
|
account_info = email_generator.get_account_info()
|
||||||
|
info(
|
||||||
|
f"初始化账号信息成功 => 邮箱: {account_info['email']}, 用户名: {account_info['first_name']}, 密码: {account_info['password']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
signup_tab = browser.new_tab(LOGIN_URL)
|
||||||
|
browser.activate_tab(signup_tab)
|
||||||
|
|
||||||
|
signup_tab.run_js("try { turnstile.reset() } catch(e) { }")
|
||||||
|
result = sign_up_account(browser, signup_tab, account_info)
|
||||||
|
|
||||||
|
if result == "SUCCESS":
|
||||||
|
token, user = get_cursor_session_token(signup_tab)
|
||||||
|
info(f"获取到账号Token: {token}, 用户: {user}")
|
||||||
|
if token:
|
||||||
|
email_generator._save_account_info(user, token, TOTAL_USAGE)
|
||||||
|
info("注册流程完成")
|
||||||
|
cleanup_and_exit(browser_manager, 0)
|
||||||
|
else:
|
||||||
|
info("获取Cursor会话Token失败")
|
||||||
|
current_retry += 1
|
||||||
|
elif result in [
|
||||||
|
"EMAIL_USED",
|
||||||
|
"SIGNUP_RESTRICTED",
|
||||||
|
"VERIFY_FAILED",
|
||||||
|
"EMAIL_GET_CODE_FAILED",
|
||||||
|
]:
|
||||||
|
info(f"遇到问题: {result},尝试切换邮箱...")
|
||||||
|
continue # 使用新邮箱重试注册
|
||||||
|
else: # ERROR
|
||||||
|
info("遇到错误,准备重试...")
|
||||||
|
current_retry += 1
|
||||||
|
|
||||||
|
# 关闭标签页,准备下一次尝试
|
||||||
|
signup_tab.close()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
info(f"当前尝试发生错误: {str(e)}")
|
||||||
|
current_retry += 1
|
||||||
|
time.sleep(2)
|
||||||
|
try:
|
||||||
|
# 尝试关闭可能存在的标签页
|
||||||
|
if "signup_tab" in locals():
|
||||||
|
signup_tab.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
info(f"达到最大重试次数 {max_retries},注册失败")
|
||||||
|
except Exception as e:
|
||||||
|
info(f"主程序错误: {str(e)}")
|
||||||
|
info(f"错误详情: {traceback.format_exc()}")
|
||||||
|
cleanup_and_exit(browser_manager, 1)
|
||||||
|
finally:
|
||||||
|
cleanup_and_exit(browser_manager, 1)
|
495
cursor_pro_keep_alive_backup.py
Normal file
495
cursor_pro_keep_alive_backup.py
Normal file
@ -0,0 +1,495 @@
|
|||||||
|
import sys
|
||||||
|
import psutil
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
from logger import info, warning, error
|
||||||
|
import traceback
|
||||||
|
from config import (
|
||||||
|
LOGIN_URL,
|
||||||
|
SIGN_UP_URL,
|
||||||
|
SETTINGS_URL,
|
||||||
|
EMAIL_DOMAINS,
|
||||||
|
REGISTRATION_MAX_RETRIES,
|
||||||
|
EMAIL_TYPE,
|
||||||
|
EMAIL_CODE_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if sys.stdout.encoding != "utf-8":
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
if sys.stderr.encoding != "utf-8":
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
from browser_utils import BrowserManager
|
||||||
|
from get_email_code import EmailVerificationHandler
|
||||||
|
|
||||||
|
from datetime import datetime # 添加这行导入
|
||||||
|
|
||||||
|
TOTAL_USAGE = 0
|
||||||
|
|
||||||
|
|
||||||
|
def handle_turnstile(tab):
|
||||||
|
info("=============正在检测 Turnstile 验证=============")
|
||||||
|
max_count = 5
|
||||||
|
try:
|
||||||
|
count = 1
|
||||||
|
while True:
|
||||||
|
if count > max_count:
|
||||||
|
error("Turnstile 验证次数超过最大限制,退出")
|
||||||
|
return False
|
||||||
|
info(f"正在进行 Turnstile 第 {count} 次验证中...")
|
||||||
|
try:
|
||||||
|
# 检查页面状态,但不直接返回,先检查是否有Turnstile验证需要处理
|
||||||
|
page_ready = False
|
||||||
|
if tab.ele("@name=password"):
|
||||||
|
info("检测到密码输入页面,检查是否有验证需要处理...")
|
||||||
|
page_ready = True
|
||||||
|
elif tab.ele("@data-index=0"):
|
||||||
|
info("检测到验证码输入页面,检查是否有验证需要处理...")
|
||||||
|
page_ready = True
|
||||||
|
elif tab.ele("Account Settings"):
|
||||||
|
info("检测到账户设置页面,检查是否有验证需要处理...")
|
||||||
|
page_ready = True
|
||||||
|
|
||||||
|
# 即使页面已经准备好,也检查是否有Turnstile验证需要处理
|
||||||
|
info("检测 Turnstile 验证...")
|
||||||
|
try:
|
||||||
|
challengeCheck = (
|
||||||
|
tab.ele("@id=cf-turnstile", timeout=2)
|
||||||
|
.child()
|
||||||
|
.shadow_root.ele("tag:iframe")
|
||||||
|
.ele("tag:body")
|
||||||
|
.sr("tag:input")
|
||||||
|
)
|
||||||
|
|
||||||
|
if challengeCheck:
|
||||||
|
info("检测到 Turnstile 验证,正在处理...")
|
||||||
|
challengeCheck.click()
|
||||||
|
time.sleep(2)
|
||||||
|
info("Turnstile 验证通过")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
info("未检测到 Turnstile 验证")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
# 如果页面已准备好且没有验证需要处理,则可以返回
|
||||||
|
if page_ready:
|
||||||
|
info("页面已准备好,没有检测到需要处理的验证")
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(random.uniform(1, 2))
|
||||||
|
count += 1
|
||||||
|
return True # 返回True表示页面已准备好
|
||||||
|
except Exception as e:
|
||||||
|
info(f"Turnstile 验证失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_cursor_session_token(tab, max_attempts=5, retry_interval=3):
|
||||||
|
try:
|
||||||
|
tab.get(SETTINGS_URL)
|
||||||
|
time.sleep(5)
|
||||||
|
try:
|
||||||
|
usage_selector = (
|
||||||
|
"css:div.col-span-2 > div > div > div > div > "
|
||||||
|
"div:nth-child(1) > div.flex.items-center.justify-between.gap-2 > "
|
||||||
|
"span.font-mono.text-sm\\/\\[0\\.875rem\\]"
|
||||||
|
)
|
||||||
|
usage_ele = tab.ele(usage_selector)
|
||||||
|
total_usage = "unknown"
|
||||||
|
if usage_ele:
|
||||||
|
total_usage = usage_ele.text.split("/")[-1].strip()
|
||||||
|
global TOTAL_USAGE
|
||||||
|
TOTAL_USAGE = total_usage
|
||||||
|
info(f"使用限制: {total_usage}")
|
||||||
|
else:
|
||||||
|
warning("未能找到使用量元素")
|
||||||
|
except Exception as e:
|
||||||
|
warning(f"获取使用量信息失败: {str(e)}")
|
||||||
|
# 继续执行,不要因为获取使用量失败而中断整个流程
|
||||||
|
|
||||||
|
info("获取Cookie中...")
|
||||||
|
attempts = 0
|
||||||
|
|
||||||
|
while attempts < max_attempts:
|
||||||
|
try:
|
||||||
|
cookies = tab.cookies()
|
||||||
|
for cookie in cookies:
|
||||||
|
if cookie.get("name") == "WorkosCursorSessionToken":
|
||||||
|
user = cookie["value"].split("%3A%3A")[0]
|
||||||
|
token = cookie["value"].split("%3A%3A")[1]
|
||||||
|
info(f"获取到账号Token: {token}, 用户: {user}")
|
||||||
|
return token, user
|
||||||
|
|
||||||
|
attempts += 1
|
||||||
|
if attempts < max_attempts:
|
||||||
|
warning(
|
||||||
|
f"未找到Cursor会话Token,重试中... ({attempts}/{max_attempts})"
|
||||||
|
)
|
||||||
|
time.sleep(retry_interval)
|
||||||
|
else:
|
||||||
|
info("未找到Cursor会话Token,已达到最大尝试次数")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
info(f"获取Token出错: {str(e)}")
|
||||||
|
attempts += 1
|
||||||
|
if attempts < max_attempts:
|
||||||
|
info(
|
||||||
|
f"重试获取Token,等待时间: {retry_interval}秒,尝试次数: {attempts}/{max_attempts}"
|
||||||
|
)
|
||||||
|
time.sleep(retry_interval)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
warning(f"获取Token过程出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def sign_up_account(browser, tab, account_info):
|
||||||
|
info("=============开始注册账号=============")
|
||||||
|
info(
|
||||||
|
f"账号信息: 邮箱: {account_info['email']}, 密码: {account_info['password']}, 姓名: {account_info['first_name']} {account_info['last_name']}"
|
||||||
|
)
|
||||||
|
if EMAIL_TYPE == "zmail":
|
||||||
|
EmailVerificationHandler.create_zmail_email(account_info)
|
||||||
|
tab.get(SIGN_UP_URL)
|
||||||
|
|
||||||
|
tab.wait(2)
|
||||||
|
|
||||||
|
if tab.ele("@name=cf-turnstile-response"):
|
||||||
|
error("开屏就是检测啊,大佬你的IP或UA需要换一下了啊,有问题了...要等一下")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if tab.ele("@name=first_name"):
|
||||||
|
info("=============正在填写个人信息=============")
|
||||||
|
tab.actions.click("@name=first_name").input(account_info["first_name"])
|
||||||
|
info(f"已输入名字: {account_info['first_name']}")
|
||||||
|
time.sleep(random.uniform(1, 3))
|
||||||
|
|
||||||
|
tab.actions.click("@name=last_name").input(account_info["last_name"])
|
||||||
|
info(f"已输入姓氏: {account_info['last_name']}")
|
||||||
|
time.sleep(random.uniform(1, 3))
|
||||||
|
|
||||||
|
tab.actions.click("@name=email").input(account_info["email"])
|
||||||
|
info(f"已输入邮箱: {account_info['email']}")
|
||||||
|
time.sleep(random.uniform(1, 3))
|
||||||
|
|
||||||
|
info("=============提交个人信息=============")
|
||||||
|
tab.actions.click("@type=submit")
|
||||||
|
time.sleep(random.uniform(0.2, 1))
|
||||||
|
if (
|
||||||
|
tab.ele("verify the user is human. Please try again.")
|
||||||
|
or tab.ele("Can't verify the user is human. Please try again.")
|
||||||
|
or tab.ele("Can‘t verify the user is human. Please try again.")
|
||||||
|
):
|
||||||
|
info("检测到turnstile验证失败,(IP问题、UA问题、域名问题)...正在重试...")
|
||||||
|
return "EMAIL_USED"
|
||||||
|
except Exception as e:
|
||||||
|
info(f"填写个人信息失败: {str(e)}")
|
||||||
|
return "ERROR"
|
||||||
|
|
||||||
|
handle_turnstile(tab)
|
||||||
|
|
||||||
|
if tab.ele("verify the user is human. Please try again.") or tab.ele(
|
||||||
|
"Can't verify the user is human. Please try again."
|
||||||
|
):
|
||||||
|
info("检测到turnstile验证失败,正在重试...")
|
||||||
|
return "EMAIL_USED"
|
||||||
|
|
||||||
|
try:
|
||||||
|
if tab.ele("@name=password"):
|
||||||
|
info(f"设置密码:{account_info['password']}")
|
||||||
|
tab.ele("@name=password").input(account_info["password"])
|
||||||
|
time.sleep(random.uniform(1, 2))
|
||||||
|
|
||||||
|
info("提交密码...")
|
||||||
|
tab.ele("@type=submit").click()
|
||||||
|
info("密码设置成功,等待系统响应....")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
info(f"密码设置失败: {str(e)}")
|
||||||
|
return "ERROR"
|
||||||
|
|
||||||
|
info("处理最终验证...")
|
||||||
|
handle_turnstile(tab)
|
||||||
|
|
||||||
|
if tab.ele("This email is not available."):
|
||||||
|
info("邮箱已被使用")
|
||||||
|
return "EMAIL_USED"
|
||||||
|
|
||||||
|
if tab.ele("Sign up is restricted."):
|
||||||
|
info("注册限制")
|
||||||
|
return "SIGNUP_RESTRICTED"
|
||||||
|
|
||||||
|
# 创建邮件处理器
|
||||||
|
email_handler = EmailVerificationHandler()
|
||||||
|
i = 0
|
||||||
|
while i < 5:
|
||||||
|
try:
|
||||||
|
time.sleep(random.uniform(0.2, 1))
|
||||||
|
if tab.ele("Account Settings"):
|
||||||
|
info("注册成功,已进入账号设置页面")
|
||||||
|
break
|
||||||
|
if tab.ele("@data-index=0"):
|
||||||
|
info("等待输入验证码...")
|
||||||
|
# 切换到邮箱标签页
|
||||||
|
code = email_handler.get_verification_code(
|
||||||
|
source_email=account_info["email"]
|
||||||
|
)
|
||||||
|
if code is None:
|
||||||
|
info("未获取到验证码...系统异常,正在退出....")
|
||||||
|
return "EMAIL_GET_CODE_FAILED"
|
||||||
|
info(f"输入验证码: {code}")
|
||||||
|
i = 0
|
||||||
|
for digit in code:
|
||||||
|
tab.ele(f"@data-index={i}").input(digit)
|
||||||
|
time.sleep(random.uniform(0.3, 0.6))
|
||||||
|
i += 1
|
||||||
|
info("验证码输入完成")
|
||||||
|
time.sleep(random.uniform(3, 5))
|
||||||
|
|
||||||
|
# 在验证码输入完成后检测是否出现了Turnstile验证
|
||||||
|
info("检查是否出现了Turnstile验证...")
|
||||||
|
try:
|
||||||
|
turnstile_element = tab.ele("@id=cf-turnstile", timeout=3)
|
||||||
|
if turnstile_element:
|
||||||
|
info("检测到验证码输入后出现Turnstile验证,正在处理...")
|
||||||
|
handle_turnstile(tab)
|
||||||
|
except:
|
||||||
|
info("未检测到Turnstile验证,继续下一步")
|
||||||
|
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
info(f"验证码处理失败: {str(e)}")
|
||||||
|
return "ERROR"
|
||||||
|
|
||||||
|
info("完成最终验证...")
|
||||||
|
handle_turnstile(tab)
|
||||||
|
time.sleep(random.uniform(3, 5))
|
||||||
|
info("账号注册流程完成")
|
||||||
|
return "SUCCESS"
|
||||||
|
|
||||||
|
|
||||||
|
class EmailGenerator:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
# 将密码生成移到这里,避免类定义时执行随机密码生成
|
||||||
|
self.default_first_name = self.generate_random_name()
|
||||||
|
self.default_last_name = self.generate_random_name()
|
||||||
|
|
||||||
|
# 从配置文件获取域名配置
|
||||||
|
self.domains = EMAIL_DOMAINS
|
||||||
|
info(f"当前可用域名: {self.domains}")
|
||||||
|
|
||||||
|
self.email = None
|
||||||
|
self.password = None
|
||||||
|
|
||||||
|
def generate_random_password(self, length=12):
|
||||||
|
"""生成随机密码 - 改进密码生成算法,确保包含各类字符"""
|
||||||
|
chars = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
upper_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
digits = "0123456789"
|
||||||
|
special = "!@#$%^&*"
|
||||||
|
|
||||||
|
# 确保密码包含至少一个大写字母、一个数字和一个特殊字符
|
||||||
|
password = [
|
||||||
|
random.choice(chars),
|
||||||
|
random.choice(upper_chars),
|
||||||
|
random.choice(digits),
|
||||||
|
random.choice(special),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 添加剩余随机字符
|
||||||
|
password.extend(
|
||||||
|
random.choices(chars + upper_chars + digits + special, k=length - 4)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 打乱密码顺序
|
||||||
|
random.shuffle(password)
|
||||||
|
return "".join(password)
|
||||||
|
|
||||||
|
def generate_random_name(self, length=6):
|
||||||
|
"""生成随机用户名"""
|
||||||
|
first_letter = random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
rest_letters = "".join(
|
||||||
|
random.choices("abcdefghijklmnopqrstuvwxyz", k=length - 1)
|
||||||
|
)
|
||||||
|
return first_letter + rest_letters
|
||||||
|
|
||||||
|
def generate_email(self, length=8):
|
||||||
|
"""生成随机邮箱地址,使用随机域名"""
|
||||||
|
random_str = "".join(
|
||||||
|
random.choices("abcdefghijklmnopqrstuvwxyz1234567890", k=length)
|
||||||
|
)
|
||||||
|
timestamp = str(int(time.time()))[-4:] # 使用时间戳后4位
|
||||||
|
# 随机选择一个域名
|
||||||
|
domain = random.choice(self.domains)
|
||||||
|
return f"{random_str}@{domain}"
|
||||||
|
|
||||||
|
def get_account_info(self):
|
||||||
|
"""获取账号信息,确保每次调用都生成新的邮箱和密码"""
|
||||||
|
self.email = self.generate_email()
|
||||||
|
self.password = self.generate_random_password()
|
||||||
|
return {
|
||||||
|
"email": self.email,
|
||||||
|
"password": self.password,
|
||||||
|
"first_name": self.default_first_name.capitalize(),
|
||||||
|
"last_name": self.default_last_name.capitalize(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _save_account_info(self, user, token, total_usage):
|
||||||
|
try:
|
||||||
|
from database import get_session, AccountModel
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
async def save_to_db():
|
||||||
|
info(f"开始保存账号信息: {self.email}")
|
||||||
|
async with get_session() as session:
|
||||||
|
# 检查账号是否已存在
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(AccountModel).where(AccountModel.email == self.email)
|
||||||
|
)
|
||||||
|
existing_account = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if existing_account:
|
||||||
|
info(f"更新现有账号信息 (ID: {existing_account.id})")
|
||||||
|
existing_account.token = token
|
||||||
|
existing_account.user = user
|
||||||
|
existing_account.password = self.password
|
||||||
|
existing_account.usage_limit = str(total_usage)
|
||||||
|
# 如果账号状态是删除,更新为活跃
|
||||||
|
if existing_account.status == "deleted":
|
||||||
|
existing_account.status = "active"
|
||||||
|
# 不更新id,保留原始id值
|
||||||
|
else:
|
||||||
|
info("创建新账号记录")
|
||||||
|
# 生成毫秒级时间戳作为id
|
||||||
|
timestamp_ms = int(time.time() * 1000)
|
||||||
|
account = AccountModel(
|
||||||
|
email=self.email,
|
||||||
|
password=self.password,
|
||||||
|
token=token,
|
||||||
|
user=user,
|
||||||
|
usage_limit=str(total_usage),
|
||||||
|
created_at=datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
|
status="active", # 设置默认状态为活跃
|
||||||
|
id=timestamp_ms, # 设置毫秒时间戳id
|
||||||
|
)
|
||||||
|
session.add(account)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
info(f"账号 {self.email} 信息保存成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return asyncio.run(save_to_db())
|
||||||
|
except Exception as e:
|
||||||
|
info(f"保存账号信息失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_and_exit(browser_manager=None, exit_code=0):
|
||||||
|
"""清理资源并退出程序"""
|
||||||
|
try:
|
||||||
|
if browser_manager:
|
||||||
|
info("正在关闭浏览器")
|
||||||
|
if hasattr(browser_manager, "browser"):
|
||||||
|
browser_manager.browser.quit()
|
||||||
|
|
||||||
|
current_process = psutil.Process()
|
||||||
|
children = current_process.children(recursive=True)
|
||||||
|
for child in children:
|
||||||
|
try:
|
||||||
|
child.terminate()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
info("程序正常退出")
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
info(f"清理退出时发生错误: {str(e)}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
browser_manager = None
|
||||||
|
max_retries = REGISTRATION_MAX_RETRIES # 从配置文件获取
|
||||||
|
current_retry = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
email_handler = EmailVerificationHandler()
|
||||||
|
if email_handler.check():
|
||||||
|
info('邮箱服务连接正常,开始注册!')
|
||||||
|
else:
|
||||||
|
if EMAIL_CODE_TYPE == "API":
|
||||||
|
error('邮箱服务连接失败,并且验证码为API获取,结束注册!')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
info('邮箱服务连接失败,并且验证码为手动输入,等待输入验证码...')
|
||||||
|
|
||||||
|
email_generator = EmailGenerator()
|
||||||
|
browser_manager = BrowserManager()
|
||||||
|
browser = browser_manager.init_browser()
|
||||||
|
while current_retry < max_retries:
|
||||||
|
try:
|
||||||
|
account_info = email_generator.get_account_info()
|
||||||
|
info(
|
||||||
|
f"初始化账号信息成功 => 邮箱: {account_info['email']}, 用户名: {account_info['first_name']}, 密码: {account_info['password']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
signup_tab = browser.new_tab(LOGIN_URL)
|
||||||
|
browser.activate_tab(signup_tab)
|
||||||
|
|
||||||
|
signup_tab.run_js("try { turnstile.reset() } catch(e) { }")
|
||||||
|
result = sign_up_account(browser, signup_tab, account_info)
|
||||||
|
|
||||||
|
if result == "SUCCESS":
|
||||||
|
token, user = get_cursor_session_token(signup_tab)
|
||||||
|
info(f"获取到账号Token: {token}, 用户: {user}")
|
||||||
|
if token:
|
||||||
|
email_generator._save_account_info(user, token, TOTAL_USAGE)
|
||||||
|
info("注册流程完成")
|
||||||
|
cleanup_and_exit(browser_manager, 0)
|
||||||
|
else:
|
||||||
|
info("获取Cursor会话Token失败")
|
||||||
|
current_retry += 1
|
||||||
|
elif result in [
|
||||||
|
"EMAIL_USED",
|
||||||
|
"SIGNUP_RESTRICTED",
|
||||||
|
"VERIFY_FAILED",
|
||||||
|
"EMAIL_GET_CODE_FAILED",
|
||||||
|
]:
|
||||||
|
info(f"遇到问题: {result},尝试切换邮箱...")
|
||||||
|
continue # 使用新邮箱重试注册
|
||||||
|
else: # ERROR
|
||||||
|
info("遇到错误,准备重试...")
|
||||||
|
current_retry += 1
|
||||||
|
|
||||||
|
# 关闭标签页,准备下一次尝试
|
||||||
|
signup_tab.close()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
info(f"当前尝试发生错误: {str(e)}")
|
||||||
|
current_retry += 1
|
||||||
|
time.sleep(2)
|
||||||
|
try:
|
||||||
|
# 尝试关闭可能存在的标签页
|
||||||
|
if "signup_tab" in locals():
|
||||||
|
signup_tab.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
info(f"达到最大重试次数 {max_retries},注册失败")
|
||||||
|
except Exception as e:
|
||||||
|
info(f"主程序错误: {str(e)}")
|
||||||
|
info(f"错误详情: {traceback.format_exc()}")
|
||||||
|
cleanup_and_exit(browser_manager, 1)
|
||||||
|
finally:
|
||||||
|
cleanup_and_exit(browser_manager, 1)
|
288
cursor_shadow_patcher.py
Normal file
288
cursor_shadow_patcher.py
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
import pathlib
|
||||||
|
import platform
|
||||||
|
from uuid import uuid4
|
||||||
|
from logger import info, warning, error
|
||||||
|
|
||||||
|
from config import CURSOR_PATH
|
||||||
|
|
||||||
|
|
||||||
|
# 颜色常量定义,保留用于日志输出
|
||||||
|
GREEN = "\033[92m"
|
||||||
|
RED = "\033[91m"
|
||||||
|
YELLOW = "\033[93m"
|
||||||
|
BLUE = "\033[96m"
|
||||||
|
PURPLE = "\033[95m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
SYSTEM = platform.system()
|
||||||
|
if SYSTEM not in ("Windows", "Linux", "Darwin"):
|
||||||
|
raise OSError(f"不支持的操作系统: {SYSTEM}")
|
||||||
|
|
||||||
|
|
||||||
|
def uuid():
|
||||||
|
"""生成随机UUID"""
|
||||||
|
return str(uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
def path(path_str):
|
||||||
|
"""获取绝对路径"""
|
||||||
|
return pathlib.Path(path_str).resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def randomuuid(randomuuid_str):
|
||||||
|
"""获取随机UUID,如果提供则使用提供的值"""
|
||||||
|
if not randomuuid_str:
|
||||||
|
randomuuid_str = uuid()
|
||||||
|
return randomuuid_str
|
||||||
|
|
||||||
|
|
||||||
|
def random_mac():
|
||||||
|
"""生成随机MAC地址"""
|
||||||
|
mac = [
|
||||||
|
0x00,
|
||||||
|
0x16,
|
||||||
|
0x3E,
|
||||||
|
random.randint(0x00, 0x7F),
|
||||||
|
random.randint(0x00, 0xFF),
|
||||||
|
random.randint(0x00, 0xFF),
|
||||||
|
]
|
||||||
|
return ":".join(map(lambda x: "%02x" % x, mac))
|
||||||
|
|
||||||
|
|
||||||
|
def load(file_path: pathlib.Path):
|
||||||
|
"""加载文件内容"""
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def save(file_path: pathlib.Path, data: bytes):
|
||||||
|
"""保存文件内容"""
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
def backup(file_path: pathlib.Path):
|
||||||
|
"""备份文件"""
|
||||||
|
backup_path = file_path.with_suffix(file_path.suffix + ".bak")
|
||||||
|
if not backup_path.exists():
|
||||||
|
shutil.copy2(file_path, backup_path)
|
||||||
|
print(f"已备份 {file_path} -> {backup_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def replace(data: bytes, pattern: str, replace_str: str, probe: str = None) -> bytes:
|
||||||
|
"""替换文件内容"""
|
||||||
|
pattern_bytes = pattern.encode() if isinstance(pattern, str) else pattern
|
||||||
|
replace_bytes = (
|
||||||
|
replace_str.encode() if isinstance(replace_str, str) else replace_str
|
||||||
|
)
|
||||||
|
|
||||||
|
if probe:
|
||||||
|
probe_bytes = probe.encode() if isinstance(probe, str) else probe
|
||||||
|
if re.search(probe_bytes, data):
|
||||||
|
print("检测到已经被修补的代码,跳过...")
|
||||||
|
return data
|
||||||
|
|
||||||
|
return re.sub(pattern_bytes, replace_bytes, data)
|
||||||
|
|
||||||
|
|
||||||
|
def find_main_js():
|
||||||
|
"""查找Cursor的main.js文件"""
|
||||||
|
error(f"SYSTEM: {SYSTEM}")
|
||||||
|
if SYSTEM == "Windows":
|
||||||
|
localappdata = os.getenv("LOCALAPPDATA")
|
||||||
|
if not localappdata:
|
||||||
|
raise OSError("环境变量 %LOCALAPPDATA% 不存在")
|
||||||
|
|
||||||
|
# 使用本地变量保存路径
|
||||||
|
cursor_path = CURSOR_PATH
|
||||||
|
if not cursor_path:
|
||||||
|
error("当前windows系统, 环境变量 CURSOR_PATH 不存在,使用默认路径")
|
||||||
|
cursor_path = os.getenv("LOCALAPPDATA", "")
|
||||||
|
else:
|
||||||
|
info(f"当前windows系统, CURSOR_PATH: {cursor_path}")
|
||||||
|
|
||||||
|
# 常见的Cursor安装路径
|
||||||
|
paths = [
|
||||||
|
path(os.path.join(cursor_path, "resources", "app", "out", "main.js")),
|
||||||
|
path(
|
||||||
|
os.path.join(
|
||||||
|
localappdata,
|
||||||
|
"Programs",
|
||||||
|
"cursor",
|
||||||
|
"resources",
|
||||||
|
"app",
|
||||||
|
"out",
|
||||||
|
"main.js",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
os.path.join(
|
||||||
|
localappdata, "cursor", "resources", "app", "out", "main.js"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
for p in paths:
|
||||||
|
info(f"检查路径: {p}")
|
||||||
|
if p.exists():
|
||||||
|
info(f"找到main.js: {p}")
|
||||||
|
return p
|
||||||
|
else:
|
||||||
|
warning(f"路径不存在: {p}")
|
||||||
|
|
||||||
|
elif SYSTEM == "Darwin": # macOS
|
||||||
|
paths = [
|
||||||
|
path("/Applications/Cursor.app/Contents/Resources/app/out/main.js"),
|
||||||
|
path(
|
||||||
|
os.path.expanduser(
|
||||||
|
"~/Applications/Cursor.app/Contents/Resources/app/out/main.js"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
for p in paths:
|
||||||
|
if p.exists():
|
||||||
|
return p
|
||||||
|
|
||||||
|
elif SYSTEM == "Linux":
|
||||||
|
# Linux上常见的安装路径
|
||||||
|
paths = [
|
||||||
|
path("/usr/share/cursor/resources/app/out/main.js"),
|
||||||
|
path(os.path.expanduser("~/.local/share/cursor/resources/app/out/main.js")),
|
||||||
|
]
|
||||||
|
|
||||||
|
for p in paths:
|
||||||
|
if p.exists():
|
||||||
|
return p
|
||||||
|
|
||||||
|
raise FileNotFoundError("无法找到Cursor的main.js文件,请手动指定路径")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_cursor(
|
||||||
|
js_path=None, machine_id=None, mac_addr=None, sqm_id=None, dev_id=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
修补Cursor的main.js文件,替换机器ID等识别信息
|
||||||
|
|
||||||
|
参数:
|
||||||
|
js_path: main.js文件路径,如果为None则自动查找
|
||||||
|
machine_id: 机器ID,如果为None则随机生成
|
||||||
|
mac_addr: MAC地址,如果为None则随机生成
|
||||||
|
sqm_id: Windows SQM ID,如果为None则使用空字符串
|
||||||
|
dev_id: 设备ID,如果为None则随机生成
|
||||||
|
|
||||||
|
返回:
|
||||||
|
bool: 是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 查找main.js文件
|
||||||
|
if not js_path:
|
||||||
|
js_path = find_main_js()
|
||||||
|
else:
|
||||||
|
js_path = path(js_path)
|
||||||
|
|
||||||
|
# 如果找不到main.js文件
|
||||||
|
if not js_path.exists():
|
||||||
|
print(f"错误: 找不到文件 {js_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"找到main.js文件: {js_path}")
|
||||||
|
|
||||||
|
# 随机生成ID
|
||||||
|
machine_id = randomuuid(machine_id)
|
||||||
|
mac_addr = mac_addr or random_mac()
|
||||||
|
sqm_id = sqm_id or ""
|
||||||
|
dev_id = randomuuid(dev_id)
|
||||||
|
|
||||||
|
# 加载文件内容
|
||||||
|
data = load(js_path)
|
||||||
|
|
||||||
|
# 备份文件
|
||||||
|
backup(js_path)
|
||||||
|
|
||||||
|
# 替换机器ID
|
||||||
|
data = replace(
|
||||||
|
data,
|
||||||
|
r"=.{0,50}timeout.{0,10}5e3.*?,",
|
||||||
|
f'=/*csp1*/"{machine_id}"/*1csp*/,',
|
||||||
|
r"=/\*csp1\*/.*?/\*1csp\*/,",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 替换MAC地址
|
||||||
|
data = replace(
|
||||||
|
data,
|
||||||
|
r"(function .{0,50}\{).{0,300}Unable to retrieve mac address.*?(\})",
|
||||||
|
f'\\1return/*csp2*/"{mac_addr}"/*2csp*/;\\2',
|
||||||
|
r"()return/\*csp2\*/.*?/\*2csp\*/;()",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 替换SQM ID
|
||||||
|
data = replace(
|
||||||
|
data,
|
||||||
|
r'return.{0,50}\.GetStringRegKey.*?HKEY_LOCAL_MACHINE.*?MachineId.*?\|\|.*?""',
|
||||||
|
f'return/*csp3*/"{sqm_id}"/*3csp*/',
|
||||||
|
r"return/\*csp3\*/.*?/\*3csp\*/",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 替换设备ID
|
||||||
|
data = replace(
|
||||||
|
data,
|
||||||
|
r"return.{0,50}vscode\/deviceid.*?getDeviceId\(\)",
|
||||||
|
f'return/*csp4*/"{dev_id}"/*4csp*/',
|
||||||
|
r"return/\*csp4\*/.*?/\*4csp\*/",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存修改后的文件
|
||||||
|
save(js_path, data)
|
||||||
|
|
||||||
|
print(f"成功修补 {js_path}")
|
||||||
|
print(f"机器ID: {machine_id}")
|
||||||
|
print(f"MAC地址: {mac_addr}")
|
||||||
|
print(f"SQM ID: {sqm_id}")
|
||||||
|
print(f"设备ID: {dev_id}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"错误: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class CursorShadowPatcher:
|
||||||
|
"""Cursor机器标识修改器"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reset_machine_ids():
|
||||||
|
"""重置所有机器标识"""
|
||||||
|
return patch_cursor()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 作为独立脚本运行时,执行交互式修补
|
||||||
|
print(f"\n{'=' * 50}")
|
||||||
|
print("Cursor 机器标识重置工具 (Shadow Patch 增强版)")
|
||||||
|
print(f"{'=' * 50}")
|
||||||
|
|
||||||
|
js_path = input("请输入main.js路径 (留空=自动检测): ")
|
||||||
|
machine_id = input("机器ID (留空=随机生成): ")
|
||||||
|
mac_addr = input("MAC地址 (留空=随机生成): ")
|
||||||
|
sqm_id = input("Windows SQM ID (留空=使用空值): ")
|
||||||
|
dev_id = input("设备ID (留空=随机生成): ")
|
||||||
|
|
||||||
|
success = patch_cursor(js_path, machine_id, mac_addr, sqm_id, dev_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"\n{'=' * 50}")
|
||||||
|
print("修补成功!")
|
||||||
|
else:
|
||||||
|
print(f"\n{'=' * 50}")
|
||||||
|
print("修补失败!")
|
||||||
|
|
||||||
|
input("按回车键退出...")
|
93
database.py
Normal file
93
database.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
from sqlalchemy import Column, String, Text, text, BigInteger, ForeignKey
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from logger import info, error
|
||||||
|
from config import DATABASE_URL
|
||||||
|
|
||||||
|
|
||||||
|
# 基础模型类
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# 账号模型
|
||||||
|
class AccountModel(Base):
|
||||||
|
__tablename__ = "accounts"
|
||||||
|
email = Column(String, primary_key=True)
|
||||||
|
user = Column(String, nullable=False)
|
||||||
|
password = Column(String, nullable=True)
|
||||||
|
token = Column(String, nullable=False)
|
||||||
|
usage_limit = Column(Text, nullable=True)
|
||||||
|
created_at = Column(Text, nullable=True)
|
||||||
|
status = Column(String, default="active", nullable=False)
|
||||||
|
id = Column(BigInteger, nullable=False, index=True) # 添加毫秒时间戳列并创建索引
|
||||||
|
|
||||||
|
|
||||||
|
# 账号使用记录模型
|
||||||
|
class AccountUsageRecordModel(Base):
|
||||||
|
__tablename__ = "account_usage_records"
|
||||||
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
account_id = Column(BigInteger, nullable=False, index=True) # 账号ID
|
||||||
|
email = Column(String, nullable=False, index=True) # 账号邮箱
|
||||||
|
ip = Column(String, nullable=True) # 使用者IP
|
||||||
|
user_agent = Column(Text, nullable=True) # 使用者UA
|
||||||
|
created_at = Column(Text, nullable=False) # 创建时间
|
||||||
|
|
||||||
|
|
||||||
|
def create_engine():
|
||||||
|
"""创建数据库引擎"""
|
||||||
|
# 直接使用配置文件中的数据库URL
|
||||||
|
engine = create_async_engine(
|
||||||
|
DATABASE_URL,
|
||||||
|
echo=False,
|
||||||
|
connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {},
|
||||||
|
future=True,
|
||||||
|
)
|
||||||
|
# info(f"数据库引擎创建成功: {DATABASE_URL}")
|
||||||
|
return engine
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def get_session() -> AsyncSession:
|
||||||
|
"""创建数据库会话的异步上下文管理器"""
|
||||||
|
# 为每个请求创建新的引擎和会话
|
||||||
|
engine = create_engine()
|
||||||
|
async_session = async_sessionmaker(
|
||||||
|
engine, class_=AsyncSession, expire_on_commit=False, future=True
|
||||||
|
)
|
||||||
|
|
||||||
|
session = async_session()
|
||||||
|
try:
|
||||||
|
# 确保连接有效
|
||||||
|
await session.execute(text("SELECT 1"))
|
||||||
|
yield session
|
||||||
|
except Exception as e:
|
||||||
|
error(f"数据库会话错误: {str(e)}")
|
||||||
|
try:
|
||||||
|
await session.rollback()
|
||||||
|
except Exception as rollback_error:
|
||||||
|
error(f"回滚过程中出错: {str(rollback_error)}")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
await session.close()
|
||||||
|
except Exception as e:
|
||||||
|
error(f"关闭会话时出错: {str(e)}")
|
||||||
|
try:
|
||||||
|
await engine.dispose()
|
||||||
|
except Exception as e:
|
||||||
|
error(f"释放引擎时出错: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
async def init_db():
|
||||||
|
"""初始化数据库表结构"""
|
||||||
|
try:
|
||||||
|
engine = create_engine()
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
await engine.dispose()
|
||||||
|
info("数据库初始化成功")
|
||||||
|
except Exception as e:
|
||||||
|
error(f"数据库初始化失败: {str(e)}")
|
||||||
|
raise
|
110
deno脚本
Normal file
110
deno脚本
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* 导入Deno标准库中的HTTP服务器模块
|
||||||
|
*/
|
||||||
|
import { serve } from "https://deno.land/std/http/server.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API映射表,定义了路由前缀到实际API端点的映射关系
|
||||||
|
* 用于将请求转发到相应的第三方服务
|
||||||
|
*/
|
||||||
|
const apiMapping = {
|
||||||
|
'/discord': 'https://discord.com/api', // Discord API
|
||||||
|
'/telegram': 'https://api.telegram.org', // Telegram API
|
||||||
|
'/openai': 'https://api.openai.com', // OpenAI API
|
||||||
|
'/claude': 'https://api.anthropic.com', // Anthropic Claude API
|
||||||
|
'/gemini': 'https://generativelanguage.googleapis.com', // Google Gemini API
|
||||||
|
'/meta': 'https://www.meta.ai/api', // Meta AI API
|
||||||
|
'/groq': 'https://api.groq.com/openai', // Groq API (OpenAI兼容)
|
||||||
|
'/xai': 'https://api.x.ai', // X.AI API
|
||||||
|
'/cohere': 'https://api.cohere.ai', // Cohere API
|
||||||
|
'/huggingface': 'https://api-inference.huggingface.co', // Hugging Face API
|
||||||
|
'/together': 'https://api.together.xyz', // Together AI API
|
||||||
|
'/novita': 'https://api.novita.ai', // Novita AI API
|
||||||
|
'/portkey': 'https://api.portkey.ai', // Portkey API
|
||||||
|
'/fireworks': 'https://api.fireworks.ai', // Fireworks AI API
|
||||||
|
'/openrouter': 'https://openrouter.ai/api' // OpenRouter API
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动HTTP服务器,处理所有传入的请求
|
||||||
|
*/
|
||||||
|
serve(async (request) => {
|
||||||
|
// 解析请求URL
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const pathname = url.pathname;
|
||||||
|
|
||||||
|
// 处理根路径和index.html请求
|
||||||
|
if (pathname === '/' || pathname === '/index.html') {
|
||||||
|
return new Response('Service is running!', {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'text/html' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理robots.txt请求,禁止搜索引擎爬取
|
||||||
|
if (pathname === '/robots.txt') {
|
||||||
|
return new Response('User-agent: *\nDisallow: /', {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从请求路径中提取API前缀和剩余路径
|
||||||
|
const [prefix, rest] = extractPrefixAndRest(pathname, Object.keys(apiMapping));
|
||||||
|
if (!prefix) {
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建目标URL
|
||||||
|
const targetUrl = `${apiMapping[prefix]}${rest}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建新的请求头,只保留允许的头部信息
|
||||||
|
const headers = new Headers();
|
||||||
|
const allowedHeaders = ['accept', 'content-type', 'authorization'];
|
||||||
|
for (const [key, value] of request.headers.entries()) {
|
||||||
|
if (allowedHeaders.includes(key.toLowerCase())) {
|
||||||
|
headers.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转发请求到目标API
|
||||||
|
const response = await fetch(targetUrl, {
|
||||||
|
method: request.method,
|
||||||
|
headers: headers,
|
||||||
|
body: request.body
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置安全相关的响应头
|
||||||
|
const responseHeaders = new Headers(response.headers);
|
||||||
|
responseHeaders.set('X-Content-Type-Options', 'nosniff');
|
||||||
|
responseHeaders.set('X-Frame-Options', 'DENY');
|
||||||
|
responseHeaders.set('Referrer-Policy', 'no-referrer');
|
||||||
|
|
||||||
|
// 返回API响应
|
||||||
|
return new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
headers: responseHeaders
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 处理请求错误
|
||||||
|
console.error('Failed to fetch:', error);
|
||||||
|
return new Response('Internal Server Error', { status: 500 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从路径中提取API前缀和剩余部分
|
||||||
|
* @param {string} pathname - 请求路径
|
||||||
|
* @param {string[]} prefixes - 可用的API前缀列表
|
||||||
|
* @returns {[string|null, string|null]} - 返回匹配的前缀和剩余路径,如果没有匹配则返回[null, null]
|
||||||
|
*/
|
||||||
|
function extractPrefixAndRest(pathname, prefixes) {
|
||||||
|
for (const prefix of prefixes) {
|
||||||
|
if (pathname.startsWith(prefix)) {
|
||||||
|
return [prefix, pathname.slice(prefix.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [null, null];
|
||||||
|
}
|
34
dockerfile
Normal file
34
dockerfile
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Dockerfile for cursor-auto-register
|
||||||
|
|
||||||
|
# 1. 选择 Python 基础镜像
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
# 2. 设置环境变量,防止 Python 缓冲输出
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# 3. 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 4. 复制依赖文件
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# 5. 安装系统依赖 (针对 Playwright) 和 Python 依赖
|
||||||
|
# 更新 apt 包列表,安装 Playwright 所需的依赖,然后安装 Python 包,最后清理 apt 缓存
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libdbus-1-3 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libxkbcommon0 libpango-1.0-0 libcairo2 libasound2 && \
|
||||||
|
pip install --no-cache-dir -r requirements.txt && \
|
||||||
|
# 下载 Playwright 浏览器 (chromium, firefox, webkit 可按需选择)
|
||||||
|
playwright install --with-deps chromium && \
|
||||||
|
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 6. 复制项目代码到工作目录
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 7. 暴露应用程序端口 (根据 .env 文件配置,默认为 8000)
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# 8. 定义容器启动时运行的命令
|
||||||
|
# 使用 uvicorn 启动 FastAPI 应用
|
||||||
|
# 确保 api.py 中 FastAPI app 实例的变量名为 'app'
|
||||||
|
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]
|
81
get_cursor_session_token_re.py
Normal file
81
get_cursor_session_token_re.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
def get_cursor_session_token(tab, max_attempts: int = 3, retry_interval: int = 2) -> Optional[Tuple[str, str]]:
|
||||||
|
"""
|
||||||
|
获取Cursor会话token
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tab: 浏览器标签页对象
|
||||||
|
max_attempts: 最大尝试次数
|
||||||
|
retry_interval: 重试间隔(秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[str, str] | None: 成功返回(userId, accessToken)元组,失败返回None
|
||||||
|
"""
|
||||||
|
logging.info("开始获取会话令牌")
|
||||||
|
|
||||||
|
# 首先尝试使用UUID深度登录方式
|
||||||
|
logging.info("尝试使用深度登录方式获取token")
|
||||||
|
|
||||||
|
def _generate_pkce_pair():
|
||||||
|
"""生成PKCE验证对"""
|
||||||
|
code_verifier = secrets.token_urlsafe(43) # 生成随机的code_verifier
|
||||||
|
code_challenge_digest = hashlib.sha256(code_verifier.encode('utf-8')).digest() # 对verifier进行SHA256哈希
|
||||||
|
code_challenge = base64.urlsafe_b64encode(code_challenge_digest).decode('utf-8').rstrip('=') # Base64编码并移除填充字符
|
||||||
|
return code_verifier, code_challenge
|
||||||
|
|
||||||
|
attempts = 0
|
||||||
|
while attempts < max_attempts:
|
||||||
|
try:
|
||||||
|
verifier, challenge = _generate_pkce_pair() # 生成PKCE验证对
|
||||||
|
id = uuid.uuid4() # 生成唯一的UUID
|
||||||
|
client_login_url = f"https://www.cursor.com/cn/loginDeepControl?challenge={challenge}&uuid={id}&mode=login" # 构建深度登录URL
|
||||||
|
|
||||||
|
logging.info(f"访问深度登录URL: {client_login_url}")
|
||||||
|
tab.get(client_login_url) # 访问登录页面
|
||||||
|
save_screenshot(tab, f"deeplogin_attempt_{attempts}") # 保存登录页面截图
|
||||||
|
|
||||||
|
if tab.ele("xpath=//span[contains(text(), 'Yes, Log In')]", timeout=5): # 查找登录确认按钮
|
||||||
|
logging.info("点击确认登录按钮")
|
||||||
|
tab.ele("xpath=//span[contains(text(), 'Yes, Log In')]").click() # 点击确认登录
|
||||||
|
time.sleep(1.5) # 等待页面响应
|
||||||
|
|
||||||
|
auth_poll_url = f"https://api2.cursor.sh/auth/poll?uuid={id}&verifier={verifier}" # 构建认证轮询URL
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/0.48.6 Chrome/132.0.6834.210 Electron/34.3.4 Safari/537.36",
|
||||||
|
"Accept": "*/*"
|
||||||
|
} # 设置请求头
|
||||||
|
|
||||||
|
logging.info(f"轮询认证状态: {auth_poll_url}")
|
||||||
|
response = requests.get(auth_poll_url, headers=headers, timeout=5) # 发送认证状态轮询请求
|
||||||
|
|
||||||
|
if response.status_code == 200: # 请求成功
|
||||||
|
data = response.json() # 解析JSON响应
|
||||||
|
accessToken = data.get("accessToken", None) # 获取访问令牌
|
||||||
|
authId = data.get("authId", "") # 获取认证ID
|
||||||
|
|
||||||
|
if accessToken: # 如果成功获取到访问令牌
|
||||||
|
userId = ""
|
||||||
|
if len(authId.split("|")) > 1: # 从authId中提取userId
|
||||||
|
userId = authId.split("|")[1]
|
||||||
|
|
||||||
|
logging.info("成功获取账号token和userId")
|
||||||
|
return userId, accessToken # 返回用户ID和访问令牌
|
||||||
|
else:
|
||||||
|
logging.error(f"API请求失败,状态码: {response.status_code}") # 记录API请求失败
|
||||||
|
else:
|
||||||
|
logging.warning("未找到登录确认按钮") # 记录未找到登录按钮
|
||||||
|
|
||||||
|
attempts += 1 # 增加尝试次数
|
||||||
|
if attempts < max_attempts: # 如果还有重试机会
|
||||||
|
wait_time = retry_interval * attempts # 计算等待时间,逐步增加
|
||||||
|
logging.warning(f"第 {attempts} 次尝试未获取到token,{wait_time}秒后重试...")
|
||||||
|
save_screenshot(tab, f"token_attempt_{attempts}") # 保存失败截图
|
||||||
|
time.sleep(wait_time) # 等待指定时间后重试
|
||||||
|
|
||||||
|
except Exception as e: # 捕获所有异常
|
||||||
|
logging.error(f"深度登录获取token失败: {str(e)}") # 记录异常信息
|
||||||
|
attempts += 1 # 增加尝试次数
|
||||||
|
save_screenshot(tab, f"token_error_{attempts}") # 保存错误截图
|
||||||
|
if attempts < max_attempts: # 如果还有重试机会
|
||||||
|
wait_time = retry_interval * attempts # 计算等待时间
|
||||||
|
logging.warning(f"将在 {wait_time} 秒后重试...")
|
||||||
|
time.sleep(wait_time) # 等待指定时间后重试
|
464
get_email_code.py
Normal file
464
get_email_code.py
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
from logger import info, error
|
||||||
|
# 添加warn函数作为info的包装
|
||||||
|
def warn(message):
|
||||||
|
"""警告日志函数"""
|
||||||
|
info(f"警告: {message}")
|
||||||
|
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
from config import (
|
||||||
|
EMAIL_USERNAME,
|
||||||
|
EMAIL_DOMAIN,
|
||||||
|
EMAIL_PIN,
|
||||||
|
EMAIL_VERIFICATION_RETRIES,
|
||||||
|
EMAIL_VERIFICATION_WAIT,
|
||||||
|
EMAIL_TYPE,
|
||||||
|
EMAIL_PROXY_ADDRESS,
|
||||||
|
EMAIL_PROXY_ENABLED,
|
||||||
|
EMAIL_API,
|
||||||
|
EMAIL_CODE_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailVerificationHandler:
|
||||||
|
def __init__(self, username=None, domain=None, pin=None, use_proxy=False):
|
||||||
|
self.email = EMAIL_TYPE
|
||||||
|
self.username = username or EMAIL_USERNAME
|
||||||
|
self.domain = domain or EMAIL_DOMAIN
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.emailApi = EMAIL_API
|
||||||
|
self.emailExtension = self.domain
|
||||||
|
self.pin = pin or EMAIL_PIN
|
||||||
|
if self.pin == "":
|
||||||
|
info("注意: 邮箱PIN码为空")
|
||||||
|
if self.email == "tempemail":
|
||||||
|
info(
|
||||||
|
f"初始化邮箱验证器成功: {self.username}@{self.domain} pin: {self.pin}"
|
||||||
|
)
|
||||||
|
elif self.email == "zmail":
|
||||||
|
info(
|
||||||
|
f"初始化邮箱验证器成功: {self.emailApi}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加代理支持
|
||||||
|
if use_proxy and EMAIL_PROXY_ENABLED:
|
||||||
|
proxy = {
|
||||||
|
"http": f"{EMAIL_PROXY_ADDRESS}",
|
||||||
|
"https": f"{EMAIL_PROXY_ADDRESS}",
|
||||||
|
}
|
||||||
|
self.session.proxies.update(proxy)
|
||||||
|
info(f"已启用代理: {EMAIL_PROXY_ADDRESS}")
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
mail_list_url = f"https://tempmail.plus/api/mails?email={self.username}%40{self.domain}&limit=20&epin={self.pin}"
|
||||||
|
try:
|
||||||
|
# 增加超时时间并添加错误重试
|
||||||
|
for retry in range(3):
|
||||||
|
try:
|
||||||
|
info(f"请求URL (尝试 {retry+1}/3): {mail_list_url}")
|
||||||
|
mail_list_response = self.session.get(mail_list_url, timeout=30) # 增加超时时间到30秒
|
||||||
|
mail_list_data = mail_list_response.json()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# 修正判断逻辑:当result为true时才是成功
|
||||||
|
if mail_list_data.get("result") == True:
|
||||||
|
info(f"成功获取邮件列表数据: 共{mail_list_data.get('count', 0)}封邮件")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
error(f"API返回结果中无result字段或result为false: {mail_list_data}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
|
||||||
|
if retry < 2: # 如果不是最后一次尝试
|
||||||
|
warn(f"请求超时或连接错误,正在重试... ({retry+1}/3)")
|
||||||
|
time.sleep(2) # 增加重试间隔
|
||||||
|
else:
|
||||||
|
raise # 最后一次尝试失败,抛出异常
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
error("获取邮件列表超时")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
error("获取邮件列表连接错误")
|
||||||
|
info(f'{mail_list_url}')
|
||||||
|
except Exception as e:
|
||||||
|
error(f"获取邮件列表发生错误: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_verification_code(
|
||||||
|
self, source_email=None, max_retries=None, wait_time=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
获取验证码,增加了重试机制
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_retries: 最大重试次数
|
||||||
|
wait_time: 每次重试间隔时间(秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 验证码或None
|
||||||
|
"""
|
||||||
|
# 如果邮箱验证码获取方式为输入,则直接返回输入的验证码
|
||||||
|
if EMAIL_CODE_TYPE == "INPUT":
|
||||||
|
info("EMAIL_CODE_TYPE设为INPUT,跳过自动获取,直接手动输入")
|
||||||
|
return self.prompt_manual_code()
|
||||||
|
|
||||||
|
max_retries = max_retries or EMAIL_VERIFICATION_RETRIES
|
||||||
|
wait_time = wait_time or EMAIL_VERIFICATION_WAIT
|
||||||
|
info(f"开始获取邮箱验证码=>最大重试次数:{max_retries}, 等待时间:{wait_time}")
|
||||||
|
|
||||||
|
# 验证邮箱类型是否支持
|
||||||
|
if self.email not in ["tempemail", "zmail"]:
|
||||||
|
error(f"不支持的邮箱类型: {self.email},支持的类型为: tempemail, zmail")
|
||||||
|
warn("自动切换到手动输入模式")
|
||||||
|
return self.prompt_manual_code()
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
info(f"当前EMail类型为: {self.email}")
|
||||||
|
code = None
|
||||||
|
mail_id = None
|
||||||
|
|
||||||
|
if self.email == "tempemail":
|
||||||
|
code, mail_id = self.get_tempmail_email_code(source_email)
|
||||||
|
elif self.email == "zmail":
|
||||||
|
code, mail_id = self.get_zmail_email_code(source_email)
|
||||||
|
|
||||||
|
if code:
|
||||||
|
info(f"成功获取验证码: {code}")
|
||||||
|
return code
|
||||||
|
elif attempt < max_retries - 1:
|
||||||
|
info(f"未找到验证码,{wait_time}秒后重试 ({attempt + 1}/{max_retries})...")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
else:
|
||||||
|
info(f"已达到最大重试次数({max_retries}),未找到验证码")
|
||||||
|
except Exception as e:
|
||||||
|
error(f"获取验证码失败: {str(e)}")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
info(f"将在{wait_time}秒后重试...")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
else:
|
||||||
|
error(f"已达到最大重试次数({max_retries}),获取验证码失败")
|
||||||
|
|
||||||
|
# 所有自动尝试都失败后,询问是否手动输入
|
||||||
|
response = input("自动获取验证码失败,是否手动输入? (y/n): ").lower()
|
||||||
|
if response == 'y':
|
||||||
|
return self.prompt_manual_code()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 手动输入验证码
|
||||||
|
def prompt_manual_code(self):
|
||||||
|
"""手动输入验证码"""
|
||||||
|
info("自动获取验证码失败,开始手动输入验证码。")
|
||||||
|
code = input("输入6位数字验证码: ").strip()
|
||||||
|
return code
|
||||||
|
|
||||||
|
def get_tempmail_email_code(self, source_email=None):
|
||||||
|
info("开始获取邮件列表")
|
||||||
|
# 获取邮件列表
|
||||||
|
mail_list_url = f"https://tempmail.plus/api/mails?email={self.username}%40{self.domain}&limit=20&epin={self.pin}"
|
||||||
|
try:
|
||||||
|
# 增加错误重试和超时时间
|
||||||
|
for retry in range(3):
|
||||||
|
try:
|
||||||
|
info(f"请求邮件列表 (尝试 {retry+1}/3): {mail_list_url}")
|
||||||
|
mail_list_response = self.session.get(
|
||||||
|
mail_list_url, timeout=30
|
||||||
|
)
|
||||||
|
mail_list_data = mail_list_response.json()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# 修正判断逻辑
|
||||||
|
if mail_list_data.get("result") == True:
|
||||||
|
info(f"成功获取邮件列表: 共{mail_list_data.get('count', 0)}封邮件")
|
||||||
|
# 继续处理
|
||||||
|
else:
|
||||||
|
error(f"API返回失败结果: {mail_list_data}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
break # 成功获取数据,跳出重试循环
|
||||||
|
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
|
||||||
|
if retry < 2: # 如果不是最后一次尝试
|
||||||
|
warn(f"请求超时或连接错误,正在重试... ({retry+1}/3)")
|
||||||
|
time.sleep(2 * (retry + 1)) # 递增的等待时间
|
||||||
|
else:
|
||||||
|
raise # 最后一次尝试失败,抛出异常
|
||||||
|
|
||||||
|
# 获取最新邮件的ID
|
||||||
|
first_id = mail_list_data.get("first_id")
|
||||||
|
if not first_id:
|
||||||
|
return None, None
|
||||||
|
info(f"开始获取邮件详情: {first_id}")
|
||||||
|
# 获取具体邮件内容
|
||||||
|
mail_detail_url = f"https://tempmail.plus/api/mails/{first_id}?email={self.username}%40{self.domain}&epin={self.pin}"
|
||||||
|
try:
|
||||||
|
mail_detail_response = self.session.get(
|
||||||
|
mail_detail_url, timeout=10
|
||||||
|
) # 添加超时参数
|
||||||
|
mail_detail_data = mail_detail_response.json()
|
||||||
|
time.sleep(0.5)
|
||||||
|
if mail_detail_data.get("result") == False:
|
||||||
|
error(f"获取邮件详情失败: {mail_detail_data}")
|
||||||
|
return None, None
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
error("获取邮件详情超时")
|
||||||
|
return None, None
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
error("获取邮件详情连接错误")
|
||||||
|
return None, None
|
||||||
|
except Exception as e:
|
||||||
|
error(f"获取邮件详情发生错误: {str(e)}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 从邮件文本中提取6位数字验证码
|
||||||
|
mail_text = mail_detail_data.get("text", "")
|
||||||
|
|
||||||
|
# 如果提供了source_email,确保邮件内容中包含该邮箱地址
|
||||||
|
if source_email and source_email.lower() not in mail_text.lower():
|
||||||
|
error(f"邮件内容不包含指定的邮箱地址: {source_email}")
|
||||||
|
else:
|
||||||
|
info(f"邮件内容包含指定的邮箱地址: {source_email}")
|
||||||
|
|
||||||
|
code_match = re.search(r"(?<![a-zA-Z@.])\b\d{6}\b", mail_text)
|
||||||
|
|
||||||
|
if code_match:
|
||||||
|
# 清理邮件
|
||||||
|
self._cleanup_mail(first_id)
|
||||||
|
return code_match.group(), first_id
|
||||||
|
return None, None
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
error("获取邮件列表超时")
|
||||||
|
return None, None
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
error("获取邮件列表连接错误")
|
||||||
|
return None, None
|
||||||
|
except Exception as e:
|
||||||
|
error(f"获取邮件列表发生错误: {str(e)}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def _cleanup_mail(self, first_id):
|
||||||
|
# 构造删除请求的URL和数据
|
||||||
|
delete_url = "https://tempmail.plus/api/mails/"
|
||||||
|
payload = {
|
||||||
|
"email": f"{self.username}@{self.domain}",
|
||||||
|
"first_id": first_id,
|
||||||
|
"epin": self.pin,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 最多尝试3次
|
||||||
|
for _ in range(3):
|
||||||
|
response = self.session.delete(delete_url, data=payload)
|
||||||
|
try:
|
||||||
|
result = response.json().get("result")
|
||||||
|
if result is True:
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 如果失败,等待0.2秒后重试
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 如果是zmail 需要先创建邮箱
|
||||||
|
def create_zmail_email(account_info):
|
||||||
|
# 如果邮箱类型是zmail 需要先创建邮箱
|
||||||
|
session = requests.Session()
|
||||||
|
if EMAIL_PROXY_ENABLED:
|
||||||
|
proxy = {
|
||||||
|
"http": f"{EMAIL_PROXY_ADDRESS}",
|
||||||
|
"https": f"{EMAIL_PROXY_ADDRESS}",
|
||||||
|
}
|
||||||
|
session.proxies.update(proxy)
|
||||||
|
# 创建临时邮箱URL
|
||||||
|
create_url = f"{EMAIL_API}/api/mailboxes"
|
||||||
|
username = account_info["email"].split("@")[0]
|
||||||
|
# 生成临时邮箱地址
|
||||||
|
payload = {
|
||||||
|
"address": f"{username}",
|
||||||
|
"expiresInHours": 24,
|
||||||
|
}
|
||||||
|
# 发送POST请求创建临时邮箱
|
||||||
|
try:
|
||||||
|
create_response = session.post(
|
||||||
|
create_url, json=payload, timeout=100
|
||||||
|
) # 添加超时参数
|
||||||
|
info(f"创建临时邮箱成功: {create_response.status_code}")
|
||||||
|
create_data = create_response.json()
|
||||||
|
info(f"创建临时邮箱返回数据: {create_data}")
|
||||||
|
# 检查创建邮箱是否成功
|
||||||
|
time.sleep(0.5)
|
||||||
|
if create_data.get("success") is True or create_data.get('error') == '邮箱地址已存在':
|
||||||
|
info(f"邮箱创建成功: {create_data}")
|
||||||
|
else:
|
||||||
|
error(f"邮箱创建失败: {create_data}")
|
||||||
|
return None, None
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
error("创建临时邮箱超时", create_url)
|
||||||
|
return None, None
|
||||||
|
info(f"创建临时邮箱成功: {create_data}, 返回值: {create_data}")
|
||||||
|
|
||||||
|
# 获取zmail邮箱验证码
|
||||||
|
def get_zmail_email_code(self, source_email=None):
|
||||||
|
info("开始获取邮件列表")
|
||||||
|
# 获取邮件列表
|
||||||
|
username = source_email.split("@")[0]
|
||||||
|
mail_list_url = f"{EMAIL_API}/api/mailboxes/{username}/emails"
|
||||||
|
proxy = {
|
||||||
|
"http": f"{EMAIL_PROXY_ADDRESS}",
|
||||||
|
"https": f"{EMAIL_PROXY_ADDRESS}",
|
||||||
|
}
|
||||||
|
self.session.proxies.update(proxy)
|
||||||
|
try:
|
||||||
|
mail_list_response = self.session.get(
|
||||||
|
mail_list_url, timeout=10000
|
||||||
|
) # 添加超时参数
|
||||||
|
mail_list_data = mail_list_response.json()
|
||||||
|
time.sleep(2)
|
||||||
|
if not mail_list_data.get("emails"):
|
||||||
|
return None, None
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
error("获取邮件列表超时")
|
||||||
|
return None, None
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
error("获取邮件列表连接错误")
|
||||||
|
return None, None
|
||||||
|
except Exception as e:
|
||||||
|
error(f"获取邮件列表发生错误: {str(e)}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 获取最新邮件的ID、
|
||||||
|
mail_detail_data_len = len(mail_list_data["emails"])
|
||||||
|
if mail_detail_data_len == 0:
|
||||||
|
return None, None
|
||||||
|
mail_list_data = mail_list_data["emails"][0]
|
||||||
|
# 获取最新邮件的ID
|
||||||
|
mail_id = mail_list_data.get("id")
|
||||||
|
if not mail_id:
|
||||||
|
return None, None
|
||||||
|
# 获取具体邮件内容
|
||||||
|
mail_detail_url = f"{EMAIL_API}/api/emails/{mail_id}"
|
||||||
|
returnData = ''
|
||||||
|
try:
|
||||||
|
mail_detail_response = self.session.get(
|
||||||
|
mail_detail_url, timeout=10
|
||||||
|
) # 添加超时参数
|
||||||
|
returnData = mail_detail_response.json()
|
||||||
|
time.sleep(2)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
error("获取邮件详情超时")
|
||||||
|
return None, None
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
error("获取邮件详情连接错误")
|
||||||
|
return None, None
|
||||||
|
except Exception as e:
|
||||||
|
error(f"获取邮件详情发生错误: {str(e)}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 从邮件文本中提取6位数字验证码\
|
||||||
|
mail_text = returnData.get("email")
|
||||||
|
mail_text = mail_text.get("textContent")
|
||||||
|
# 如果提供了source_email,确保邮件内容中包含该邮箱地址
|
||||||
|
if source_email and source_email.lower() not in mail_text.lower():
|
||||||
|
error(f"邮件内容不包含指定的邮箱地址: {source_email}")
|
||||||
|
else:
|
||||||
|
info(f"邮件内容包含指定的邮箱地址: {source_email}")
|
||||||
|
|
||||||
|
code_match = re.search(r"(?<![a-zA-Z@.])\b\d{6}\b", mail_text)
|
||||||
|
info(f"验证码匹配结果: {code_match}")
|
||||||
|
# 如果找到验证码, 返回验证码和邮件ID
|
||||||
|
if code_match:
|
||||||
|
return code_match.group(), mail_id
|
||||||
|
else:
|
||||||
|
error("未找到验证码")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def diagnose_email_setup(self):
|
||||||
|
"""诊断邮箱设置并显示可能的问题"""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# 检查邮箱类型
|
||||||
|
if self.email not in ["tempemail", "zmail"]:
|
||||||
|
issues.append(f"不支持的邮箱类型: {self.email}")
|
||||||
|
|
||||||
|
# 检查邮箱用户名
|
||||||
|
if not self.username:
|
||||||
|
issues.append("邮箱用户名为空")
|
||||||
|
|
||||||
|
# 检查域名
|
||||||
|
if not self.domain:
|
||||||
|
issues.append("邮箱域名为空")
|
||||||
|
|
||||||
|
# 检查获取验证码类型
|
||||||
|
if EMAIL_CODE_TYPE == "INPUT":
|
||||||
|
issues.append("EMAIL_CODE_TYPE设为INPUT,将跳过自动获取")
|
||||||
|
|
||||||
|
info("----- 邮箱设置诊断 -----")
|
||||||
|
info(f"邮箱类型: {self.email}")
|
||||||
|
info(f"邮箱地址: {self.username}@{self.domain}")
|
||||||
|
info(f"验证码获取方式: {EMAIL_CODE_TYPE}")
|
||||||
|
|
||||||
|
if issues:
|
||||||
|
warn("发现以下问题:")
|
||||||
|
for issue in issues:
|
||||||
|
warn(f"- {issue}")
|
||||||
|
else:
|
||||||
|
info("未发现明显问题")
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# 添加代码检查并显示配置值
|
||||||
|
info(f"当前配置: EMAIL_TYPE={EMAIL_TYPE}, EMAIL_CODE_TYPE={EMAIL_CODE_TYPE}")
|
||||||
|
|
||||||
|
# 如果EMAIL_CODE_TYPE为INPUT则警告用户
|
||||||
|
if EMAIL_CODE_TYPE == "INPUT":
|
||||||
|
warn("EMAIL_CODE_TYPE设为INPUT将会跳过自动获取验证码,直接手动输入")
|
||||||
|
# 给用户选择是否临时更改为自动模式
|
||||||
|
response = input("是否临时更改为自动模式? (y/n): ").lower()
|
||||||
|
if response == 'y':
|
||||||
|
EMAIL_CODE_TYPE = "AUTO"
|
||||||
|
info("已临时更改为自动模式")
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='测试邮箱验证码获取功能')
|
||||||
|
parser.add_argument('--username', default=EMAIL_USERNAME, help='邮箱用户名')
|
||||||
|
parser.add_argument('--domain', default=EMAIL_DOMAIN, help='邮箱域名')
|
||||||
|
parser.add_argument('--pin', default=EMAIL_PIN, help='邮箱PIN码(可以为空)')
|
||||||
|
parser.add_argument('--source', help='来源邮箱(可选)')
|
||||||
|
parser.add_argument('--type', default=EMAIL_TYPE, choices=['tempemail', 'zmail'], help='邮箱类型')
|
||||||
|
parser.add_argument('--proxy', action='store_true', help='是否使用代理')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 覆盖全局EMAIL_TYPE以便测试不同类型
|
||||||
|
from config import EMAIL_TYPE
|
||||||
|
if args.type != EMAIL_TYPE:
|
||||||
|
info(f"覆盖EMAIL_TYPE从{EMAIL_TYPE}到{args.type}")
|
||||||
|
EMAIL_TYPE = args.type
|
||||||
|
|
||||||
|
# 创建邮箱验证处理器
|
||||||
|
handler = EmailVerificationHandler(
|
||||||
|
username=args.username,
|
||||||
|
domain=args.domain,
|
||||||
|
pin=args.pin,
|
||||||
|
use_proxy=args.proxy
|
||||||
|
)
|
||||||
|
|
||||||
|
# 诊断邮箱设置
|
||||||
|
handler.diagnose_email_setup()
|
||||||
|
|
||||||
|
# 测试检查邮箱
|
||||||
|
info("测试检查邮箱...")
|
||||||
|
check_result = handler.check()
|
||||||
|
info(f"检查结果: {'成功' if check_result else '失败'}")
|
||||||
|
|
||||||
|
# 测试获取验证码
|
||||||
|
info("测试获取验证码...")
|
||||||
|
code = handler.get_verification_code(source_email=args.source)
|
||||||
|
|
||||||
|
if code:
|
||||||
|
info(f"成功获取验证码: {code}")
|
||||||
|
else:
|
||||||
|
error("获取验证码失败")
|
||||||
|
|
||||||
|
info("测试完成")
|
122
handle_turnstile_update.py
Normal file
122
handle_turnstile_update.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
def handle_turnstile(self, tab=None, max_retries: int = 2, retry_interval: tuple = (1, 2)) -> bool:
|
||||||
|
"""
|
||||||
|
处理Turnstile验证
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tab: 浏览器标签对象
|
||||||
|
max_retries: 最大重试次数
|
||||||
|
retry_interval: 重试间隔范围(最小值, 最大值)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 验证是否成功
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TurnstileError: 验证过程中的异常
|
||||||
|
"""
|
||||||
|
# 如果没有传入tab参数,则使用实例的tab属性
|
||||||
|
tab = tab or self.tab
|
||||||
|
|
||||||
|
# 记录开始检测Turnstile验证的日志
|
||||||
|
logging.info(get_translation("detecting_turnstile"))
|
||||||
|
# 保存验证开始前的屏幕截图
|
||||||
|
save_screenshot(tab, "start")
|
||||||
|
|
||||||
|
# 初始化重试计数器
|
||||||
|
retry_count = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 在最大重试次数内循环尝试验证
|
||||||
|
while retry_count < max_retries:
|
||||||
|
# 增加重试计数
|
||||||
|
retry_count += 1
|
||||||
|
# 记录当前是第几次尝试验证
|
||||||
|
logging.debug(get_translation("retry_verification", count=retry_count))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 初始化元素变量
|
||||||
|
element = None
|
||||||
|
try:
|
||||||
|
# 尝试通过层级结构定位到Turnstile验证框的容器元素
|
||||||
|
element = (
|
||||||
|
tab.ele(".main-content") # 找到 .main-content 元素
|
||||||
|
.ele("tag:div") # 找到第一个子 div
|
||||||
|
.ele("tag:div") # 找到第二个子 div
|
||||||
|
.ele("tag:div") # 找到第三个子 div
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# 如果无法通过第一种方式找到元素,忽略异常继续执行
|
||||||
|
pass
|
||||||
|
|
||||||
|
if element:
|
||||||
|
# 如果找到了容器元素,则在其中定位验证框的输入元素
|
||||||
|
challenge_check = (
|
||||||
|
element
|
||||||
|
.shadow_root.ele("tag:iframe") # 找到shadow DOM中的iframe
|
||||||
|
.ele("tag:body") # 找到iframe中的body
|
||||||
|
.sr("tag:input") # 找到body中的input元素
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 如果没有找到容器元素,则尝试另一种方式定位验证框
|
||||||
|
challenge_check = (
|
||||||
|
tab.ele("@id=cf-turnstile", timeout=2) # 通过id直接找到turnstile元素
|
||||||
|
.child() # 获取其子元素
|
||||||
|
.shadow_root.ele("tag:iframe") # 找到shadow DOM中的iframe
|
||||||
|
.ele("tag:body") # 找到iframe中的body
|
||||||
|
.sr("tag:input") # 找到body中的input元素
|
||||||
|
)
|
||||||
|
|
||||||
|
if challenge_check:
|
||||||
|
# 如果找到了验证输入元素,记录日志
|
||||||
|
logging.info(get_translation("detected_turnstile"))
|
||||||
|
# 点击前随机延迟,模拟人工操作
|
||||||
|
time.sleep(random.uniform(1, 3))
|
||||||
|
# 点击验证元素触发验证
|
||||||
|
challenge_check.click()
|
||||||
|
# 等待验证处理
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# 保存点击验证后的屏幕截图
|
||||||
|
save_screenshot(tab, "clicked")
|
||||||
|
|
||||||
|
# 检查验证是否成功
|
||||||
|
if check_verification_success(tab):
|
||||||
|
# 验证成功,记录日志
|
||||||
|
logging.info(get_translation("turnstile_verification_passed"))
|
||||||
|
# 保存验证成功的屏幕截图
|
||||||
|
save_screenshot(tab, "success")
|
||||||
|
# 返回验证成功
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 记录当前尝试失败的详细信息
|
||||||
|
logging.debug(f"Current attempt unsuccessful: {str(e)}")
|
||||||
|
|
||||||
|
# 再次检查验证是否已经成功(可能在异常处理过程中已经通过验证)
|
||||||
|
if check_verification_success(tab):
|
||||||
|
# 返回验证成功
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 在下一次尝试前随机延迟
|
||||||
|
time.sleep(random.uniform(*retry_interval))
|
||||||
|
|
||||||
|
# 超过最大重试次数,验证失败
|
||||||
|
logging.error(get_translation("verification_failed_max_retries", max_retries=max_retries))
|
||||||
|
# 提供额外的帮助信息,引导用户访问开源项目
|
||||||
|
logging.error(
|
||||||
|
"Please visit the open source project for more information: https://github.com/wangffei/wf-cursor-auto-free.git"
|
||||||
|
)
|
||||||
|
# 保存验证失败的屏幕截图
|
||||||
|
save_screenshot(tab, "failed")
|
||||||
|
# 返回验证失败
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 捕获整个验证过程中的异常
|
||||||
|
# 构建错误信息
|
||||||
|
error_msg = get_translation("turnstile_exception", error=str(e))
|
||||||
|
# 记录错误日志
|
||||||
|
logging.error(error_msg)
|
||||||
|
# 保存发生错误时的屏幕截图
|
||||||
|
save_screenshot(tab, "error")
|
||||||
|
# 抛出TurnstileError异常
|
||||||
|
raise TurnstileError(error_msg)
|
BIN
images/1.jpg
Normal file
BIN
images/1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 810 KiB |
BIN
images/2.jpg
Normal file
BIN
images/2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 130 KiB |
BIN
images/3.jpg
Normal file
BIN
images/3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 174 KiB |
BIN
images/4.jpg
Normal file
BIN
images/4.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 673 KiB |
644
index.html
Normal file
644
index.html
Normal file
@ -0,0 +1,644 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Cursor账号管理系统</title>
|
||||||
|
<!-- Bootstrap 5 CSS -->
|
||||||
|
<link href="static/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- Font Awesome 图标 -->
|
||||||
|
<link rel="stylesheet" href="static/css/all.min.css">
|
||||||
|
<!-- 自定义样式 -->
|
||||||
|
<link href="static/css/styles.css" rel="stylesheet">
|
||||||
|
<!-- 添加现代字体 -->
|
||||||
|
<link href="static/css/css2" rel="stylesheet">
|
||||||
|
<!-- 添加动画库 -->
|
||||||
|
<link href="static/css/animate.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="app-layout">
|
||||||
|
<!-- 烟花动画画布 -->
|
||||||
|
<canvas id="fireworks-canvas"></canvas>
|
||||||
|
|
||||||
|
<!-- 加载中遮罩 -->
|
||||||
|
<div class="loading-overlay" id="loading-overlay">
|
||||||
|
<div class="spinner-container">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">加载中...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 mb-0">加载中,请稍候...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 通知容器 -->
|
||||||
|
<div id="alert-container"></div>
|
||||||
|
|
||||||
|
<!-- 侧边栏菜单 -->
|
||||||
|
<div id="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<!-- <img src="static/img/logo.png" alt="Cursor Logo" class="logo-img"> -->
|
||||||
|
<h4>Cursor账号管理</h4>
|
||||||
|
</div>
|
||||||
|
<ul class="nav-menu">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#" class="nav-link active" data-page="tasks-accounts">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
<span>账号管理</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#" class="nav-link" data-page="system-config">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
<span>系统配置</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span>版本: 1.0.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<div id="content">
|
||||||
|
<!-- 保留原有的页面结构,只添加页面包装器 -->
|
||||||
|
<div id="tasks-accounts" class="page-content active">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">任务控制</h5>
|
||||||
|
<div id="registration-status" class="badge bg-success">空闲中</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div id="task-status">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-check-circle text-success me-2"></i>
|
||||||
|
<span id="task-status-text">系统空闲中,可以开始新任务</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 账号使用情况显示 -->
|
||||||
|
<div class="usage-info mt-2">
|
||||||
|
<div class="usage-numbers">
|
||||||
|
<span class="used-count" id="current-count">0</span>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span class="total-count" id="max-accounts">10</span>
|
||||||
|
<span class="remaining-count" id="remaining-slots">剩余: 10</span>
|
||||||
|
</div>
|
||||||
|
<div class="battery-progress mt-1" data-percent="0">
|
||||||
|
<div class="battery-bars">
|
||||||
|
<span class="battery-bar"></span>
|
||||||
|
<span class="battery-bar"></span>
|
||||||
|
<span class="battery-bar"></span>
|
||||||
|
<span class="battery-bar"></span>
|
||||||
|
<span class="battery-bar"></span>
|
||||||
|
<span class="battery-bar"></span>
|
||||||
|
<span class="battery-bar"></span>
|
||||||
|
<span class="battery-bar"></span>
|
||||||
|
<span class="battery-bar"></span>
|
||||||
|
<span class="battery-bar"></span>
|
||||||
|
</div>
|
||||||
|
<span class="battery-percent">0%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-md-end mt-3 mt-md-0">
|
||||||
|
<button id="refresh-btn" class="btn btn-outline-secondary me-2"
|
||||||
|
style="display: none;">
|
||||||
|
<i class="fas fa-sync-alt me-1"></i> 刷新数据
|
||||||
|
</button>
|
||||||
|
<button id="start-registration" class="btn btn-primary me-2">
|
||||||
|
<i class="fas fa-user-plus me-1"></i> 注册新账号
|
||||||
|
</button>
|
||||||
|
<button id="stop-registration" class="btn btn-danger" style="display: none;">
|
||||||
|
<i class="fas fa-stop-circle me-1"></i> 停止任务
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务详细信息 -->
|
||||||
|
<div id="task-details" class="mt-4">
|
||||||
|
<div class="card border-0 bg-light">
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small mb-2">
|
||||||
|
<span class="text-muted">上次运行: </span>
|
||||||
|
<span id="last-run">从未运行</span>
|
||||||
|
</div>
|
||||||
|
<div class="small mb-2">
|
||||||
|
<span class="text-muted">下次运行: </span>
|
||||||
|
<span id="next-run">未排程</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small mb-2">
|
||||||
|
<span class="text-muted">总运行次数: </span>
|
||||||
|
<span id="total-runs">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="small">
|
||||||
|
<span class="text-muted">成功率: </span>
|
||||||
|
<span id="success-rate">N/A</span>
|
||||||
|
<span class="ms-2">
|
||||||
|
(<span class="text-success" id="successful-runs">0</span> 成功 /
|
||||||
|
<span class="text-danger" id="failed-runs">0</span> 失败)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="registration-details" class="mt-3" style="display: none;">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
<div>
|
||||||
|
<strong>注册进度: </strong><span id="registration-progress">0%</span>
|
||||||
|
<div id="registration-message">正在初始化注册流程...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 在搜索栏旁边添加排序控件 -->
|
||||||
|
<div class="row mb-3 align-items-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<label class="me-2 text-muted mb-0">排序方式:</label>
|
||||||
|
<select id="sort-field" class="form-select form-select-sm me-2" style="width: auto;">
|
||||||
|
<option value="created_at">创建时间</option>
|
||||||
|
<option value="email">邮箱</option>
|
||||||
|
<option value="usage_limit">余量</option>
|
||||||
|
</select>
|
||||||
|
<select id="sort-order" class="form-select form-select-sm" style="width: auto;">
|
||||||
|
<option value="desc">降序</option>
|
||||||
|
<option value="asc">升序</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-end">
|
||||||
|
<button id="import-accounts-btn" class="btn btn-outline-secondary btn-sm me-1">
|
||||||
|
<i class="fas fa-file-import me-1"></i>导入账号
|
||||||
|
</button>
|
||||||
|
<button id="export-accounts-btn" class="btn btn-outline-secondary btn-sm me-1">
|
||||||
|
<i class="fas fa-file-export me-1"></i>导出账号
|
||||||
|
</button>
|
||||||
|
<button id="update-all-usage-btn" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-sync-alt me-1"></i>更新所有余量
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加隐藏的文件输入 -->
|
||||||
|
<input type="file" id="import-file-input" accept=".json" style="display: none;">
|
||||||
|
|
||||||
|
<!-- 添加导入确认对话框 -->
|
||||||
|
<div class="modal fade" id="import-confirm-modal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">确认导入账号</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>即将导入 <span id="import-count">0</span> 个账号。</p>
|
||||||
|
<p>注意:同名账号将被更新覆盖。确定要继续吗?</p>
|
||||||
|
<div class="form-check mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="import-overwrite-check" checked>
|
||||||
|
<label class="form-check-label" for="import-overwrite-check">
|
||||||
|
允许覆盖现有账号
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="confirm-import-btn">确认导入</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 账号列表 -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<h5 class="mb-0">账号列表</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>邮箱</th>
|
||||||
|
<th>密码</th>
|
||||||
|
<th>TOKEN</th>
|
||||||
|
<th>高级使用</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>余量</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="accounts-tbody">
|
||||||
|
<!-- 账号数据将通过JavaScript动态填充 -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 重新设计分页控件 -->
|
||||||
|
<div class="pagination-container mt-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="pagination-info">
|
||||||
|
共 <span id="total-accounts" class="fw-bold text-primary">0</span> 个账号,
|
||||||
|
第 <span id="current-page" class="fw-bold">1</span>/<span id="total-pages">1</span> 页
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav aria-label="账号分页">
|
||||||
|
<ul class="pagination mb-0">
|
||||||
|
<li class="page-item" id="prev-page">
|
||||||
|
<a class="page-link" href="#" aria-label="上一页">
|
||||||
|
<i class="fas fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item page-number active"><a class="page-link" href="#" data-page="1">1</a>
|
||||||
|
</li>
|
||||||
|
<!-- 页码会动态生成 -->
|
||||||
|
<li class="page-item" id="next-page">
|
||||||
|
<a class="page-link" href="#" aria-label="下一页">
|
||||||
|
<i class="fas fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="per-page-selector">
|
||||||
|
<select id="per-page" class="form-select form-select-sm">
|
||||||
|
<option value="10">每页10条</option>
|
||||||
|
<option value="20">每页20条</option>
|
||||||
|
<option value="50">每页50条</option>
|
||||||
|
<option value="100">每页100条</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统配置页面 -->
|
||||||
|
<div id="system-config" class="page-content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">系统配置</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- 保留原有的配置表单 -->
|
||||||
|
<form id="config-form">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="browser-headless" class="form-label">浏览器无头模式</label>
|
||||||
|
<select id="browser-headless" class="form-control" disabled>
|
||||||
|
<option value="true">是 (无界面)</option>
|
||||||
|
<option value="false">否 (有界面)</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">True=无界面运行浏览器,False=显示浏览器界面</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="dynamic-useragent" class="form-label">动态User-Agent(先不要配置)</label>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch"
|
||||||
|
id="dynamic-useragent" disabled>
|
||||||
|
<label class="form-check-label"
|
||||||
|
for="dynamic-useragent">启用动态User-Agent</label>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">(建议不要)启用后将从预设的UA列表中随机选择,无需手动配置User-Agent</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="useragent-input-container">
|
||||||
|
<label for="browser-useragent" class="form-label">浏览器User-Agent</label>
|
||||||
|
<input type="text" id="browser-useragent" class="form-control" disabled>
|
||||||
|
<small class="text-muted">浏览器模拟的用户代理字符串</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="browser-path" class="form-label">浏览器路径 (可选)</label>
|
||||||
|
<input type="text" id="browser-path" class="form-control" disabled>
|
||||||
|
<small class="text-muted">Windows下浏览器可执行文件的完整路径(示例:C:\Users\Administrator\AppData\Local\Google\Chrome\Bin\chrome.exe)</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="accounts-limit" class="form-label">最大账号数量</label>
|
||||||
|
<input type="number" id="accounts-limit" class="form-control" min="1"
|
||||||
|
disabled>
|
||||||
|
<small class="text-muted">系统允许创建的最大账号数量</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 验证码获取方式 -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="captcha-method" class="form-label">验证码获取方式</label>
|
||||||
|
<select id="captcha-method" class="form-control" disabled>
|
||||||
|
<option value="API">API自动获取</option>
|
||||||
|
<option value="INPUT">控制台手动输入</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">选择验证码的获取方式:API自动获取或通过控制台手动输入</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email-type" class="form-label">邮箱类型</label>
|
||||||
|
<select id="email-type" class="form-control" disabled>
|
||||||
|
<option value="tempemail">TempEmail(https://tempmail.plus/)</option>
|
||||||
|
<option value="zmail">ZMail(https://zmail.plus/)</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">选择用于注册的临时邮箱服务类型</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email-domains" class="form-label">邮箱域名</label>
|
||||||
|
<input type="text" id="email-domains" class="form-control" disabled>
|
||||||
|
<small class="text-muted">用于注册的邮箱域名,多个域名用逗号分隔</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TempEmail相关字段 -->
|
||||||
|
<div id="tempemail-fields">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email-username" class="form-label">临时邮箱用户名</label>
|
||||||
|
<input type="text" id="email-username" class="form-control" disabled
|
||||||
|
placeholder="加载中...">
|
||||||
|
<small class="text-muted">用于接收验证码的临时邮箱用户名(不需要输入@及后面的部分)</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email-pin" class="form-label">临时邮箱PIN</label>
|
||||||
|
<input type="text" id="email-pin" class="form-control" disabled>
|
||||||
|
<small class="text-muted">临时邮箱PIN码(如果需要)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ZMail相关字段 -->
|
||||||
|
<div id="zmail-fields" style="display: none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email-api" class="form-label">邮箱API</label>
|
||||||
|
<input type="text" id="email-api" class="form-control" disabled>
|
||||||
|
<small class="text-muted">ZMail API地址</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email-proxy-enabled" class="form-label">邮箱代理</label>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch"
|
||||||
|
id="email-proxy-enabled" disabled>
|
||||||
|
<label class="form-check-label"
|
||||||
|
for="email-proxy-enabled">启用邮箱代理</label>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">是否启用邮箱API代理</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="email-proxy-address-container">
|
||||||
|
<label for="email-proxy-address" class="form-label">邮箱代理地址</label>
|
||||||
|
<input type="text" id="email-proxy-address" class="form-control"
|
||||||
|
disabled>
|
||||||
|
<small class="text-muted">邮箱API代理服务器地址</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="cursor-path" class="form-label">Cursor路径 (可选)</label>
|
||||||
|
<input type="text" id="cursor-path" class="form-control" disabled>
|
||||||
|
<small class="text-muted">Cursor安装目录的完整路径(示例:D:\devtools\cursor)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 在系统配置表单中添加代理设置部分 -->
|
||||||
|
<div class="config-section card mb-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0">代理服务器设置</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check form-switch mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch"
|
||||||
|
id="use-proxy" disabled name="USE_PROXY">
|
||||||
|
<label class="form-check-label" for="use-proxy">启用代理服务器</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="proxy-settings" class="mb-3">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="proxy-type" class="form-label">代理类型</label>
|
||||||
|
<select class="form-select" id="proxy-type" disabled
|
||||||
|
name="PROXY_TYPE">
|
||||||
|
<option value="http">HTTP</option>
|
||||||
|
<option value="https">HTTPS</option>
|
||||||
|
<option value="socks4">SOCKS4</option>
|
||||||
|
<option value="socks5">SOCKS5</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="proxy-host" class="form-label">代理服务器地址</label>
|
||||||
|
<input type="text" class="form-control" id="proxy-host" disabled
|
||||||
|
name="PROXY_HOST" placeholder="例如: 127.0.0.1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="proxy-port" class="form-label">代理服务器端口</label>
|
||||||
|
<input type="number" class="form-control" id="proxy-port"
|
||||||
|
disabled name="PROXY_PORT" placeholder="例如: 7890">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="proxy-timeout" class="form-label">超时时间 (秒)</label>
|
||||||
|
<input type="number" class="form-control" id="proxy-timeout"
|
||||||
|
disabled name="PROXY_TIMEOUT" value="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="proxy-username" class="form-label">用户名 (可选)</label>
|
||||||
|
<input type="text" class="form-control" id="proxy-username"
|
||||||
|
disabled name="PROXY_USERNAME" placeholder="认证用户名">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="proxy-password" class="form-label">密码 (可选)</label>
|
||||||
|
<input type="password" class="form-control" id="proxy-password"
|
||||||
|
disabled name="PROXY_PASSWORD" placeholder="认证密码">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 将按钮容器从text-end改为新的栅格布局 -->
|
||||||
|
<div class="row config-actions" id="config-actions" style="display:none">
|
||||||
|
<div class="col-6 pe-1">
|
||||||
|
<button type="button" id="cancel-config-btn"
|
||||||
|
class="btn btn-secondary w-100">取消</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 ps-1">
|
||||||
|
<button type="submit" id="save-config-btn"
|
||||||
|
class="btn btn-success w-100">保存配置</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<button id="edit-config-btn" class="btn btn-warning position-relative">
|
||||||
|
<i class="fas fa-edit me-1"></i> 编辑配置
|
||||||
|
<span class="position-absolute badge rounded-pill bg-danger">
|
||||||
|
<i class="fas fa-pencil-alt fa-xs"></i>
|
||||||
|
<span class="visually-hidden">编辑提示</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 在系统配置区域添加重置机器ID按钮 -->
|
||||||
|
<div class="config-section card mb-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0">系统维护</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<button id="restart-service-btn" class="btn btn-primary w-100">
|
||||||
|
<i class="fas fa-sync-alt me-2"></i>重启服务
|
||||||
|
</button>
|
||||||
|
<p class="text-muted small mt-1">某些配置更改需要重启服务才能生效</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<button id="reset-machine-btn" class="btn btn-danger w-100">
|
||||||
|
<i class="fas fa-microchip me-2"></i>重置机器ID(慎用,实测切换了一下账号直接被Cursor封了。。。)
|
||||||
|
</button>
|
||||||
|
<p class="text-muted small mt-1">解决设备绑定或配额限制问题</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 删除确认对话框 -->
|
||||||
|
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="deleteConfirmModalLabel">确认删除</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>确定要删除此账号吗?此操作不可恢复。</p>
|
||||||
|
<p class="text-danger fw-bold">邮箱: <span id="deleteEmailConfirm"></span></p>
|
||||||
|
<p class="text-muted">ID: <span id="deleteIdConfirm"></span></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Token查看模态框 -->
|
||||||
|
<div class="modal fade" id="tokenViewModal" tabindex="-1" aria-labelledby="tokenViewModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="tokenViewModalLabel">查看Token</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tokenFullText" class="form-label">完整Token:</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<textarea id="tokenFullText" class="form-control" rows="5" readonly></textarea>
|
||||||
|
<button class="btn btn-outline-primary copy-btn" type="button" id="copyTokenBtn"
|
||||||
|
title="复制Token">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">此Token用于Cursor客户端身份验证,请妥善保管</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="resetMachineIdCheck">
|
||||||
|
<label class="form-check-label" for="resetMachineIdCheck">
|
||||||
|
同时重置机器ID
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary" id="useTokenBtn" data-account-id="">
|
||||||
|
<i class="fas fa-plug"></i> 使用此Token
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 账号使用记录模态框 -->
|
||||||
|
<div class="modal fade" id="usageRecordModal" tabindex="-1" aria-labelledby="usageRecordModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="usageRecordModalLabel">账号使用记录</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>账号邮箱:</strong> <span id="recordEmail"></span>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-sm align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>使用时间</th>
|
||||||
|
<th>使用者IP</th>
|
||||||
|
<th>使用者UA</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="usageRecordBody">
|
||||||
|
<!-- 记录将被动态填充 -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="no-records" class="text-center p-4" style="display: none;">
|
||||||
|
<i class="fas fa-history fa-2x text-muted mb-2"></i>
|
||||||
|
<p>暂无使用记录</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自动刷新指示器 -->
|
||||||
|
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 5">
|
||||||
|
<div class="d-flex align-items-center small text-muted">
|
||||||
|
<span class="me-2">自动刷新(60秒/次)</span>
|
||||||
|
<div class="spinner-border spinner-border-sm text-secondary" role="status"
|
||||||
|
style="width: 0.8rem; height: 0.8rem;">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="static/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- jQuery -->
|
||||||
|
<script src="static/js/jquery-3.6.0.min.js"></script>
|
||||||
|
<!-- 原有业务逻辑 JS -->
|
||||||
|
<script src="static/js/app.js"></script>
|
||||||
|
<!-- 添加菜单交互 JS -->
|
||||||
|
<script src="static/js/menu.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
32
logger.py
Normal file
32
logger.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from config import LOG_LEVEL, LOG_FORMAT, LOG_DATE_FORMAT
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, LOG_LEVEL),
|
||||||
|
format=LOG_FORMAT,
|
||||||
|
datefmt=LOG_DATE_FORMAT,
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
logging.FileHandler("app.log", encoding="utf-8"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
def info(message):
|
||||||
|
logger.info(message)
|
||||||
|
|
||||||
|
|
||||||
|
def warning(message):
|
||||||
|
logger.warning(message)
|
||||||
|
|
||||||
|
|
||||||
|
def error(message):
|
||||||
|
logger.error(message)
|
||||||
|
|
||||||
|
|
||||||
|
def debug(message):
|
||||||
|
logger.debug(message)
|
32
migrate_add_id.py
Normal file
32
migrate_add_id.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from sqlalchemy import select
|
||||||
|
from database import init_db, get_session, AccountModel
|
||||||
|
|
||||||
|
|
||||||
|
async def migrate_add_id():
|
||||||
|
"""为现有记录添加ID字段"""
|
||||||
|
await init_db()
|
||||||
|
|
||||||
|
async with get_session() as session:
|
||||||
|
# 查询所有没有id的记录
|
||||||
|
result = await session.execute(
|
||||||
|
select(AccountModel).where(AccountModel.id == None)
|
||||||
|
)
|
||||||
|
accounts = result.scalars().all()
|
||||||
|
|
||||||
|
print(f"找到 {len(accounts)} 条需要更新的记录")
|
||||||
|
|
||||||
|
# 为每条记录添加id
|
||||||
|
for i, account in enumerate(accounts):
|
||||||
|
# 生成基于索引的间隔时间戳,避免所有记录使用同一时间戳
|
||||||
|
timestamp_ms = int(time.time() * 1000) - (len(accounts) - i) * 1000
|
||||||
|
account.id = timestamp_ms
|
||||||
|
print(f"更新记录 {account.email} 的ID为 {timestamp_ms}")
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
print("迁移完成")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(migrate_add_id())
|
12
requirements.txt
Normal file
12
requirements.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
DrissionPage==4.1.0.9
|
||||||
|
psutil==6.1.0
|
||||||
|
fastapi==0.109.0
|
||||||
|
uvicorn==0.27.0
|
||||||
|
pydantic==2.5.3
|
||||||
|
sqlalchemy==2.0.25
|
||||||
|
asyncpg==0.29.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
playwright==1.41.2
|
||||||
|
aiosqlite==0.21.0
|
||||||
|
fake-useragent==2.1.0
|
||||||
|
python-multipart
|
133
reset_machine.py
Normal file
133
reset_machine.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
from colorama import Fore, Style, init
|
||||||
|
|
||||||
|
# 初始化colorama
|
||||||
|
init()
|
||||||
|
|
||||||
|
# 定义emoji和颜色常量
|
||||||
|
EMOJI = {
|
||||||
|
"FILE": "📄",
|
||||||
|
"BACKUP": "💾",
|
||||||
|
"SUCCESS": "✅",
|
||||||
|
"ERROR": "❌",
|
||||||
|
"INFO": "ℹ️",
|
||||||
|
"RESET": "🔄",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MachineIDResetter:
|
||||||
|
def __init__(self):
|
||||||
|
# 判断操作系统
|
||||||
|
if sys.platform == "win32": # Windows
|
||||||
|
appdata = os.getenv("APPDATA")
|
||||||
|
if appdata is None:
|
||||||
|
raise EnvironmentError("APPDATA 环境变量未设置")
|
||||||
|
self.db_path = os.path.join(
|
||||||
|
appdata, "Cursor", "User", "globalStorage", "storage.json"
|
||||||
|
)
|
||||||
|
elif sys.platform == "darwin": # macOS
|
||||||
|
self.db_path = os.path.abspath(
|
||||||
|
os.path.expanduser(
|
||||||
|
"~/Library/Application Support/Cursor/User/globalStorage/storage.json"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif sys.platform == "linux": # Linux 和其他类Unix系统
|
||||||
|
self.db_path = os.path.abspath(
|
||||||
|
os.path.expanduser("~/.config/Cursor/User/globalStorage/storage.json")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f"不支持的操作系统: {sys.platform}")
|
||||||
|
|
||||||
|
def generate_new_ids(self):
|
||||||
|
"""生成新的机器ID"""
|
||||||
|
# 生成新的UUID
|
||||||
|
dev_device_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# 生成新的machineId (64个字符的十六进制)
|
||||||
|
machine_id = hashlib.sha256(os.urandom(32)).hexdigest()
|
||||||
|
|
||||||
|
# 生成新的macMachineId (128个字符的十六进制)
|
||||||
|
mac_machine_id = hashlib.sha512(os.urandom(64)).hexdigest()
|
||||||
|
|
||||||
|
# 生成新的sqmId
|
||||||
|
sqm_id = "{" + str(uuid.uuid4()).upper() + "}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"telemetry.devDeviceId": dev_device_id,
|
||||||
|
"telemetry.macMachineId": mac_machine_id,
|
||||||
|
"telemetry.machineId": machine_id,
|
||||||
|
"telemetry.sqmId": sqm_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset_machine_ids(self):
|
||||||
|
"""重置机器ID并备份原文件"""
|
||||||
|
try:
|
||||||
|
print(f"{Fore.CYAN}{EMOJI['INFO']} 正在检查配置文件...{Style.RESET_ALL}")
|
||||||
|
|
||||||
|
# 检查文件是否存在
|
||||||
|
if not os.path.exists(self.db_path):
|
||||||
|
print(
|
||||||
|
f"{Fore.RED}{EMOJI['ERROR']} 配置文件不存在: {self.db_path}{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查文件权限
|
||||||
|
if not os.access(self.db_path, os.R_OK | os.W_OK):
|
||||||
|
print(
|
||||||
|
f"{Fore.RED}{EMOJI['ERROR']} 无法读写配置文件,请检查文件权限!{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"{Fore.RED}{EMOJI['ERROR']} 如果你使用过 go-cursor-help 来修改 ID; 请修改文件只读权限 {self.db_path} {Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 读取现有配置
|
||||||
|
print(f"{Fore.CYAN}{EMOJI['FILE']} 读取当前配置...{Style.RESET_ALL}")
|
||||||
|
with open(self.db_path, "r", encoding="utf-8") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# 生成新的ID
|
||||||
|
print(f"{Fore.CYAN}{EMOJI['RESET']} 生成新的机器标识...{Style.RESET_ALL}")
|
||||||
|
new_ids = self.generate_new_ids()
|
||||||
|
|
||||||
|
# 更新配置
|
||||||
|
config.update(new_ids)
|
||||||
|
|
||||||
|
# 保存新配置
|
||||||
|
print(f"{Fore.CYAN}{EMOJI['FILE']} 保存新配置...{Style.RESET_ALL}")
|
||||||
|
with open(self.db_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(config, f, indent=4)
|
||||||
|
|
||||||
|
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} 机器标识重置成功!{Style.RESET_ALL}")
|
||||||
|
print(f"\n{Fore.CYAN}新的机器标识:{Style.RESET_ALL}")
|
||||||
|
for key, value in new_ids.items():
|
||||||
|
print(f"{EMOJI['INFO']} {key}: {Fore.GREEN}{value}{Style.RESET_ALL}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except PermissionError as e:
|
||||||
|
print(f"{Fore.RED}{EMOJI['ERROR']} 权限错误: {str(e)}{Style.RESET_ALL}")
|
||||||
|
print(
|
||||||
|
f"{Fore.YELLOW}{EMOJI['INFO']} 请尝试以管理员身份运行此程序{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{Fore.RED}{EMOJI['ERROR']} 重置过程出错: {str(e)}{Style.RESET_ALL}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"\n{Fore.CYAN}{'=' * 50}{Style.RESET_ALL}")
|
||||||
|
print(f"{Fore.CYAN}{EMOJI['RESET']} Cursor 机器标识重置工具{Style.RESET_ALL}")
|
||||||
|
print(f"{Fore.CYAN}{'=' * 50}{Style.RESET_ALL}")
|
||||||
|
|
||||||
|
resetter = MachineIDResetter()
|
||||||
|
resetter.reset_machine_ids()
|
||||||
|
|
||||||
|
print(f"\n{Fore.CYAN}{'=' * 50}{Style.RESET_ALL}")
|
||||||
|
input(f"{EMOJI['INFO']} 按回车键退出...")
|
3
restart.sh
Normal file
3
restart.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
sleep 1
|
||||||
|
/Library/Frameworks/Python.framework/Versions/3.10/bin/python3 /Users/catdd/Desktop/work/ai/cursor-auto-register/api.py > restart.log 2>&1 &
|
7053
static/css/all.min.css
vendored
Normal file
7053
static/css/all.min.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
0
static/css/animate.min.css
vendored
Normal file
0
static/css/animate.min.css
vendored
Normal file
13181
static/css/bootstrap.min.css
vendored
Normal file
13181
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
315
static/css/css2
Normal file
315
static/css/css2
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
991
static/css/styles.css
Normal file
991
static/css/styles.css
Normal file
@ -0,0 +1,991 @@
|
|||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
color: #212529;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 组合相似的选择器,减少重复 */
|
||||||
|
.card, .table, .alert {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化按钮相关样式 */
|
||||||
|
.btn {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化按钮波纹效果 */
|
||||||
|
.btn::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
opacity: 0;
|
||||||
|
border-radius: 100%;
|
||||||
|
transform: scale(1) translate(-50%, -50%);
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus:not(:active)::after {
|
||||||
|
animation: ripple 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple {
|
||||||
|
0% { transform: scale(0); opacity: 0.6; }
|
||||||
|
100% { transform: scale(25); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化表格样式 */
|
||||||
|
.table {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td, .table th {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 简化表格悬停效果 */
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background-color: rgba(241, 245, 249, 0.7);
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 合并加载遮罩样式 */
|
||||||
|
.loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: none;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay.show {
|
||||||
|
display: flex;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-container {
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(120deg, #4f46e5, #7c3aed);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #1f2937;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.35em 0.6em;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-column .btn {
|
||||||
|
margin: 0.15rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-column {
|
||||||
|
max-width: 250px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-cell, .token-cell {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
margin-left: 0.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fireworks-canvas {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#alert-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10000;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 10px;
|
||||||
|
border-left: 4px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
border-left-color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-alert {
|
||||||
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #6c757d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整卡片内部间距,使内容更紧凑 */
|
||||||
|
.card-body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整统计图表容器高度 */
|
||||||
|
#usage-chart-container {
|
||||||
|
height: 200px; /* 从250px减小到200px */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整任务控制详情部分 */
|
||||||
|
#task-details .card {
|
||||||
|
margin-bottom: 0.5rem; /* 减小底部边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
#task-details .card .card-body {
|
||||||
|
padding: 0.5rem; /* 减小内部填充 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整操作按钮大小和间距 */
|
||||||
|
.task-control-buttons .btn {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整任务状态信息间距 */
|
||||||
|
.task-status-info {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整图表大小 */
|
||||||
|
#usage-chart {
|
||||||
|
max-height: 160px; /* 从200px减小到160px */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 使用情况进度条样式更新 */
|
||||||
|
.usage-progress-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 使用情况列样式优化 */
|
||||||
|
.usage-info {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-numbers {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.used-count {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-count {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remaining-count {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #28a745;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修改进度条背景为灰色 */
|
||||||
|
.usage-progress {
|
||||||
|
height: 8px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修改进度条填充颜色为浅红色 */
|
||||||
|
.usage-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #ff8a8a, #ffb3b3);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整表格列宽 */
|
||||||
|
.usage-column {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-column {
|
||||||
|
max-width: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 高亮显示当前行 */
|
||||||
|
.table tbody tr.active {
|
||||||
|
background-color: rgba(79, 70, 229, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整使用量查询按钮 */
|
||||||
|
.get-usage-btn {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保留操作列样式,但可以调整 */
|
||||||
|
.operation-column {
|
||||||
|
min-width: 120px; /* 由于按钮组,宽度需要调整 */
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题和面板间距优化 */
|
||||||
|
h2, h3, h4, h5 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加鼠标悬停手形光标效果 */
|
||||||
|
.copy-btn,
|
||||||
|
.toggle-password,
|
||||||
|
.toggle-token,
|
||||||
|
.toggle-username,
|
||||||
|
.btn,
|
||||||
|
.status-action,
|
||||||
|
.get-usage-btn,
|
||||||
|
.delete-account-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 为复制和显示/隐藏图标添加悬停效果 */
|
||||||
|
.copy-btn:hover,
|
||||||
|
.toggle-password:hover,
|
||||||
|
.toggle-token:hover,
|
||||||
|
.toggle-username:hover {
|
||||||
|
color: #4f46e5;
|
||||||
|
transform: scale(1.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加额度进度条样式 */
|
||||||
|
.battery-progress {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
/* background-color: #e9ecef; */
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
/* border: 1px solid #dee2e6; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 电池内部格子线 */
|
||||||
|
.battery-grid {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 电池格子 */
|
||||||
|
.battery-cell {
|
||||||
|
flex: 1;
|
||||||
|
height: 2px;
|
||||||
|
border-right: 1px solid rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.battery-cell:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已使用的部分 - 浅红色 */
|
||||||
|
.battery-used {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #ff8a8a, #ffb3b3);
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 额度信息文本 */
|
||||||
|
.usage-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
text-shadow: 0 0 2px rgba(255,255,255,0.7);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 使用量显示样式 */
|
||||||
|
.usage-display {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-text {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
height: 8px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: transparent !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义颜色 */
|
||||||
|
.bg-danger-soft {
|
||||||
|
background-color: #ffcccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-success-soft {
|
||||||
|
background-color: #d1ffd1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新的额度显示样式 - 参考图片精确复刻 */
|
||||||
|
.usage-bar-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-bar {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 15px;
|
||||||
|
background-color: #e8f7e8; /* 浅绿背景 */
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-right: 15px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-bar-used {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #ffcccc; /* 浅红色 */
|
||||||
|
border-radius: 10px 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-percent {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 55px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 使用情况列样式 */
|
||||||
|
.usage-info {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-numbers {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.used-count {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-count {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remaining-count {
|
||||||
|
color: #28a745;
|
||||||
|
font-size: 0.85em;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 电池式进度指示器 - 修改颜色逻辑 */
|
||||||
|
.battery-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.battery-bars {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.battery-bars::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -3px;
|
||||||
|
height: 8px;
|
||||||
|
width: 3px;
|
||||||
|
background-color: #adb5bd;
|
||||||
|
border-radius: 2px 0 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.battery-bar {
|
||||||
|
width: 6px;
|
||||||
|
height: 14px;
|
||||||
|
background-color: rgba(40, 167, 69, 0.2); /* 默认未使用为浅绿色 */
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 1px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 根据进度点亮电池条 - 反转逻辑,显示已用部分 */
|
||||||
|
.battery-progress[data-percent="0"] .battery-bar:nth-child(n) {
|
||||||
|
background-color: rgba(40, 167, 69, 0.2); /* 全未用 - 全部浅绿色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已使用部分为浅红色 */
|
||||||
|
.battery-progress[data-percent="10"] .battery-bar:nth-child(1),
|
||||||
|
.battery-progress[data-percent="20"] .battery-bar:nth-child(-n+2),
|
||||||
|
.battery-progress[data-percent="30"] .battery-bar:nth-child(-n+3),
|
||||||
|
.battery-progress[data-percent="40"] .battery-bar:nth-child(-n+4),
|
||||||
|
.battery-progress[data-percent="50"] .battery-bar:nth-child(-n+5),
|
||||||
|
.battery-progress[data-percent="60"] .battery-bar:nth-child(-n+6),
|
||||||
|
.battery-progress[data-percent="70"] .battery-bar:nth-child(-n+7),
|
||||||
|
.battery-progress[data-percent="80"] .battery-bar:nth-child(-n+8),
|
||||||
|
.battery-progress[data-percent="90"] .battery-bar:nth-child(-n+9),
|
||||||
|
.battery-progress[data-percent="100"] .battery-bar:nth-child(-n+10) {
|
||||||
|
background-color: rgba(220, 53, 69, 0.2); /* 浅红色表示已使用 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除旧的根据进度值设置颜色的规则,因为现在使用统一的红绿对比 */
|
||||||
|
.battery-percent {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6c757d;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
#registration-status {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加侧边栏菜单样式 */
|
||||||
|
.app-layout {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
width: 250px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-right: 1px solid #dee2e6;
|
||||||
|
height: 100vh;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
list-style: none;
|
||||||
|
padding: 15px 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
color: #495057;
|
||||||
|
text-decoration: none;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover, .nav-link.active {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #0d6efd;
|
||||||
|
border-left-color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link i {
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 250px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式样式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#sidebar {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
margin-left: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header h4, .nav-link span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link i {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
#sidebar {
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏底部文案样式 */
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整侧边栏内容区域,为底部文案留出空间 */
|
||||||
|
.nav-menu {
|
||||||
|
padding-bottom: 60px; /* 确保内容不会被底部文案遮挡 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在响应式布局中隐藏/显示footer文本 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 10px 5px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.sidebar-footer span {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 更新编辑配置按钮样式 - 占满一行 */
|
||||||
|
#edit-config-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-config-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-config-btn:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整徽章位置,使其更加合理 */
|
||||||
|
#edit-config-btn .position-absolute {
|
||||||
|
top: -8px !important;
|
||||||
|
right: -8px !important;
|
||||||
|
left: auto !important;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片标题与按钮的对齐 */
|
||||||
|
.card-header.bg-white.d-flex {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配置按钮样式 */
|
||||||
|
.config-actions button {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cancel-config-btn {
|
||||||
|
background-color: #6c757d;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#cancel-config-btn:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#save-config-btn {
|
||||||
|
background-color: #198754;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#save-config-btn:hover {
|
||||||
|
background-color: #157347;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页控件样式优化 */
|
||||||
|
.pagination {
|
||||||
|
border-radius: 30px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||||
|
background-color: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-item:first-child .page-link,
|
||||||
|
.pagination .page-item:last-child .page-link {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-item .page-link {
|
||||||
|
border: none;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6c757d;
|
||||||
|
min-width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-item.active .page-link {
|
||||||
|
background-color: #4285f4;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 5px rgba(66, 133, 244, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-item .page-link:hover:not(.disabled) {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #4285f4;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-item.disabled .page-link {
|
||||||
|
color: #ccc;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页容器样式 */
|
||||||
|
.pagination-container {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.per-page-selector .form-select {
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.375rem 2rem 0.375rem 1rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加表头排序图标样式 */
|
||||||
|
.sortable {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable:after {
|
||||||
|
content: '\f0dc';
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable.asc:after {
|
||||||
|
content: '\f0de';
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable.desc:after {
|
||||||
|
content: '\f0dd';
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代理配置样式 */
|
||||||
|
#proxy-settings {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
margin-top: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:checked + .form-check-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代理类型下拉框样式 */
|
||||||
|
#proxy-type {
|
||||||
|
border-color: #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#proxy-type:focus {
|
||||||
|
border-color: #86b7fe;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 系统维护按钮样式 - 更新蓝色按钮样式 */
|
||||||
|
.config-section .btn-danger,
|
||||||
|
.config-section .btn-primary {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section .btn-primary {
|
||||||
|
background-color: #0b5ed7;
|
||||||
|
border-color: #0a58ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section .btn-danger:hover {
|
||||||
|
background-color: #dc2626;
|
||||||
|
box-shadow: 0 4px 8px rgba(220, 38, 38, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section .btn-primary:hover {
|
||||||
|
background-color: #0a4fbf;
|
||||||
|
box-shadow: 0 4px 8px rgba(11, 94, 215, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 系统维护卡片强调 */
|
||||||
|
.config-section.card {
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 更新所有余量按钮样式 */
|
||||||
|
#update-all-usage {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#update-all-usage .fa-sync-alt {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#update-all-usage:active .fa-sync-alt {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮加载状态 */
|
||||||
|
.btn-loading {
|
||||||
|
position: relative;
|
||||||
|
pointer-events: none;
|
||||||
|
color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-loading:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
margin-top: -0.5em;
|
||||||
|
margin-left: -0.5em;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-right-color: transparent;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 更新余量按钮样式 */
|
||||||
|
.btn-purple {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #6f42c1;
|
||||||
|
border-color: #6610f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-purple:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #5a32a3;
|
||||||
|
border-color: #5a32a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-purple:focus {
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(111, 66, 193, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-purple:active {
|
||||||
|
background-color: #4c2888;
|
||||||
|
border-color: #4c2888;
|
||||||
|
}
|
1903
static/js/app.js
Normal file
1903
static/js/app.js
Normal file
File diff suppressed because it is too large
Load Diff
3881
static/js/bootstrap.bundle.min.js
vendored
Normal file
3881
static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4232
static/js/jquery-3.6.0.min.js
vendored
Normal file
4232
static/js/jquery-3.6.0.min.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
79
static/js/menu.js
Normal file
79
static/js/menu.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// 菜单项切换功能
|
||||||
|
$(document).ready(function() {
|
||||||
|
// 初始化页面导航
|
||||||
|
initNavigation();
|
||||||
|
|
||||||
|
// 菜单项点击事件
|
||||||
|
$('.nav-link').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 获取目标页面
|
||||||
|
const targetPage = $(this).data('page');
|
||||||
|
|
||||||
|
// 导航到该页面
|
||||||
|
navigateToPage(targetPage);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 响应式处理 - 移动设备上点击菜单后自动收起
|
||||||
|
if ($(window).width() <= 768) {
|
||||||
|
$('.nav-link').click(function() {
|
||||||
|
$('body').removeClass('sidebar-open');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加移动设备菜单切换按钮
|
||||||
|
$('<button id="sidebar-toggle" class="btn btn-sm btn-primary position-fixed" style="top: 10px; left: 10px; z-index: 1040;"><i class="fas fa-bars"></i></button>')
|
||||||
|
.appendTo('body')
|
||||||
|
.click(function() {
|
||||||
|
$('body').toggleClass('sidebar-open');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化导航函数
|
||||||
|
function initNavigation() {
|
||||||
|
// 检查URL中是否有哈希值
|
||||||
|
let targetPage = window.location.hash.substring(1); // 移除#符号
|
||||||
|
|
||||||
|
// 如果哈希值为空或无效,默认显示账号管理页面
|
||||||
|
if (!targetPage || !$('#' + targetPage).length) {
|
||||||
|
targetPage = 'tasks-accounts';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航到目标页面
|
||||||
|
navigateToPage(targetPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航到指定页面
|
||||||
|
function navigateToPage(pageId) {
|
||||||
|
// 切换活动菜单
|
||||||
|
$('.nav-link').removeClass('active');
|
||||||
|
$(`.nav-link[data-page="${pageId}"]`).addClass('active');
|
||||||
|
|
||||||
|
// 切换显示页面
|
||||||
|
$('.page-content').removeClass('active');
|
||||||
|
$('#' + pageId).addClass('active');
|
||||||
|
|
||||||
|
// 更新URL哈希值,但不触发页面滚动
|
||||||
|
if (history.pushState) {
|
||||||
|
history.pushState(null, null, '#' + pageId);
|
||||||
|
} else {
|
||||||
|
window.location.hash = pageId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 窗口大小变化时的响应式处理
|
||||||
|
$(window).resize(function() {
|
||||||
|
if ($(window).width() > 576) {
|
||||||
|
$('body').removeClass('sidebar-open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#email-type").change(function() {
|
||||||
|
if ($(this).val() === "tempemail") {
|
||||||
|
$("#tempemail-fields").show();
|
||||||
|
$("#zmail-fields").hide();
|
||||||
|
} else if ($(this).val() === "zmail") {
|
||||||
|
$("#tempemail-fields").hide();
|
||||||
|
$("#zmail-fields").show();
|
||||||
|
}
|
||||||
|
});
|
BIN
static/webfonts/fa-brands-400.ttf
Normal file
BIN
static/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
static/webfonts/fa-brands-400.woff2
Normal file
BIN
static/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
static/webfonts/fa-regular-400.ttf
Normal file
BIN
static/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
static/webfonts/fa-regular-400.woff2
Normal file
BIN
static/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
static/webfonts/fa-solid-900.ttf
Normal file
BIN
static/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
static/webfonts/fa-solid-900.woff2
Normal file
BIN
static/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
static/webfonts/fa-v4compatibility.ttf
Normal file
BIN
static/webfonts/fa-v4compatibility.ttf
Normal file
Binary file not shown.
BIN
static/webfonts/fa-v4compatibility.woff2
Normal file
BIN
static/webfonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
56
tokenManager/cursor.py
Normal file
56
tokenManager/cursor.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class Cursor:
|
||||||
|
models = [
|
||||||
|
"claude-3-5-sonnet-20241022",
|
||||||
|
"claude-3-opus",
|
||||||
|
"claude-3.5-haiku",
|
||||||
|
"claude-3.5-sonnet",
|
||||||
|
"cursor-fast",
|
||||||
|
"cursor-small",
|
||||||
|
"deepseek-r1",
|
||||||
|
"deepseek-v3",
|
||||||
|
"gemini-2.0-flash-exp",
|
||||||
|
"gemini-2.0-flash-thinking-exp",
|
||||||
|
"gemini-exp-1206",
|
||||||
|
"gpt-3.5-turbo",
|
||||||
|
"gpt-4",
|
||||||
|
"gpt-4-turbo-2024-04-09",
|
||||||
|
"gpt-4o",
|
||||||
|
"gpt-4o-mini",
|
||||||
|
"o1",
|
||||||
|
"o1-mini",
|
||||||
|
"o1-preview",
|
||||||
|
"o3-mini",
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_remaining_balance(cls, user, token):
|
||||||
|
url = f"https://www.cursor.com/api/usage?user={user}"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cookie": f"WorkosCursorSessionToken={user}%3A%3A{token}",
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
usage = response.json().get("gpt-4", None)
|
||||||
|
if (
|
||||||
|
usage is None
|
||||||
|
or "maxRequestUsage" not in usage
|
||||||
|
or "numRequests" not in usage
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
return usage["maxRequestUsage"] - usage["numRequests"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_trial_remaining_days(cls, user, token):
|
||||||
|
url = "https://www.cursor.com/api/auth/stripe"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cookie": f"WorkosCursorSessionToken={user}%3A%3A{token}",
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
remaining_days = response.json().get("daysRemainingOnTrial", None)
|
||||||
|
return remaining_days
|
124
tokenManager/oneapi_cursor_cleaner.py
Normal file
124
tokenManager/oneapi_cursor_cleaner.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
#####################################
|
||||||
|
#
|
||||||
|
# If you meet 429 when running this script, please increase the `GLOBAL_API_RATE_LIMIT` in your Chat-API service.
|
||||||
|
# See more details in https://github.com/ai365vip/chat-api?tab=readme-ov-file#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F
|
||||||
|
#
|
||||||
|
#####################################
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
|
from .oneapi_manager import OneAPIManager
|
||||||
|
from .cursor import Cursor
|
||||||
|
|
||||||
|
|
||||||
|
def handle_oneapi_cursor_channel(
|
||||||
|
oneapi: OneAPIManager,
|
||||||
|
channel_id,
|
||||||
|
test_channel: bool,
|
||||||
|
disable_low_balance_channel: bool,
|
||||||
|
delete_low_balance_channel: bool,
|
||||||
|
low_balance_threshold=10,
|
||||||
|
):
|
||||||
|
if test_channel:
|
||||||
|
test_response = oneapi.test_channel(channel_id)
|
||||||
|
if test_response.status_code != 200:
|
||||||
|
print(
|
||||||
|
f"Fail to test channel {channel_id}. Status Code: {response.status_code}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
response = oneapi.get_channel(channel_id)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"Fail to get channel {channel_id}. Status Code: {response.status_code}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = response.json()["data"]
|
||||||
|
key = data["key"]
|
||||||
|
status = data["status"] # 1 for enable, 2 for disbale
|
||||||
|
test_time = data["test_time"]
|
||||||
|
response_time = data["response_time"]
|
||||||
|
remaining_balance = Cursor.get_remaining_balance(key)
|
||||||
|
remaining_days = Cursor.get_trial_remaining_days(key)
|
||||||
|
print(
|
||||||
|
f"[OneAPI] Channel {channel_id} Info: Balance = {remaining_balance}. Trial Remaining Days = {remaining_days}. Response Time = {response_time}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if None in [remaining_balance, remaining_days]:
|
||||||
|
print("[OneAPI] Invalid resposne")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if remaining_balance < low_balance_threshold or (
|
||||||
|
test_time != 0 and response_time < 1000
|
||||||
|
): # or remaining_days <= 0:
|
||||||
|
if delete_low_balance_channel:
|
||||||
|
response = oneapi.delete_channel(channel_id)
|
||||||
|
print(
|
||||||
|
f"[OneAPI] Delete Channel {channel_id}. Status Coue: {response.status_code}"
|
||||||
|
)
|
||||||
|
elif disable_low_balance_channel and status == 1:
|
||||||
|
response = oneapi.disable_channel(channel_id)
|
||||||
|
print(
|
||||||
|
f"[OneAPI] Disable Channel {channel_id}. Status Code: {response.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--oneapi_url", type=str, required=False, help="URL link for One-API website"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--oneapi_token", type=str, required=False, help="Token for One-API website"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--test_channel", default=False, type=lambda x: (str(x).lower() == "true")
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--disable_low_balance_accounts",
|
||||||
|
default=False,
|
||||||
|
type=lambda x: (str(x).lower() == "true"),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--delete_low_balance_accounts",
|
||||||
|
default=False,
|
||||||
|
type=lambda x: (str(x).lower() == "true"),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--max_workers",
|
||||||
|
type=int,
|
||||||
|
default=10,
|
||||||
|
help="How many workers in multi-threading",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
oneapi_url = args.oneapi_url
|
||||||
|
oneapi_token = args.oneapi_token
|
||||||
|
test_channel = args.test_channel
|
||||||
|
disable_low_balance_accounts = args.disable_low_balance_accounts
|
||||||
|
delete_low_balance_accounts = args.delete_low_balance_accounts
|
||||||
|
max_workers = args.max_workers
|
||||||
|
|
||||||
|
oneapi = OneAPIManager(oneapi_url, oneapi_token)
|
||||||
|
|
||||||
|
response_channels = oneapi.get_channels(0, 2147483647)
|
||||||
|
channels = response_channels.json()["data"]
|
||||||
|
channels_ids = [channel["id"] for channel in channels]
|
||||||
|
channels_ids = sorted(channels_ids, key=int)
|
||||||
|
print(f"[OneAPI] Channel Count: {len(channels_ids)}")
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
futures = [
|
||||||
|
executor.submit(
|
||||||
|
handle_oneapi_cursor_channel,
|
||||||
|
oneapi,
|
||||||
|
id,
|
||||||
|
test_channel,
|
||||||
|
disable_low_balance_accounts,
|
||||||
|
delete_low_balance_accounts,
|
||||||
|
)
|
||||||
|
for id in channels_ids
|
||||||
|
]
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
result = future.result()
|
92
tokenManager/oneapi_manager.py
Normal file
92
tokenManager/oneapi_manager.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class OneAPIManager:
|
||||||
|
def __init__(self, url, access_token):
|
||||||
|
self.base_url = url
|
||||||
|
self.access_token = access_token
|
||||||
|
self.headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": self.access_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_channel(self, id):
|
||||||
|
url = self.base_url + f"/api/channel/{id}"
|
||||||
|
|
||||||
|
response = requests.get(url, headers=self.headers)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_channels(self, page, pagesize):
|
||||||
|
url = self.base_url + f"/api/channel/?p={page}&page_size={pagesize}"
|
||||||
|
|
||||||
|
response = requests.get(url, headers=self.headers)
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Support multiple keys separated by '\n'
|
||||||
|
def add_channel(self, name, base_url, key, models, rate_limit_count=0):
|
||||||
|
url = self.base_url + "/api/channel"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"name": name,
|
||||||
|
"type": 1,
|
||||||
|
"key": key,
|
||||||
|
"openai_organization": "",
|
||||||
|
"base_url": base_url,
|
||||||
|
"other": "",
|
||||||
|
"model_mapping": "",
|
||||||
|
"status_code_mapping": "",
|
||||||
|
"headers": "",
|
||||||
|
"models": ",".join(models),
|
||||||
|
"auto_ban": 0,
|
||||||
|
"is_image_url_enabled": 0,
|
||||||
|
"model_test": models[0],
|
||||||
|
"tested_time": 0,
|
||||||
|
"priority": 0,
|
||||||
|
"weight": 0,
|
||||||
|
"groups": ["default"],
|
||||||
|
"proxy_url": "",
|
||||||
|
"region": "",
|
||||||
|
"sk": "",
|
||||||
|
"ak": "",
|
||||||
|
"project_id": "",
|
||||||
|
"client_id": "",
|
||||||
|
"client_secret": "",
|
||||||
|
"refresh_token": "",
|
||||||
|
"gcp_account": "",
|
||||||
|
"rate_limit_count": rate_limit_count,
|
||||||
|
"gemini_model": "",
|
||||||
|
"tags": "",
|
||||||
|
"rate_limited": rate_limit_count > 0,
|
||||||
|
"is_tools": False,
|
||||||
|
"claude_original_request": False,
|
||||||
|
"group": "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, json=data, headers=self.headers)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def delete_channel(self, id):
|
||||||
|
url = self.base_url + f"/api/channel/{id}"
|
||||||
|
|
||||||
|
response = requests.delete(url, headers=self.headers)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def enable_channel(self, id):
|
||||||
|
url = self.base_url + "/api/channel"
|
||||||
|
data = {"id": id, "status": 1}
|
||||||
|
|
||||||
|
response = requests.put(url, json=data, headers=self.headers)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def disable_channel(self, id):
|
||||||
|
url = self.base_url + "/api/channel"
|
||||||
|
data = {"id": id, "status": 2}
|
||||||
|
|
||||||
|
response = requests.put(url, json=data, headers=self.headers)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def test_channel(self, id, model=""):
|
||||||
|
url = self.base_url + f"/api/channel/test/{id}?model={model}"
|
||||||
|
|
||||||
|
response = requests.get(url, headers=self.headers)
|
||||||
|
return response
|
18
turnstilePatch/manifest.json
Normal file
18
turnstilePatch/manifest.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Turnstile Patcher",
|
||||||
|
"version": "2.1",
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"js": [
|
||||||
|
"./script.js"
|
||||||
|
],
|
||||||
|
"matches": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"run_at": "document_start",
|
||||||
|
"all_frames": true,
|
||||||
|
"world": "MAIN"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
1
turnstilePatch/readme.txt
Normal file
1
turnstilePatch/readme.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
12
turnstilePatch/script.js
Normal file
12
turnstilePatch/script.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
function getRandomInt(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// old method wouldn't work on 4k screens
|
||||||
|
|
||||||
|
let screenX = getRandomInt(800, 1200);
|
||||||
|
let screenY = getRandomInt(400, 600);
|
||||||
|
|
||||||
|
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: screenX });
|
||||||
|
|
||||||
|
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: screenY });
|
15
vercel.json
Normal file
15
vercel.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"builds": [
|
||||||
|
{
|
||||||
|
"src": "api.py",
|
||||||
|
"use": "@vercel/python"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"src": "/(.*)",
|
||||||
|
"dest": "api.py"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user