mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-07 05:32:48 +08:00
4
This commit is contained in:
288
README.md
Normal file
288
README.md
Normal file
@@ -0,0 +1,288 @@
|
||||
# OpenAI/Codex CLI 自动注册系统
|
||||
|
||||
一个功能完整的 OpenAI/Codex CLI 自动注册系统,支持多种邮箱服务、Web UI 管理界面和命令行操作。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 核心功能
|
||||
- **自动注册流程** - 完整的 OpenAI 账号自动注册,支持 OAuth 2.0 + PKCE 认证
|
||||
- **多种邮箱服务** - 支持 Tempmail.lol、Outlook IMAP、自定义域名邮箱
|
||||
- **Web UI 界面** - 现代化的 Web 管理界面,包含控制台日志输出
|
||||
- **账号管理** - 批量删除、导出(JSON/CSV)、刷新 Token、查看详情
|
||||
- **后台任务** - 异步执行注册任务,实时查看任务状态和日志
|
||||
- **SQLite 存储** - 轻量级数据库存储账号和配置信息
|
||||
|
||||
### 邮箱服务
|
||||
| 服务类型 | 描述 | 适用场景 |
|
||||
|---------|------|---------|
|
||||
| Tempmail.lol | 临时邮箱服务 | 快速测试、一次性注册 |
|
||||
| Outlook IMAP | Outlook 邮箱 IMAP 接收 | 长期使用的邮箱 |
|
||||
| 自定义域名 | 支持自定义域名邮箱 API | 企业级邮箱服务 |
|
||||
|
||||
### Web UI 页面
|
||||
- **注册页面** - 嵌入式控制台,实时显示注册日志
|
||||
- **账号管理** - 表格展示、批量操作、搜索过滤
|
||||
- **系统设置** - 代理配置、邮箱服务管理、注册参数、数据库管理
|
||||
|
||||
## 安装
|
||||
|
||||
### 环境要求
|
||||
- Python >= 3.10
|
||||
- pip 或 uv 包管理器
|
||||
|
||||
### 安装步骤
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone <repository-url>
|
||||
cd codex-register-v2
|
||||
|
||||
# 创建虚拟环境
|
||||
python -m venv .venv
|
||||
|
||||
# 激活虚拟环境
|
||||
# Windows
|
||||
.venv\Scripts\activate
|
||||
# Linux/macOS
|
||||
source .venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
pip install -e .
|
||||
|
||||
# 或使用 uv(推荐)
|
||||
uv sync
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方式一:Web UI(推荐)
|
||||
|
||||
```bash
|
||||
# 启动 Web UI
|
||||
python webui.py
|
||||
|
||||
# 或指定参数
|
||||
python webui.py --host 0.0.0.0 --port 8000 --debug
|
||||
|
||||
# 使用已安装的命令
|
||||
codex-webui
|
||||
```
|
||||
|
||||
访问 `http://localhost:8000` 即可使用 Web 界面。
|
||||
|
||||
### 方式二:命令行
|
||||
|
||||
```bash
|
||||
# 单次注册
|
||||
python cli.py --once
|
||||
|
||||
# 循环注册
|
||||
python cli.py --sleep-min 5 --sleep-max 30
|
||||
|
||||
# 使用代理
|
||||
python cli.py --proxy http://127.0.0.1:7890
|
||||
|
||||
# 使用已安装的命令
|
||||
codex-register --once
|
||||
```
|
||||
|
||||
### 命令行参数
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `--proxy` | 代理地址 | 无 |
|
||||
| `--once` | 只运行一次 | False |
|
||||
| `--sleep-min` | 循环模式最短等待秒数 | 5 |
|
||||
| `--sleep-max` | 循环模式最长等待秒数 | 30 |
|
||||
| `--log-level` | 日志级别 | INFO |
|
||||
| `--log-file` | 日志文件路径 | 无 |
|
||||
|
||||
## 配置
|
||||
|
||||
### 配置方式
|
||||
|
||||
配置优先级:环境变量 > `.env` 文件 > 默认值
|
||||
|
||||
创建 `.env` 文件:
|
||||
|
||||
```env
|
||||
# 应用配置
|
||||
DEBUG=false
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# 数据库
|
||||
DATABASE_URL=sqlite:///data/database.db
|
||||
|
||||
# Web UI
|
||||
WEBUI_HOST=0.0.0.0
|
||||
WEBUI_PORT=8000
|
||||
WEBUI_SECRET_KEY=your-secret-key
|
||||
|
||||
# 代理配置
|
||||
PROXY_ENABLED=false
|
||||
PROXY_TYPE=http
|
||||
PROXY_HOST=127.0.0.1
|
||||
PROXY_PORT=7890
|
||||
|
||||
# OpenAI OAuth
|
||||
OPENAI_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
|
||||
|
||||
# Tempmail 配置
|
||||
TEMPMAIL_BASE_URL=https://api.tempmail.lol/v2
|
||||
TEMPMAIL_TIMEOUT=30
|
||||
|
||||
# 自定义域名邮箱
|
||||
CUSTOM_DOMAIN_BASE_URL=https://mail.example.com/api
|
||||
CUSTOM_DOMAIN_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
### 主要配置项
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `database_url` | 数据库连接 URL | `data/database.db` |
|
||||
| `webui_host` | Web UI 监听地址 | `0.0.0.0` |
|
||||
| `webui_port` | Web UI 监听端口 | `8000` |
|
||||
| `proxy_enabled` | 是否启用代理 | `false` |
|
||||
| `proxy_url` | 代理地址 | - |
|
||||
| `registration_max_retries` | 注册最大重试次数 | `3` |
|
||||
| `registration_timeout` | 注册超时时间(秒) | `120` |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
codex-register-v2/
|
||||
├── cli.py # 命令行入口
|
||||
├── webui.py # Web UI 入口
|
||||
├── pyproject.toml # 项目配置
|
||||
├── requirements.txt # 依赖列表
|
||||
├── src/
|
||||
│ ├── __init__.py
|
||||
│ ├── config/ # 配置模块
|
||||
│ │ ├── settings.py # Pydantic 设置模型
|
||||
│ │ └── constants.py # 常量定义
|
||||
│ ├── core/ # 核心功能
|
||||
│ │ ├── oauth.py # OAuth 认证
|
||||
│ │ ├── register.py # 注册引擎
|
||||
│ │ ├── http_client.py # HTTP 客户端
|
||||
│ │ └── utils.py # 工具函数
|
||||
│ ├── database/ # 数据库模块
|
||||
│ │ ├── models.py # 数据模型
|
||||
│ │ ├── crud.py # CRUD 操作
|
||||
│ │ ├── session.py # 会话管理
|
||||
│ │ └── init_db.py # 数据库初始化
|
||||
│ ├── services/ # 邮箱服务
|
||||
│ │ ├── base.py # 基类和工厂
|
||||
│ │ ├── tempmail.py # Tempmail.lol 服务
|
||||
│ │ ├── outlook.py # Outlook IMAP 服务
|
||||
│ │ └── custom_domain.py # 自定义域名服务
|
||||
│ └── web/ # Web 模块
|
||||
│ ├── app.py # FastAPI 应用
|
||||
│ └── routes/ # API 路由
|
||||
│ ├── accounts.py # 账号管理
|
||||
│ ├── registration.py # 注册任务
|
||||
│ ├── settings.py # 系统设置
|
||||
│ └── email_services.py # 邮箱服务
|
||||
├── templates/ # Jinja2 模板
|
||||
│ ├── index.html # 注册页面
|
||||
│ ├── accounts.html # 账号管理
|
||||
│ └── settings.html # 系统设置
|
||||
├── static/ # 静态文件
|
||||
│ ├── css/style.css
|
||||
│ └── js/
|
||||
│ ├── app.js
|
||||
│ ├── accounts.js
|
||||
│ └── settings.js
|
||||
└── data/ # 数据目录
|
||||
└── database.db # SQLite 数据库
|
||||
```
|
||||
|
||||
## API 文档
|
||||
|
||||
### 账号管理 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/accounts` | 获取账号列表 |
|
||||
| GET | `/api/accounts/{id}` | 获取单个账号 |
|
||||
| DELETE | `/api/accounts/{id}` | 删除账号 |
|
||||
| POST | `/api/accounts/batch-delete` | 批量删除 |
|
||||
| GET | `/api/accounts/export` | 导出账号 |
|
||||
| POST | `/api/accounts/{id}/refresh` | 刷新 Token |
|
||||
|
||||
### 注册任务 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/registration/start` | 开始注册 |
|
||||
| GET | `/api/registration/status/{uuid}` | 任务状态 |
|
||||
| GET | `/api/registration/logs/{uuid}` | 任务日志 |
|
||||
| DELETE | `/api/registration/cancel/{uuid}` | 取消任务 |
|
||||
|
||||
### 设置 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/settings` | 获取设置 |
|
||||
| PUT | `/api/settings` | 更新设置 |
|
||||
| GET | `/api/settings/email-services` | 邮箱服务列表 |
|
||||
| POST | `/api/settings/email-services` | 创建邮箱服务 |
|
||||
|
||||
## 开发
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 安装开发依赖
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# 运行测试
|
||||
pytest
|
||||
```
|
||||
|
||||
### 代码风格
|
||||
|
||||
项目使用 Python 3.10+ 特性,包括类型注解、dataclass、Pydantic 模型等。
|
||||
|
||||
### 添加新的邮箱服务
|
||||
|
||||
1. 继承 `BaseEmailService` 类
|
||||
2. 实现必需的方法:`get_email()`, `wait_for_code()`, `get_status()`
|
||||
3. 在 `EmailServiceFactory` 中注册服务
|
||||
|
||||
```python
|
||||
from src.services import BaseEmailService, EmailServiceFactory, EmailServiceType
|
||||
|
||||
class MyEmailService(BaseEmailService):
|
||||
async def get_email(self) -> str:
|
||||
# 实现获取邮箱地址
|
||||
pass
|
||||
|
||||
async def wait_for_code(self, email: str, timeout: int = 300) -> str:
|
||||
# 实现等待验证码
|
||||
pass
|
||||
|
||||
# 注册服务
|
||||
EmailServiceFactory.register(EmailServiceType.CUSTOM, MyEmailService)
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Web 框架**: FastAPI + Uvicorn
|
||||
- **模板引擎**: Jinja2
|
||||
- **数据库**: SQLAlchemy + SQLite
|
||||
- **HTTP 客户端**: curl_cffi(支持浏览器指纹模拟)
|
||||
- **数据验证**: Pydantic
|
||||
- **认证**: OAuth 2.0 + PKCE
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 作者
|
||||
|
||||
Yasal
|
||||
|
||||
---
|
||||
|
||||
**注意**: 本工具仅供学习和研究目的,请勿用于违反 OpenAI 服务条款的活动。
|
||||
170
cli.py
Normal file
170
cli.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
命令行入口 - 保持向后兼容性
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from src.core.utils import setup_logging, get_data_dir
|
||||
from src.core.register import RegistrationEngine
|
||||
from src.services import EmailServiceFactory, EmailServiceType
|
||||
from src.database.init_db import initialize_database
|
||||
from src.config.settings import get_settings
|
||||
|
||||
|
||||
def setup_database():
|
||||
"""初始化数据库"""
|
||||
try:
|
||||
initialize_database()
|
||||
print("[Info] 数据库初始化完成")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[Error] 数据库初始化失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def create_tempmail_service(proxy_url: Optional[str] = None):
|
||||
"""创建 Tempmail 服务"""
|
||||
config = {
|
||||
"base_url": "https://api.tempmail.lol/v2",
|
||||
"timeout": 30,
|
||||
"max_retries": 3,
|
||||
"proxy_url": proxy_url,
|
||||
}
|
||||
|
||||
try:
|
||||
service = EmailServiceFactory.create(
|
||||
EmailServiceType.TEMPMAIL,
|
||||
config,
|
||||
name="tempmail_cli"
|
||||
)
|
||||
print("[Info] Tempmail 服务创建成功")
|
||||
return service
|
||||
except Exception as e:
|
||||
print(f"[Error] 创建 Tempmail 服务失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def run_registration(proxy: Optional[str] = None) -> Optional[dict]:
|
||||
"""
|
||||
执行一次注册流程
|
||||
|
||||
Args:
|
||||
proxy: 代理地址
|
||||
|
||||
Returns:
|
||||
注册结果字典,如果失败返回 None
|
||||
"""
|
||||
# 创建邮箱服务
|
||||
email_service = create_tempmail_service(proxy)
|
||||
if not email_service:
|
||||
return None
|
||||
|
||||
# 创建注册引擎
|
||||
engine = RegistrationEngine(
|
||||
email_service=email_service,
|
||||
proxy_url=proxy,
|
||||
callback_logger=lambda msg: print(msg)
|
||||
)
|
||||
|
||||
# 执行注册
|
||||
result = engine.run()
|
||||
|
||||
if result.success:
|
||||
# 保存到数据库
|
||||
engine.save_to_database(result)
|
||||
|
||||
# 保存到文件(保持向后兼容)
|
||||
try:
|
||||
t_data = {
|
||||
"id_token": result.id_token,
|
||||
"access_token": result.access_token,
|
||||
"refresh_token": result.refresh_token,
|
||||
"account_id": result.account_id,
|
||||
"last_refresh": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"email": result.email,
|
||||
"type": "codex",
|
||||
"expired": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") # 简化处理
|
||||
}
|
||||
|
||||
fname_email = result.email.replace("@", "_")
|
||||
file_name = f"token_{fname_email}_{int(time.time())}.json"
|
||||
|
||||
with open(file_name, "w", encoding="utf-8") as f:
|
||||
json.dump(t_data, f, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
print(f"[*] 成功! Token 已保存至: {file_name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Warning] 保存 Token 文件失败: {e}")
|
||||
|
||||
return result.to_dict()
|
||||
else:
|
||||
print(f"[-] 本次注册失败: {result.error_message}")
|
||||
return None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description="OpenAI 自动注册脚本 (重构版本)")
|
||||
parser.add_argument(
|
||||
"--proxy", default=None, help="代理地址,如 http://127.0.0.1:7890"
|
||||
)
|
||||
parser.add_argument("--once", action="store_true", help="只运行一次")
|
||||
parser.add_argument("--sleep-min", type=int, default=5, help="循环模式最短等待秒数")
|
||||
parser.add_argument(
|
||||
"--sleep-max", type=int, default=30, help="循环模式最长等待秒数"
|
||||
)
|
||||
parser.add_argument("--log-level", default="INFO", help="日志级别")
|
||||
parser.add_argument("--log-file", help="日志文件路径")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 配置日志
|
||||
setup_logging(
|
||||
log_level=args.log_level,
|
||||
log_file=args.log_file
|
||||
)
|
||||
|
||||
# 初始化数据库
|
||||
if not setup_database():
|
||||
return
|
||||
|
||||
# 参数验证
|
||||
sleep_min = max(1, args.sleep_min)
|
||||
sleep_max = max(sleep_min, args.sleep_max)
|
||||
|
||||
count = 0
|
||||
print("[Info] Yasal's Seamless OpenAI Auto-Registrar Started for ZJH (重构版本)")
|
||||
|
||||
while True:
|
||||
count += 1
|
||||
print(
|
||||
f"\n[{datetime.now().strftime('%H:%M:%S')}] >>> 开始第 {count} 次注册流程 <<<"
|
||||
)
|
||||
|
||||
try:
|
||||
result = run_registration(args.proxy)
|
||||
|
||||
if result:
|
||||
print(f"[*] 注册成功! 邮箱: {result.get('email')}")
|
||||
else:
|
||||
print("[-] 本次注册失败。")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Error] 发生未捕获异常: {e}")
|
||||
|
||||
if args.once:
|
||||
break
|
||||
|
||||
wait_time = random.randint(sleep_min, sleep_max)
|
||||
print(f"[*] 休息 {wait_time} 秒...")
|
||||
time.sleep(wait_time)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
33
pyproject.toml
Normal file
33
pyproject.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[project]
|
||||
name = "codex-register-v2"
|
||||
version = "0.1.0"
|
||||
description = "OpenAI/Codex CLI 自动注册系统"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"curl-cffi>=0.14.0",
|
||||
"fastapi>=0.100.0",
|
||||
"uvicorn>=0.23.0",
|
||||
"jinja2>=3.1.0",
|
||||
"python-multipart>=0.0.6",
|
||||
"pydantic>=2.0.0",
|
||||
"pydantic-settings>=2.0.0",
|
||||
"sqlalchemy>=2.0.0",
|
||||
"aiosqlite>=0.19.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"httpx>=0.24.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
codex-register = "cli:main"
|
||||
codex-webui = "webui:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src"]
|
||||
@@ -40,11 +40,13 @@ class RegistrationResult:
|
||||
"""注册结果"""
|
||||
success: bool
|
||||
email: str = ""
|
||||
password: str = "" # 注册密码
|
||||
account_id: str = ""
|
||||
workspace_id: str = ""
|
||||
access_token: str = ""
|
||||
refresh_token: str = ""
|
||||
id_token: str = ""
|
||||
session_token: str = "" # 会话令牌
|
||||
error_message: str = ""
|
||||
logs: list = None
|
||||
metadata: dict = None
|
||||
@@ -54,11 +56,13 @@ class RegistrationResult:
|
||||
return {
|
||||
"success": self.success,
|
||||
"email": self.email,
|
||||
"password": self.password,
|
||||
"account_id": self.account_id,
|
||||
"workspace_id": self.workspace_id,
|
||||
"access_token": self.access_token[:20] + "..." if self.access_token else "",
|
||||
"refresh_token": self.refresh_token[:20] + "..." if self.refresh_token else "",
|
||||
"id_token": self.id_token[:20] + "..." if self.id_token else "",
|
||||
"session_token": self.session_token[:20] + "..." if self.session_token else "",
|
||||
"error_message": self.error_message,
|
||||
"logs": self.logs or [],
|
||||
"metadata": self.metadata or {},
|
||||
@@ -107,9 +111,11 @@ class RegistrationEngine:
|
||||
|
||||
# 状态变量
|
||||
self.email: Optional[str] = None
|
||||
self.password: Optional[str] = None # 注册密码
|
||||
self.email_info: Optional[Dict[str, Any]] = None
|
||||
self.oauth_start: Optional[OAuthStart] = None
|
||||
self.session: Optional[cffi_requests.Session] = None
|
||||
self.session_token: Optional[str] = None # 会话令牌
|
||||
self.logs: list = []
|
||||
|
||||
def _log(self, message: str, level: str = "info"):
|
||||
@@ -268,6 +274,7 @@ class RegistrationEngine:
|
||||
try:
|
||||
# 生成密码
|
||||
password = self._generate_password()
|
||||
self.password = password # 保存密码到实例变量
|
||||
self._log(f"生成密码: {password}")
|
||||
|
||||
# 提交密码注册
|
||||
@@ -662,6 +669,14 @@ class RegistrationEngine:
|
||||
result.access_token = token_info.get("access_token", "")
|
||||
result.refresh_token = token_info.get("refresh_token", "")
|
||||
result.id_token = token_info.get("id_token", "")
|
||||
result.password = self.password or "" # 保存密码
|
||||
|
||||
# 尝试获取 session_token 从 cookie
|
||||
session_cookie = self.session.cookies.get("__Secure-next-auth.session-token")
|
||||
if session_cookie:
|
||||
self.session_token = session_cookie
|
||||
result.session_token = session_cookie
|
||||
self._log(f"获取到 Session Token")
|
||||
|
||||
# 17. 完成
|
||||
self._log("=" * 60)
|
||||
@@ -699,11 +714,17 @@ class RegistrationEngine:
|
||||
return False
|
||||
|
||||
try:
|
||||
# 获取默认 client_id
|
||||
settings = get_settings()
|
||||
|
||||
with get_db() as db:
|
||||
# 保存账户信息
|
||||
account = crud.create_account(
|
||||
db,
|
||||
email=result.email,
|
||||
password=result.password,
|
||||
client_id=settings.openai_client_id,
|
||||
session_token=result.session_token,
|
||||
email_service=self.email_service.service_type.value,
|
||||
email_service_id=self.email_info.get("service_id") if self.email_info else None,
|
||||
account_id=result.account_id,
|
||||
@@ -712,7 +733,7 @@ class RegistrationEngine:
|
||||
refresh_token=result.refresh_token,
|
||||
id_token=result.id_token,
|
||||
proxy_used=self.proxy_url,
|
||||
metadata=result.metadata
|
||||
extra_data=result.metadata
|
||||
)
|
||||
|
||||
self._log(f"账户已保存到数据库,ID: {account.id}")
|
||||
|
||||
332
src/core/token_refresh.py
Normal file
332
src/core/token_refresh.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
Token 刷新模块
|
||||
支持 Session Token 和 OAuth Refresh Token 两种刷新方式
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from curl_cffi import requests as cffi_requests
|
||||
|
||||
from ..config.settings import get_settings
|
||||
from ..database.session import get_db
|
||||
from ..database import crud
|
||||
from ..database.models import Account
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenRefreshResult:
|
||||
"""Token 刷新结果"""
|
||||
success: bool
|
||||
access_token: str = ""
|
||||
refresh_token: str = ""
|
||||
expires_at: Optional[datetime] = None
|
||||
error_message: str = ""
|
||||
|
||||
|
||||
class TokenRefreshManager:
|
||||
"""
|
||||
Token 刷新管理器
|
||||
支持两种刷新方式:
|
||||
1. Session Token 刷新(优先)
|
||||
2. OAuth Refresh Token 刷新
|
||||
"""
|
||||
|
||||
# OpenAI OAuth 端点
|
||||
SESSION_URL = "https://chatgpt.com/api/auth/session"
|
||||
TOKEN_URL = "https://auth.openai.com/oauth/token"
|
||||
|
||||
def __init__(self, proxy_url: Optional[str] = None):
|
||||
"""
|
||||
初始化 Token 刷新管理器
|
||||
|
||||
Args:
|
||||
proxy_url: 代理 URL
|
||||
"""
|
||||
self.proxy_url = proxy_url
|
||||
self.settings = get_settings()
|
||||
|
||||
def _create_session(self) -> cffi_requests.Session:
|
||||
"""创建 HTTP 会话"""
|
||||
session = cffi_requests.Session(impersonate="chrome120", proxy=self.proxy_url)
|
||||
return session
|
||||
|
||||
def refresh_by_session_token(self, session_token: str) -> TokenRefreshResult:
|
||||
"""
|
||||
使用 Session Token 刷新
|
||||
|
||||
Args:
|
||||
session_token: 会话令牌
|
||||
|
||||
Returns:
|
||||
TokenRefreshResult: 刷新结果
|
||||
"""
|
||||
result = TokenRefreshResult(success=False)
|
||||
|
||||
try:
|
||||
session = self._create_session()
|
||||
|
||||
# 设置会话 Cookie
|
||||
session.cookies.set(
|
||||
"__Secure-next-auth.session-token",
|
||||
session_token,
|
||||
domain=".chatgpt.com",
|
||||
path="/"
|
||||
)
|
||||
|
||||
# 请求会话端点
|
||||
response = session.get(
|
||||
self.SESSION_URL,
|
||||
headers={
|
||||
"accept": "application/json",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
result.error_message = f"Session token 刷新失败: HTTP {response.status_code}"
|
||||
logger.warning(result.error_message)
|
||||
return result
|
||||
|
||||
data = response.json()
|
||||
|
||||
# 提取 access_token
|
||||
access_token = data.get("accessToken")
|
||||
if not access_token:
|
||||
result.error_message = "Session token 刷新失败: 未找到 accessToken"
|
||||
logger.warning(result.error_message)
|
||||
return result
|
||||
|
||||
# 提取过期时间
|
||||
expires_at = None
|
||||
expires_str = data.get("expires")
|
||||
if expires_str:
|
||||
try:
|
||||
expires_at = datetime.fromisoformat(expires_str.replace("Z", "+00:00"))
|
||||
except:
|
||||
pass
|
||||
|
||||
result.success = True
|
||||
result.access_token = access_token
|
||||
result.expires_at = expires_at
|
||||
|
||||
logger.info(f"Session token 刷新成功,过期时间: {expires_at}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
result.error_message = f"Session token 刷新异常: {str(e)}"
|
||||
logger.error(result.error_message)
|
||||
return result
|
||||
|
||||
def refresh_by_oauth_token(
|
||||
self,
|
||||
refresh_token: str,
|
||||
client_id: Optional[str] = None
|
||||
) -> TokenRefreshResult:
|
||||
"""
|
||||
使用 OAuth Refresh Token 刷新
|
||||
|
||||
Args:
|
||||
refresh_token: OAuth 刷新令牌
|
||||
client_id: OAuth Client ID
|
||||
|
||||
Returns:
|
||||
TokenRefreshResult: 刷新结果
|
||||
"""
|
||||
result = TokenRefreshResult(success=False)
|
||||
|
||||
try:
|
||||
session = self._create_session()
|
||||
|
||||
# 使用配置的 client_id 或默认值
|
||||
client_id = client_id or self.settings.openai_client_id
|
||||
|
||||
# 构建请求体
|
||||
token_data = {
|
||||
"client_id": client_id,
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refresh_token,
|
||||
"redirect_uri": self.settings.openai_redirect_uri
|
||||
}
|
||||
|
||||
response = session.post(
|
||||
self.TOKEN_URL,
|
||||
headers={
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
"accept": "application/json"
|
||||
},
|
||||
data=token_data,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
result.error_message = f"OAuth token 刷新失败: HTTP {response.status_code}"
|
||||
logger.warning(f"{result.error_message}, 响应: {response.text[:200]}")
|
||||
return result
|
||||
|
||||
data = response.json()
|
||||
|
||||
# 提取令牌
|
||||
access_token = data.get("access_token")
|
||||
new_refresh_token = data.get("refresh_token", refresh_token)
|
||||
expires_in = data.get("expires_in", 3600)
|
||||
|
||||
if not access_token:
|
||||
result.error_message = "OAuth token 刷新失败: 未找到 access_token"
|
||||
logger.warning(result.error_message)
|
||||
return result
|
||||
|
||||
# 计算过期时间
|
||||
expires_at = datetime.utcnow() + timedelta(seconds=expires_in)
|
||||
|
||||
result.success = True
|
||||
result.access_token = access_token
|
||||
result.refresh_token = new_refresh_token
|
||||
result.expires_at = expires_at
|
||||
|
||||
logger.info(f"OAuth token 刷新成功,过期时间: {expires_at}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
result.error_message = f"OAuth token 刷新异常: {str(e)}"
|
||||
logger.error(result.error_message)
|
||||
return result
|
||||
|
||||
def refresh_account(self, account: Account) -> TokenRefreshResult:
|
||||
"""
|
||||
刷新账号的 Token
|
||||
|
||||
优先级:
|
||||
1. Session Token 刷新
|
||||
2. OAuth Refresh Token 刷新
|
||||
|
||||
Args:
|
||||
account: 账号对象
|
||||
|
||||
Returns:
|
||||
TokenRefreshResult: 刷新结果
|
||||
"""
|
||||
# 优先尝试 Session Token
|
||||
if account.session_token:
|
||||
logger.info(f"尝试使用 Session Token 刷新账号 {account.email}")
|
||||
result = self.refresh_by_session_token(account.session_token)
|
||||
if result.success:
|
||||
return result
|
||||
logger.warning(f"Session Token 刷新失败,尝试 OAuth 刷新")
|
||||
|
||||
# 尝试 OAuth Refresh Token
|
||||
if account.refresh_token:
|
||||
logger.info(f"尝试使用 OAuth Refresh Token 刷新账号 {account.email}")
|
||||
result = self.refresh_by_oauth_token(
|
||||
refresh_token=account.refresh_token,
|
||||
client_id=account.client_id
|
||||
)
|
||||
return result
|
||||
|
||||
# 无可用刷新方式
|
||||
return TokenRefreshResult(
|
||||
success=False,
|
||||
error_message="账号没有可用的刷新方式(缺少 session_token 和 refresh_token)"
|
||||
)
|
||||
|
||||
def validate_token(self, access_token: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
验证 Access Token 是否有效
|
||||
|
||||
Args:
|
||||
access_token: 访问令牌
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (是否有效, 错误信息)
|
||||
"""
|
||||
try:
|
||||
session = self._create_session()
|
||||
|
||||
# 调用 OpenAI API 验证 token
|
||||
response = session.get(
|
||||
"https://chatgpt.com/backend-api/me",
|
||||
headers={
|
||||
"authorization": f"Bearer {access_token}",
|
||||
"accept": "application/json"
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return True, None
|
||||
elif response.status_code == 401:
|
||||
return False, "Token 无效或已过期"
|
||||
elif response.status_code == 403:
|
||||
return False, "账号可能被封禁"
|
||||
else:
|
||||
return False, f"验证失败: HTTP {response.status_code}"
|
||||
|
||||
except Exception as e:
|
||||
return False, f"验证异常: {str(e)}"
|
||||
|
||||
|
||||
def refresh_account_token(account_id: int, proxy_url: Optional[str] = None) -> TokenRefreshResult:
|
||||
"""
|
||||
刷新指定账号的 Token 并更新数据库
|
||||
|
||||
Args:
|
||||
account_id: 账号 ID
|
||||
proxy_url: 代理 URL
|
||||
|
||||
Returns:
|
||||
TokenRefreshResult: 刷新结果
|
||||
"""
|
||||
with get_db() as db:
|
||||
account = crud.get_account_by_id(db, account_id)
|
||||
if not account:
|
||||
return TokenRefreshResult(success=False, error_message="账号不存在")
|
||||
|
||||
manager = TokenRefreshManager(proxy_url=proxy_url)
|
||||
result = manager.refresh_account(account)
|
||||
|
||||
if result.success:
|
||||
# 更新数据库
|
||||
update_data = {
|
||||
"access_token": result.access_token,
|
||||
"last_refresh": datetime.utcnow()
|
||||
}
|
||||
|
||||
if result.refresh_token:
|
||||
update_data["refresh_token"] = result.refresh_token
|
||||
|
||||
if result.expires_at:
|
||||
update_data["expires_at"] = result.expires_at
|
||||
|
||||
crud.update_account(db, account_id, **update_data)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def validate_account_token(account_id: int, proxy_url: Optional[str] = None) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
验证指定账号的 Token 是否有效
|
||||
|
||||
Args:
|
||||
account_id: 账号 ID
|
||||
proxy_url: 代理 URL
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (是否有效, 错误信息)
|
||||
"""
|
||||
with get_db() as db:
|
||||
account = crud.get_account_by_id(db, account_id)
|
||||
if not account:
|
||||
return False, "账号不存在"
|
||||
|
||||
if not account.access_token:
|
||||
return False, "账号没有 access_token"
|
||||
|
||||
manager = TokenRefreshManager(proxy_url=proxy_url)
|
||||
return manager.validate_token(account.access_token)
|
||||
@@ -7,7 +7,7 @@ from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, desc, asc, func
|
||||
|
||||
from .models import Account, EmailService, RegistrationTask, Setting
|
||||
from .models import Account, EmailService, RegistrationTask, Setting, Proxy
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -18,6 +18,9 @@ def create_account(
|
||||
db: Session,
|
||||
email: str,
|
||||
email_service: str,
|
||||
password: Optional[str] = None,
|
||||
client_id: Optional[str] = None,
|
||||
session_token: Optional[str] = None,
|
||||
email_service_id: Optional[str] = None,
|
||||
account_id: Optional[str] = None,
|
||||
workspace_id: Optional[str] = None,
|
||||
@@ -25,11 +28,15 @@ def create_account(
|
||||
refresh_token: Optional[str] = None,
|
||||
id_token: Optional[str] = None,
|
||||
proxy_used: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
expires_at: Optional['datetime'] = None,
|
||||
extra_data: Optional[Dict[str, Any]] = None
|
||||
) -> Account:
|
||||
"""创建新账户"""
|
||||
db_account = Account(
|
||||
email=email,
|
||||
password=password,
|
||||
client_id=client_id,
|
||||
session_token=session_token,
|
||||
email_service=email_service,
|
||||
email_service_id=email_service_id,
|
||||
account_id=account_id,
|
||||
@@ -38,7 +45,8 @@ def create_account(
|
||||
refresh_token=refresh_token,
|
||||
id_token=id_token,
|
||||
proxy_used=proxy_used,
|
||||
metadata=metadata or {},
|
||||
expires_at=expires_at,
|
||||
extra_data=extra_data or {},
|
||||
registered_at=datetime.utcnow()
|
||||
)
|
||||
db.add(db_account)
|
||||
@@ -369,4 +377,120 @@ def delete_setting(db: Session, key: str) -> bool:
|
||||
|
||||
db.delete(db_setting)
|
||||
db.commit()
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 代理 CRUD
|
||||
# ============================================================================
|
||||
|
||||
def create_proxy(
|
||||
db: Session,
|
||||
name: str,
|
||||
type: str,
|
||||
host: str,
|
||||
port: int,
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
enabled: bool = True,
|
||||
priority: int = 0
|
||||
) -> Proxy:
|
||||
"""创建代理配置"""
|
||||
db_proxy = Proxy(
|
||||
name=name,
|
||||
type=type,
|
||||
host=host,
|
||||
port=port,
|
||||
username=username,
|
||||
password=password,
|
||||
enabled=enabled,
|
||||
priority=priority
|
||||
)
|
||||
db.add(db_proxy)
|
||||
db.commit()
|
||||
db.refresh(db_proxy)
|
||||
return db_proxy
|
||||
|
||||
|
||||
def get_proxy_by_id(db: Session, proxy_id: int) -> Optional[Proxy]:
|
||||
"""根据 ID 获取代理"""
|
||||
return db.query(Proxy).filter(Proxy.id == proxy_id).first()
|
||||
|
||||
|
||||
def get_proxies(
|
||||
db: Session,
|
||||
enabled: Optional[bool] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[Proxy]:
|
||||
"""获取代理列表"""
|
||||
query = db.query(Proxy)
|
||||
|
||||
if enabled is not None:
|
||||
query = query.filter(Proxy.enabled == enabled)
|
||||
|
||||
query = query.order_by(desc(Proxy.created_at)).offset(skip).limit(limit)
|
||||
return query.all()
|
||||
|
||||
|
||||
def get_enabled_proxies(db: Session) -> List[Proxy]:
|
||||
"""获取所有启用的代理"""
|
||||
return db.query(Proxy).filter(Proxy.enabled == True).all()
|
||||
|
||||
|
||||
def update_proxy(
|
||||
db: Session,
|
||||
proxy_id: int,
|
||||
**kwargs
|
||||
) -> Optional[Proxy]:
|
||||
"""更新代理配置"""
|
||||
db_proxy = get_proxy_by_id(db, proxy_id)
|
||||
if not db_proxy:
|
||||
return None
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(db_proxy, key):
|
||||
setattr(db_proxy, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_proxy)
|
||||
return db_proxy
|
||||
|
||||
|
||||
def delete_proxy(db: Session, proxy_id: int) -> bool:
|
||||
"""删除代理配置"""
|
||||
db_proxy = get_proxy_by_id(db, proxy_id)
|
||||
if not db_proxy:
|
||||
return False
|
||||
|
||||
db.delete(db_proxy)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
def update_proxy_last_used(db: Session, proxy_id: int) -> bool:
|
||||
"""更新代理最后使用时间"""
|
||||
db_proxy = get_proxy_by_id(db, proxy_id)
|
||||
if not db_proxy:
|
||||
return False
|
||||
|
||||
db_proxy.last_used = datetime.utcnow()
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
def get_random_proxy(db: Session) -> Optional[Proxy]:
|
||||
"""随机获取一个启用的代理"""
|
||||
import random
|
||||
proxies = get_enabled_proxies(db)
|
||||
if not proxies:
|
||||
return None
|
||||
return random.choice(proxies)
|
||||
|
||||
|
||||
def get_proxies_count(db: Session, enabled: Optional[bool] = None) -> int:
|
||||
"""获取代理数量"""
|
||||
query = db.query(func.count(Proxy.id))
|
||||
if enabled is not None:
|
||||
query = query.filter(Proxy.enabled == enabled)
|
||||
return query.scalar()
|
||||
@@ -34,18 +34,20 @@ class Account(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
email = Column(String(255), nullable=False, unique=True, index=True)
|
||||
password_hash = Column(String(255))
|
||||
password = Column(String(255)) # 注册密码(明文存储)
|
||||
access_token = Column(Text)
|
||||
refresh_token = Column(Text)
|
||||
id_token = Column(Text)
|
||||
session_token = Column(Text) # 会话令牌(优先刷新方式)
|
||||
client_id = Column(String(255)) # OAuth Client ID
|
||||
account_id = Column(String(255))
|
||||
workspace_id = Column(String(255))
|
||||
email_service = Column(String(50), nullable=False) # 'tempmail', 'outlook', 'custom_domain'
|
||||
email_service_id = Column(String(255)) # 邮箱服务中的ID
|
||||
proxy_used = Column(String(255))
|
||||
registered_at = Column(DateTime, default=datetime.utcnow)
|
||||
last_refresh = Column(DateTime)
|
||||
expires_at = Column(DateTime)
|
||||
last_refresh = Column(DateTime) # 最后刷新时间
|
||||
expires_at = Column(DateTime) # Token 过期时间
|
||||
status = Column(String(20), default='active') # 'active', 'expired', 'banned', 'failed'
|
||||
extra_data = Column(JSONEncodedDict) # 额外信息存储
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
@@ -56,10 +58,14 @@ class Account(Base):
|
||||
return {
|
||||
'id': self.id,
|
||||
'email': self.email,
|
||||
'password': self.password,
|
||||
'client_id': self.client_id,
|
||||
'email_service': self.email_service,
|
||||
'account_id': self.account_id,
|
||||
'workspace_id': self.workspace_id,
|
||||
'registered_at': self.registered_at.isoformat() if self.registered_at else None,
|
||||
'last_refresh': self.last_refresh.isoformat() if self.last_refresh else None,
|
||||
'expires_at': self.expires_at.isoformat() if self.expires_at else None,
|
||||
'status': self.status,
|
||||
'proxy_used': self.proxy_used,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
@@ -110,4 +116,59 @@ class Setting(Base):
|
||||
value = Column(Text)
|
||||
description = Column(Text)
|
||||
category = Column(String(50), default='general') # 'general', 'email', 'proxy', 'openai'
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class Proxy(Base):
|
||||
"""代理列表表"""
|
||||
__tablename__ = 'proxies'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String(100), nullable=False) # 代理名称
|
||||
type = Column(String(20), nullable=False, default='http') # http, socks5
|
||||
host = Column(String(255), nullable=False)
|
||||
port = Column(Integer, nullable=False)
|
||||
username = Column(String(100))
|
||||
password = Column(String(255))
|
||||
enabled = Column(Boolean, default=True)
|
||||
priority = Column(Integer, default=0) # 优先级(保留字段)
|
||||
last_used = Column(DateTime) # 最后使用时间
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def to_dict(self, include_password: bool = False) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
result = {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'username': self.username,
|
||||
'enabled': self.enabled,
|
||||
'priority': self.priority,
|
||||
'last_used': self.last_used.isoformat() if self.last_used else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
if include_password:
|
||||
result['password'] = self.password
|
||||
else:
|
||||
result['has_password'] = bool(self.password)
|
||||
return result
|
||||
|
||||
@property
|
||||
def proxy_url(self) -> str:
|
||||
"""获取完整的代理 URL"""
|
||||
if self.type == "http":
|
||||
scheme = "http"
|
||||
elif self.type == "socks5":
|
||||
scheme = "socks5"
|
||||
else:
|
||||
scheme = self.type
|
||||
|
||||
auth = ""
|
||||
if self.username and self.password:
|
||||
auth = f"{self.username}:{self.password}@"
|
||||
|
||||
return f"{scheme}://{auth}{self.host}:{self.port}"
|
||||
@@ -78,6 +78,11 @@ def create_app() -> FastAPI:
|
||||
"""账号管理页面"""
|
||||
return templates.TemplateResponse("accounts.html", {"request": request})
|
||||
|
||||
@app.get("/email-services", response_class=HTMLResponse)
|
||||
async def email_services_page(request: Request):
|
||||
"""邮箱服务管理页面"""
|
||||
return templates.TemplateResponse("email_services.html", {"request": request})
|
||||
|
||||
@app.get("/settings", response_class=HTMLResponse)
|
||||
async def settings_page(request: Request):
|
||||
"""设置页面"""
|
||||
|
||||
@@ -26,10 +26,14 @@ class AccountResponse(BaseModel):
|
||||
"""账号响应模型"""
|
||||
id: int
|
||||
email: str
|
||||
password: Optional[str] = None
|
||||
client_id: Optional[str] = None
|
||||
email_service: str
|
||||
account_id: Optional[str] = None
|
||||
workspace_id: Optional[str] = None
|
||||
registered_at: Optional[str] = None
|
||||
last_refresh: Optional[str] = None
|
||||
expires_at: Optional[str] = None
|
||||
status: str
|
||||
proxy_used: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
@@ -69,10 +73,14 @@ def account_to_response(account: Account) -> AccountResponse:
|
||||
return AccountResponse(
|
||||
id=account.id,
|
||||
email=account.email,
|
||||
password=account.password,
|
||||
client_id=account.client_id,
|
||||
email_service=account.email_service,
|
||||
account_id=account.account_id,
|
||||
workspace_id=account.workspace_id,
|
||||
registered_at=account.registered_at.isoformat() if account.registered_at else None,
|
||||
last_refresh=account.last_refresh.isoformat() if account.last_refresh else None,
|
||||
expires_at=account.expires_at.isoformat() if account.expires_at else None,
|
||||
status=account.status,
|
||||
proxy_used=account.proxy_used,
|
||||
created_at=account.created_at.isoformat() if account.created_at else None,
|
||||
@@ -260,13 +268,18 @@ async def export_accounts_json(
|
||||
for acc in accounts:
|
||||
export_data.append({
|
||||
"email": acc.email,
|
||||
"password": acc.password,
|
||||
"client_id": acc.client_id,
|
||||
"account_id": acc.account_id,
|
||||
"workspace_id": acc.workspace_id,
|
||||
"access_token": acc.access_token,
|
||||
"refresh_token": acc.refresh_token,
|
||||
"id_token": acc.id_token,
|
||||
"session_token": acc.session_token,
|
||||
"email_service": acc.email_service,
|
||||
"registered_at": acc.registered_at.isoformat() if acc.registered_at else None,
|
||||
"last_refresh": acc.last_refresh.isoformat() if acc.last_refresh else None,
|
||||
"expires_at": acc.expires_at.isoformat() if acc.expires_at else None,
|
||||
"status": acc.status,
|
||||
})
|
||||
|
||||
@@ -310,9 +323,10 @@ async def export_accounts_csv(
|
||||
|
||||
# 写入表头
|
||||
writer.writerow([
|
||||
"ID", "Email", "Account ID", "Workspace ID",
|
||||
"Access Token", "Refresh Token", "ID Token",
|
||||
"Email Service", "Status", "Registered At"
|
||||
"ID", "Email", "Password", "Client ID",
|
||||
"Account ID", "Workspace ID",
|
||||
"Access Token", "Refresh Token", "ID Token", "Session Token",
|
||||
"Email Service", "Status", "Registered At", "Last Refresh", "Expires At"
|
||||
])
|
||||
|
||||
# 写入数据
|
||||
@@ -320,14 +334,19 @@ async def export_accounts_csv(
|
||||
writer.writerow([
|
||||
acc.id,
|
||||
acc.email,
|
||||
acc.password or "",
|
||||
acc.client_id or "",
|
||||
acc.account_id or "",
|
||||
acc.workspace_id or "",
|
||||
acc.access_token or "",
|
||||
acc.refresh_token or "",
|
||||
acc.id_token or "",
|
||||
acc.session_token or "",
|
||||
acc.email_service,
|
||||
acc.status,
|
||||
acc.registered_at.isoformat() if acc.registered_at else ""
|
||||
acc.registered_at.isoformat() if acc.registered_at else "",
|
||||
acc.last_refresh.isoformat() if acc.last_refresh else "",
|
||||
acc.expires_at.isoformat() if acc.expires_at else ""
|
||||
])
|
||||
|
||||
# 生成文件名
|
||||
@@ -367,3 +386,123 @@ async def get_accounts_stats():
|
||||
"by_status": {status: count for status, count in status_stats},
|
||||
"by_email_service": {service: count for service, count in service_stats}
|
||||
}
|
||||
|
||||
|
||||
# ============== Token 刷新相关 ==============
|
||||
|
||||
class TokenRefreshRequest(BaseModel):
|
||||
"""Token 刷新请求"""
|
||||
proxy: Optional[str] = None
|
||||
|
||||
|
||||
class BatchRefreshRequest(BaseModel):
|
||||
"""批量刷新请求"""
|
||||
ids: List[int]
|
||||
proxy: Optional[str] = None
|
||||
|
||||
|
||||
class TokenValidateRequest(BaseModel):
|
||||
"""Token 验证请求"""
|
||||
proxy: Optional[str] = None
|
||||
|
||||
|
||||
class BatchValidateRequest(BaseModel):
|
||||
"""批量验证请求"""
|
||||
ids: List[int]
|
||||
proxy: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/{account_id}/refresh")
|
||||
async def refresh_account_token(account_id: int, request: TokenRefreshRequest = None):
|
||||
"""刷新单个账号的 Token"""
|
||||
from ...core.token_refresh import refresh_account_token as do_refresh
|
||||
|
||||
proxy = request.proxy if request else None
|
||||
result = do_refresh(account_id, proxy)
|
||||
|
||||
if result.success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Token 刷新成功",
|
||||
"expires_at": result.expires_at.isoformat() if result.expires_at else None
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": result.error_message
|
||||
}
|
||||
|
||||
|
||||
@router.post("/batch-refresh")
|
||||
async def batch_refresh_tokens(request: BatchRefreshRequest, background_tasks: BackgroundTasks):
|
||||
"""批量刷新账号 Token"""
|
||||
from ...core.token_refresh import refresh_account_token as do_refresh
|
||||
|
||||
results = {
|
||||
"success_count": 0,
|
||||
"failed_count": 0,
|
||||
"errors": []
|
||||
}
|
||||
|
||||
for account_id in request.ids:
|
||||
try:
|
||||
result = do_refresh(account_id, request.proxy)
|
||||
if result.success:
|
||||
results["success_count"] += 1
|
||||
else:
|
||||
results["failed_count"] += 1
|
||||
results["errors"].append({"id": account_id, "error": result.error_message})
|
||||
except Exception as e:
|
||||
results["failed_count"] += 1
|
||||
results["errors"].append({"id": account_id, "error": str(e)})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@router.post("/{account_id}/validate")
|
||||
async def validate_account_token(account_id: int, request: TokenValidateRequest = None):
|
||||
"""验证单个账号的 Token 有效性"""
|
||||
from ...core.token_refresh import validate_account_token as do_validate
|
||||
|
||||
proxy = request.proxy if request else None
|
||||
is_valid, error = do_validate(account_id, proxy)
|
||||
|
||||
return {
|
||||
"id": account_id,
|
||||
"valid": is_valid,
|
||||
"error": error
|
||||
}
|
||||
|
||||
|
||||
@router.post("/batch-validate")
|
||||
async def batch_validate_tokens(request: BatchValidateRequest):
|
||||
"""批量验证账号 Token 有效性"""
|
||||
from ...core.token_refresh import validate_account_token as do_validate
|
||||
|
||||
results = {
|
||||
"valid_count": 0,
|
||||
"invalid_count": 0,
|
||||
"details": []
|
||||
}
|
||||
|
||||
for account_id in request.ids:
|
||||
try:
|
||||
is_valid, error = do_validate(account_id, request.proxy)
|
||||
results["details"].append({
|
||||
"id": account_id,
|
||||
"valid": is_valid,
|
||||
"error": error
|
||||
})
|
||||
if is_valid:
|
||||
results["valid_count"] += 1
|
||||
else:
|
||||
results["invalid_count"] += 1
|
||||
except Exception as e:
|
||||
results["invalid_count"] += 1
|
||||
results["details"].append({
|
||||
"id": account_id,
|
||||
"valid": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
@@ -43,6 +43,7 @@ class EmailServiceResponse(BaseModel):
|
||||
name: str
|
||||
enabled: bool
|
||||
priority: int
|
||||
config: Optional[Dict[str, Any]] = None # 过滤敏感信息后的配置
|
||||
last_used: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
@@ -82,6 +83,29 @@ class OutlookBatchImportResponse(BaseModel):
|
||||
|
||||
# ============== Helper Functions ==============
|
||||
|
||||
# 敏感字段列表,返回响应时需要过滤
|
||||
SENSITIVE_FIELDS = {'password', 'api_key', 'refresh_token', 'access_token'}
|
||||
|
||||
def filter_sensitive_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""过滤敏感配置信息"""
|
||||
if not config:
|
||||
return {}
|
||||
|
||||
filtered = {}
|
||||
for key, value in config.items():
|
||||
if key in SENSITIVE_FIELDS:
|
||||
# 敏感字段不返回,但标记是否存在
|
||||
filtered[f"has_{key}"] = bool(value)
|
||||
else:
|
||||
filtered[key] = value
|
||||
|
||||
# 为 Outlook 计算是否有 OAuth
|
||||
if config.get('client_id') and config.get('refresh_token'):
|
||||
filtered['has_oauth'] = True
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def service_to_response(service: EmailServiceModel) -> EmailServiceResponse:
|
||||
"""转换服务模型为响应"""
|
||||
return EmailServiceResponse(
|
||||
@@ -90,6 +114,7 @@ def service_to_response(service: EmailServiceModel) -> EmailServiceResponse:
|
||||
name=service.name,
|
||||
enabled=service.enabled,
|
||||
priority=service.priority,
|
||||
config=filter_sensitive_config(service.config),
|
||||
last_used=service.last_used.isoformat() if service.last_used else None,
|
||||
created_at=service.created_at.isoformat() if service.created_at else None,
|
||||
updated_at=service.updated_at.isoformat() if service.updated_at else None,
|
||||
@@ -98,6 +123,39 @@ def service_to_response(service: EmailServiceModel) -> EmailServiceResponse:
|
||||
|
||||
# ============== API Endpoints ==============
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_email_services_stats():
|
||||
"""获取邮箱服务统计信息"""
|
||||
with get_db() as db:
|
||||
from sqlalchemy import func
|
||||
|
||||
# 按类型统计
|
||||
type_stats = db.query(
|
||||
EmailServiceModel.service_type,
|
||||
func.count(EmailServiceModel.id)
|
||||
).group_by(EmailServiceModel.service_type).all()
|
||||
|
||||
# 启用数量
|
||||
enabled_count = db.query(func.count(EmailServiceModel.id)).filter(
|
||||
EmailServiceModel.enabled == True
|
||||
).scalar()
|
||||
|
||||
stats = {
|
||||
'outlook_count': 0,
|
||||
'custom_count': 0,
|
||||
'tempmail_available': True, # 临时邮箱始终可用
|
||||
'enabled_count': enabled_count
|
||||
}
|
||||
|
||||
for service_type, count in type_stats:
|
||||
if service_type == 'outlook':
|
||||
stats['outlook_count'] = count
|
||||
elif service_type == 'custom_domain':
|
||||
stats['custom_count'] = count
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
@router.get("/types")
|
||||
async def get_service_types():
|
||||
"""获取支持的邮箱服务类型"""
|
||||
@@ -435,3 +493,36 @@ async def batch_delete_outlook(service_ids: List[int]):
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "deleted": deleted, "message": f"已删除 {deleted} 个服务"}
|
||||
|
||||
|
||||
# ============== 临时邮箱测试 ==============
|
||||
|
||||
class TempmailTestRequest(BaseModel):
|
||||
"""临时邮箱测试请求"""
|
||||
api_url: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/test-tempmail")
|
||||
async def test_tempmail_service(request: TempmailTestRequest):
|
||||
"""测试临时邮箱服务是否可用"""
|
||||
try:
|
||||
from ...services import EmailServiceFactory, EmailServiceType
|
||||
from ...config.settings import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
base_url = request.api_url or settings.tempmail_base_url
|
||||
|
||||
config = {"base_url": base_url}
|
||||
tempmail = EmailServiceFactory.create(EmailServiceType.TEMPMAIL, config)
|
||||
|
||||
# 检查服务健康状态
|
||||
health = tempmail.check_health()
|
||||
|
||||
if health:
|
||||
return {"success": True, "message": "临时邮箱连接正常"}
|
||||
else:
|
||||
return {"success": False, "message": "临时邮箱连接失败"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"测试临时邮箱失败: {e}")
|
||||
return {"success": False, "message": f"测试失败: {str(e)}"}
|
||||
|
||||
@@ -7,14 +7,14 @@ import logging
|
||||
import uuid
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, BackgroundTasks
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ...database import crud
|
||||
from ...database.session import get_db
|
||||
from ...database.models import RegistrationTask
|
||||
from ...database.models import RegistrationTask, Proxy
|
||||
from ...core.register import RegistrationEngine, RegistrationResult
|
||||
from ...services import EmailServiceFactory, EmailServiceType
|
||||
from ...config.settings import get_settings
|
||||
@@ -28,6 +28,38 @@ running_tasks: dict = {}
|
||||
batch_tasks: Dict[str, dict] = {}
|
||||
|
||||
|
||||
# ============== Proxy Helper Functions ==============
|
||||
|
||||
def get_proxy_for_registration(db) -> Tuple[Optional[str], Optional[int]]:
|
||||
"""
|
||||
获取用于注册的代理
|
||||
|
||||
策略:
|
||||
1. 优先从代理列表中随机选择一个启用的代理
|
||||
2. 如果代理列表为空,使用系统设置中的默认代理
|
||||
|
||||
Returns:
|
||||
Tuple[proxy_url, proxy_id]: 代理 URL 和代理 ID(如果来自代理列表)
|
||||
"""
|
||||
# 先尝试从代理列表中获取
|
||||
proxy = crud.get_random_proxy(db)
|
||||
if proxy:
|
||||
return proxy.proxy_url, proxy.id
|
||||
|
||||
# 代理列表为空,使用系统设置中的默认代理
|
||||
settings = get_settings()
|
||||
if settings.proxy_enabled and settings.proxy_url:
|
||||
return settings.proxy_url, None
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def update_proxy_usage(db, proxy_id: Optional[int]):
|
||||
"""更新代理的使用时间"""
|
||||
if proxy_id:
|
||||
crud.update_proxy_last_used(db, proxy_id)
|
||||
|
||||
|
||||
# ============== Pydantic Models ==============
|
||||
|
||||
class RegistrationTaskCreate(BaseModel):
|
||||
@@ -114,6 +146,20 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
logger.error(f"任务不存在: {task_uuid}")
|
||||
return
|
||||
|
||||
# 确定使用的代理
|
||||
# 如果前端传入了代理参数,使用传入的
|
||||
# 否则从代理列表或系统设置中获取
|
||||
actual_proxy_url = proxy
|
||||
proxy_id = None
|
||||
|
||||
if not actual_proxy_url:
|
||||
actual_proxy_url, proxy_id = get_proxy_for_registration(db)
|
||||
if actual_proxy_url:
|
||||
logger.info(f"任务 {task_uuid} 使用代理: {actual_proxy_url[:50]}...")
|
||||
|
||||
# 更新任务的代理记录
|
||||
crud.update_registration_task(db, task_uuid, proxy=actual_proxy_url)
|
||||
|
||||
# 创建邮箱服务
|
||||
service_type = EmailServiceType(email_service_type)
|
||||
settings = get_settings()
|
||||
@@ -140,7 +186,7 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
"base_url": settings.tempmail_base_url,
|
||||
"timeout": settings.tempmail_timeout,
|
||||
"max_retries": settings.tempmail_max_retries,
|
||||
"proxy_url": proxy,
|
||||
"proxy_url": actual_proxy_url,
|
||||
}
|
||||
elif service_type == EmailServiceType.CUSTOM_DOMAIN:
|
||||
# 检查数据库中是否有可用的自定义域名服务
|
||||
@@ -158,7 +204,7 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
config = {
|
||||
"base_url": settings.custom_domain_base_url,
|
||||
"api_key": settings.custom_domain_api_key.get_secret_value() if settings.custom_domain_api_key else "",
|
||||
"proxy_url": proxy,
|
||||
"proxy_url": actual_proxy_url,
|
||||
}
|
||||
else:
|
||||
raise ValueError("没有可用的自定义域名邮箱服务,请先在设置中配置")
|
||||
@@ -188,7 +234,7 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
|
||||
engine = RegistrationEngine(
|
||||
email_service=email_service,
|
||||
proxy_url=proxy,
|
||||
proxy_url=actual_proxy_url,
|
||||
callback_logger=log_callback,
|
||||
task_uuid=task_uuid
|
||||
)
|
||||
@@ -197,6 +243,9 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
result = engine.run()
|
||||
|
||||
if result.success:
|
||||
# 更新代理使用时间
|
||||
update_proxy_usage(db, proxy_id)
|
||||
|
||||
# 保存到数据库
|
||||
engine.save_to_database(result)
|
||||
|
||||
|
||||
@@ -135,6 +135,66 @@ async def update_proxy_settings(request: ProxySettings):
|
||||
return {"success": True, "message": "代理设置已更新"}
|
||||
|
||||
|
||||
@router.post("/proxy/test")
|
||||
async def test_proxy_settings(request: ProxySettings):
|
||||
"""测试代理连接"""
|
||||
import time
|
||||
from curl_cffi import requests as cffi_requests
|
||||
|
||||
# 构建代理 URL
|
||||
if request.type == "http":
|
||||
scheme = "http"
|
||||
elif request.type == "socks5":
|
||||
scheme = "socks5"
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="不支持的代理类型")
|
||||
|
||||
auth = ""
|
||||
if request.username and request.password:
|
||||
auth = f"{request.username}:{request.password}@"
|
||||
|
||||
proxy_url = f"{scheme}://{auth}{request.host}:{request.port}"
|
||||
|
||||
# 测试连接
|
||||
test_url = "https://api.ipify.org?format=json"
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
proxies = {
|
||||
"http": proxy_url,
|
||||
"https": proxy_url
|
||||
}
|
||||
|
||||
response = cffi_requests.get(
|
||||
test_url,
|
||||
proxies=proxies,
|
||||
timeout=3,
|
||||
impersonate="chrome110"
|
||||
)
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
if response.status_code == 200:
|
||||
ip_info = response.json()
|
||||
return {
|
||||
"success": True,
|
||||
"ip": ip_info.get("ip", ""),
|
||||
"response_time": round(elapsed_time * 1000), # 毫秒
|
||||
"message": f"代理连接成功,出口 IP: {ip_info.get('ip', 'unknown')}"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"代理返回错误状态码: {response.status_code}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"代理连接失败: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/registration")
|
||||
async def get_registration_settings():
|
||||
"""获取注册设置"""
|
||||
@@ -292,3 +352,275 @@ async def get_recent_logs(
|
||||
}
|
||||
except Exception as e:
|
||||
return {"logs": [], "error": str(e)}
|
||||
|
||||
|
||||
# ============== 临时邮箱设置 ==============
|
||||
|
||||
class TempmailSettings(BaseModel):
|
||||
"""临时邮箱设置"""
|
||||
api_url: Optional[str] = None
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
@router.get("/tempmail")
|
||||
async def get_tempmail_settings():
|
||||
"""获取临时邮箱设置"""
|
||||
settings = get_settings()
|
||||
|
||||
return {
|
||||
"api_url": settings.tempmail_base_url,
|
||||
"timeout": settings.tempmail_timeout,
|
||||
"max_retries": settings.tempmail_max_retries,
|
||||
"enabled": True # 临时邮箱默认可用
|
||||
}
|
||||
|
||||
|
||||
@router.post("/tempmail")
|
||||
async def update_tempmail_settings(request: TempmailSettings):
|
||||
"""更新临时邮箱设置"""
|
||||
update_dict = {}
|
||||
|
||||
if request.api_url:
|
||||
update_dict["tempmail_base_url"] = request.api_url
|
||||
|
||||
update_settings(**update_dict)
|
||||
|
||||
return {"success": True, "message": "临时邮箱设置已更新"}
|
||||
|
||||
|
||||
# ============== 代理列表 CRUD ==============
|
||||
|
||||
class ProxyCreateRequest(BaseModel):
|
||||
"""创建代理请求"""
|
||||
name: str
|
||||
type: str = "http" # http, socks5
|
||||
host: str
|
||||
port: int
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
enabled: bool = True
|
||||
priority: int = 0
|
||||
|
||||
|
||||
class ProxyUpdateRequest(BaseModel):
|
||||
"""更新代理请求"""
|
||||
name: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
host: Optional[str] = None
|
||||
port: Optional[int] = None
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
priority: Optional[int] = None
|
||||
|
||||
|
||||
@router.get("/proxies")
|
||||
async def get_proxies_list(enabled: Optional[bool] = None):
|
||||
"""获取代理列表"""
|
||||
with get_db() as db:
|
||||
proxies = crud.get_proxies(db, enabled=enabled)
|
||||
return {
|
||||
"proxies": [p.to_dict() for p in proxies],
|
||||
"total": len(proxies)
|
||||
}
|
||||
|
||||
|
||||
@router.post("/proxies")
|
||||
async def create_proxy_item(request: ProxyCreateRequest):
|
||||
"""创建代理"""
|
||||
with get_db() as db:
|
||||
proxy = crud.create_proxy(
|
||||
db,
|
||||
name=request.name,
|
||||
type=request.type,
|
||||
host=request.host,
|
||||
port=request.port,
|
||||
username=request.username,
|
||||
password=request.password,
|
||||
enabled=request.enabled,
|
||||
priority=request.priority
|
||||
)
|
||||
return {"success": True, "proxy": proxy.to_dict()}
|
||||
|
||||
|
||||
@router.get("/proxies/{proxy_id}")
|
||||
async def get_proxy_item(proxy_id: int):
|
||||
"""获取单个代理"""
|
||||
with get_db() as db:
|
||||
proxy = crud.get_proxy_by_id(db, proxy_id)
|
||||
if not proxy:
|
||||
raise HTTPException(status_code=404, detail="代理不存在")
|
||||
return proxy.to_dict(include_password=True)
|
||||
|
||||
|
||||
@router.patch("/proxies/{proxy_id}")
|
||||
async def update_proxy_item(proxy_id: int, request: ProxyUpdateRequest):
|
||||
"""更新代理"""
|
||||
with get_db() as db:
|
||||
update_data = {}
|
||||
if request.name is not None:
|
||||
update_data["name"] = request.name
|
||||
if request.type is not None:
|
||||
update_data["type"] = request.type
|
||||
if request.host is not None:
|
||||
update_data["host"] = request.host
|
||||
if request.port is not None:
|
||||
update_data["port"] = request.port
|
||||
if request.username is not None:
|
||||
update_data["username"] = request.username
|
||||
if request.password is not None:
|
||||
update_data["password"] = request.password
|
||||
if request.enabled is not None:
|
||||
update_data["enabled"] = request.enabled
|
||||
if request.priority is not None:
|
||||
update_data["priority"] = request.priority
|
||||
|
||||
proxy = crud.update_proxy(db, proxy_id, **update_data)
|
||||
if not proxy:
|
||||
raise HTTPException(status_code=404, detail="代理不存在")
|
||||
return {"success": True, "proxy": proxy.to_dict()}
|
||||
|
||||
|
||||
@router.delete("/proxies/{proxy_id}")
|
||||
async def delete_proxy_item(proxy_id: int):
|
||||
"""删除代理"""
|
||||
with get_db() as db:
|
||||
success = crud.delete_proxy(db, proxy_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="代理不存在")
|
||||
return {"success": True, "message": "代理已删除"}
|
||||
|
||||
|
||||
@router.post("/proxies/{proxy_id}/test")
|
||||
async def test_proxy_item(proxy_id: int):
|
||||
"""测试单个代理"""
|
||||
import time
|
||||
from curl_cffi import requests as cffi_requests
|
||||
|
||||
with get_db() as db:
|
||||
proxy = crud.get_proxy_by_id(db, proxy_id)
|
||||
if not proxy:
|
||||
raise HTTPException(status_code=404, detail="代理不存在")
|
||||
|
||||
proxy_url = proxy.proxy_url
|
||||
test_url = "https://api.ipify.org?format=json"
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
proxies = {
|
||||
"http": proxy_url,
|
||||
"https": proxy_url
|
||||
}
|
||||
|
||||
response = cffi_requests.get(
|
||||
test_url,
|
||||
proxies=proxies,
|
||||
timeout=3,
|
||||
impersonate="chrome110"
|
||||
)
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
if response.status_code == 200:
|
||||
ip_info = response.json()
|
||||
return {
|
||||
"success": True,
|
||||
"ip": ip_info.get("ip", ""),
|
||||
"response_time": round(elapsed_time * 1000),
|
||||
"message": f"代理连接成功,出口 IP: {ip_info.get('ip', 'unknown')}"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"代理返回错误状态码: {response.status_code}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"代理连接失败: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/proxies/test-all")
|
||||
async def test_all_proxies():
|
||||
"""测试所有启用的代理"""
|
||||
import time
|
||||
from curl_cffi import requests as cffi_requests
|
||||
|
||||
with get_db() as db:
|
||||
proxies = crud.get_enabled_proxies(db)
|
||||
|
||||
results = []
|
||||
for proxy in proxies:
|
||||
proxy_url = proxy.proxy_url
|
||||
test_url = "https://api.ipify.org?format=json"
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
proxies_dict = {
|
||||
"http": proxy_url,
|
||||
"https": proxy_url
|
||||
}
|
||||
|
||||
response = cffi_requests.get(
|
||||
test_url,
|
||||
proxies=proxies_dict,
|
||||
timeout=3,
|
||||
impersonate="chrome110"
|
||||
)
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
if response.status_code == 200:
|
||||
ip_info = response.json()
|
||||
results.append({
|
||||
"id": proxy.id,
|
||||
"name": proxy.name,
|
||||
"success": True,
|
||||
"ip": ip_info.get("ip", ""),
|
||||
"response_time": round(elapsed_time * 1000)
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
"id": proxy.id,
|
||||
"name": proxy.name,
|
||||
"success": False,
|
||||
"message": f"状态码: {response.status_code}"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
results.append({
|
||||
"id": proxy.id,
|
||||
"name": proxy.name,
|
||||
"success": False,
|
||||
"message": str(e)
|
||||
})
|
||||
|
||||
success_count = sum(1 for r in results if r["success"])
|
||||
return {
|
||||
"total": len(proxies),
|
||||
"success": success_count,
|
||||
"failed": len(proxies) - success_count,
|
||||
"results": results
|
||||
}
|
||||
|
||||
|
||||
@router.post("/proxies/{proxy_id}/enable")
|
||||
async def enable_proxy(proxy_id: int):
|
||||
"""启用代理"""
|
||||
with get_db() as db:
|
||||
proxy = crud.update_proxy(db, proxy_id, enabled=True)
|
||||
if not proxy:
|
||||
raise HTTPException(status_code=404, detail="代理不存在")
|
||||
return {"success": True, "message": "代理已启用"}
|
||||
|
||||
|
||||
@router.post("/proxies/{proxy_id}/disable")
|
||||
async def disable_proxy(proxy_id: int):
|
||||
"""禁用代理"""
|
||||
with get_db() as db:
|
||||
proxy = crud.update_proxy(db, proxy_id, enabled=False)
|
||||
if not proxy:
|
||||
raise HTTPException(status_code=404, detail="代理不存在")
|
||||
return {"success": True, "message": "代理已禁用"}
|
||||
|
||||
@@ -21,6 +21,8 @@ const elements = {
|
||||
filterService: document.getElementById('filter-service'),
|
||||
searchInput: document.getElementById('search-input'),
|
||||
refreshBtn: document.getElementById('refresh-btn'),
|
||||
batchRefreshBtn: document.getElementById('batch-refresh-btn'),
|
||||
batchValidateBtn: document.getElementById('batch-validate-btn'),
|
||||
batchDeleteBtn: document.getElementById('batch-delete-btn'),
|
||||
exportBtn: document.getElementById('export-btn'),
|
||||
exportMenu: document.getElementById('export-menu'),
|
||||
@@ -75,6 +77,12 @@ function initEventListeners() {
|
||||
toast.info('已刷新');
|
||||
});
|
||||
|
||||
// 批量刷新Token
|
||||
elements.batchRefreshBtn.addEventListener('click', handleBatchRefresh);
|
||||
|
||||
// 批量验证Token
|
||||
elements.batchValidateBtn.addEventListener('click', handleBatchValidate);
|
||||
|
||||
// 批量删除
|
||||
elements.batchDeleteBtn.addEventListener('click', handleBatchDelete);
|
||||
|
||||
@@ -173,7 +181,7 @@ async function loadAccounts() {
|
||||
// 显示加载状态
|
||||
elements.table.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<td colspan="8">
|
||||
<div class="empty-state">
|
||||
<div class="skeleton skeleton-text" style="width: 60%;"></div>
|
||||
<div class="skeleton skeleton-text" style="width: 80%;"></div>
|
||||
@@ -209,7 +217,7 @@ async function loadAccounts() {
|
||||
console.error('加载账号列表失败:', error);
|
||||
elements.table.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<td colspan="8">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">❌</div>
|
||||
<div class="empty-state-title">加载失败</div>
|
||||
@@ -228,7 +236,7 @@ function renderAccounts(accounts) {
|
||||
if (accounts.length === 0) {
|
||||
elements.table.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<td colspan="8">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div class="empty-state-title">暂无数据</div>
|
||||
@@ -252,21 +260,30 @@ function renderAccounts(accounts) {
|
||||
${escapeHtml(account.email)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="password-cell">
|
||||
${account.password
|
||||
? `<span class="password-hidden" onclick="togglePassword(this, '${escapeHtml(account.password)}')" title="点击查看">${escapeHtml(account.password.substring(0, 4) + '****')}</span>`
|
||||
: '-'}
|
||||
</td>
|
||||
<td>${getServiceTypeText(account.email_service)}</td>
|
||||
<td>
|
||||
<span class="status-badge ${getStatusClass('account', account.status)}">
|
||||
${getStatusText('account', account.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td>${format.date(account.registered_at)}</td>
|
||||
<td>${format.date(account.last_refresh) || '-'}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-ghost btn-sm" onclick="refreshToken(${account.id})" title="刷新Token">
|
||||
🔄
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="viewAccount(${account.id})" title="查看详情">
|
||||
👁️
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="copyEmail('${escapeHtml(account.email)}')" title="复制邮箱">
|
||||
📋
|
||||
</button>
|
||||
${account.password ? `<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(account.password)}')" title="复制密码">🔑</button>` : ''}
|
||||
<button class="btn btn-ghost btn-sm" onclick="deleteAccount(${account.id}, '${escapeHtml(account.email)}')" title="删除">
|
||||
🗑️
|
||||
</button>
|
||||
@@ -289,6 +306,19 @@ function renderAccounts(accounts) {
|
||||
});
|
||||
}
|
||||
|
||||
// 切换密码显示
|
||||
function togglePassword(element, password) {
|
||||
if (element.dataset.revealed === 'true') {
|
||||
element.textContent = password.substring(0, 4) + '****';
|
||||
element.classList.add('password-hidden');
|
||||
element.dataset.revealed = 'false';
|
||||
} else {
|
||||
element.textContent = password;
|
||||
element.classList.remove('password-hidden');
|
||||
element.dataset.revealed = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新分页
|
||||
function updatePagination() {
|
||||
const totalPages = Math.max(1, Math.ceil(totalAccounts / pageSize));
|
||||
@@ -303,7 +333,74 @@ function updatePagination() {
|
||||
function updateBatchButtons() {
|
||||
const count = selectedAccounts.size;
|
||||
elements.batchDeleteBtn.disabled = count === 0;
|
||||
elements.batchDeleteBtn.textContent = count > 0 ? `🗑️ 删除选中 (${count})` : '🗑️ 批量删除';
|
||||
elements.batchRefreshBtn.disabled = count === 0;
|
||||
elements.batchValidateBtn.disabled = count === 0;
|
||||
|
||||
elements.batchDeleteBtn.textContent = count > 0 ? `🗑️ 删除 (${count})` : '🗑️ 批量删除';
|
||||
elements.batchRefreshBtn.textContent = count > 0 ? `🔄 刷新 (${count})` : '🔄 刷新Token';
|
||||
elements.batchValidateBtn.textContent = count > 0 ? `✅ 验证 (${count})` : '✅ 验证Token';
|
||||
}
|
||||
|
||||
// 刷新单个账号Token
|
||||
async function refreshToken(id) {
|
||||
try {
|
||||
toast.info('正在刷新Token...');
|
||||
const result = await api.post(`/accounts/${id}/refresh`);
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Token刷新成功');
|
||||
loadAccounts();
|
||||
} else {
|
||||
toast.error('刷新失败: ' + (result.error || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('刷新失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量刷新Token
|
||||
async function handleBatchRefresh() {
|
||||
if (selectedAccounts.size === 0) return;
|
||||
|
||||
const confirmed = await confirm(`确定要刷新选中的 ${selectedAccounts.size} 个账号的Token吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
elements.batchRefreshBtn.disabled = true;
|
||||
elements.batchRefreshBtn.textContent = '刷新中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/accounts/batch-refresh', {
|
||||
ids: Array.from(selectedAccounts)
|
||||
});
|
||||
|
||||
toast.success(`成功刷新 ${result.success_count} 个,失败 ${result.failed_count} 个`);
|
||||
loadAccounts();
|
||||
} catch (error) {
|
||||
toast.error('批量刷新失败: ' + error.message);
|
||||
} finally {
|
||||
updateBatchButtons();
|
||||
}
|
||||
}
|
||||
|
||||
// 批量验证Token
|
||||
async function handleBatchValidate() {
|
||||
if (selectedAccounts.size === 0) return;
|
||||
|
||||
elements.batchValidateBtn.disabled = true;
|
||||
elements.batchValidateBtn.textContent = '验证中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/accounts/batch-validate', {
|
||||
ids: Array.from(selectedAccounts)
|
||||
});
|
||||
|
||||
toast.info(`有效: ${result.valid_count},无效: ${result.invalid_count}`);
|
||||
loadAccounts();
|
||||
} catch (error) {
|
||||
toast.error('批量验证失败: ' + error.message);
|
||||
} finally {
|
||||
updateBatchButtons();
|
||||
}
|
||||
}
|
||||
|
||||
// 查看账号详情
|
||||
@@ -323,6 +420,15 @@ async function viewAccount(id) {
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">密码</span>
|
||||
<span class="value">
|
||||
${account.password
|
||||
? `<code style="font-size: 0.75rem;">${escapeHtml(account.password)}</code>
|
||||
<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(account.password)}')" title="复制">📋</button>`
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">邮箱服务</span>
|
||||
<span class="value">${getServiceTypeText(account.email_service)}</span>
|
||||
@@ -339,6 +445,10 @@ async function viewAccount(id) {
|
||||
<span class="label">注册时间</span>
|
||||
<span class="value">${format.date(account.registered_at)}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">最后刷新</span>
|
||||
<span class="value">${format.date(account.last_refresh) || '-'}</span>
|
||||
</div>
|
||||
<div class="info-item" style="grid-column: span 2;">
|
||||
<span class="label">Account ID</span>
|
||||
<span class="value" style="font-size: 0.75rem; word-break: break-all;">
|
||||
@@ -351,6 +461,12 @@ async function viewAccount(id) {
|
||||
${escapeHtml(account.workspace_id || '-')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item" style="grid-column: span 2;">
|
||||
<span class="label">Client ID</span>
|
||||
<span class="value" style="font-size: 0.75rem; word-break: break-all;">
|
||||
${escapeHtml(account.client_id || '-')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item" style="grid-column: span 2;">
|
||||
<span class="label">Access Token</span>
|
||||
<div class="value" style="font-size: 0.7rem; word-break: break-all; font-family: var(--font-mono); background: var(--surface-hover); padding: 8px; border-radius: 4px;">
|
||||
@@ -366,6 +482,11 @@ async function viewAccount(id) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: var(--spacing-lg); display: flex; gap: var(--spacing-sm);">
|
||||
<button class="btn btn-primary" onclick="refreshToken(${id}); elements.detailModal.classList.remove('active');">
|
||||
🔄 刷新Token
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
elements.detailModal.classList.add('active');
|
||||
|
||||
124
static/js/app.js
124
static/js/app.js
@@ -8,6 +8,7 @@ let currentTask = null;
|
||||
let currentBatch = null;
|
||||
let logPollingInterval = null;
|
||||
let batchPollingInterval = null;
|
||||
let accountsPollingInterval = null;
|
||||
let isBatchMode = false;
|
||||
let availableServices = {
|
||||
tempmail: { available: true, services: [] },
|
||||
@@ -19,7 +20,6 @@ let availableServices = {
|
||||
const elements = {
|
||||
form: document.getElementById('registration-form'),
|
||||
emailService: document.getElementById('email-service'),
|
||||
proxy: document.getElementById('proxy'),
|
||||
regMode: document.getElementById('reg-mode'),
|
||||
batchCountGroup: document.getElementById('batch-count-group'),
|
||||
batchCount: document.getElementById('batch-count'),
|
||||
@@ -28,8 +28,8 @@ const elements = {
|
||||
intervalMax: document.getElementById('interval-max'),
|
||||
startBtn: document.getElementById('start-btn'),
|
||||
cancelBtn: document.getElementById('cancel-btn'),
|
||||
taskStatusCard: document.getElementById('task-status-card'),
|
||||
batchStatusCard: document.getElementById('batch-status-card'),
|
||||
taskStatusRow: document.getElementById('task-status-row'),
|
||||
batchProgressSection: document.getElementById('batch-progress-section'),
|
||||
consoleLog: document.getElementById('console-log'),
|
||||
clearLogBtn: document.getElementById('clear-log-btn'),
|
||||
// 任务状态
|
||||
@@ -39,18 +39,23 @@ const elements = {
|
||||
taskService: document.getElementById('task-service'),
|
||||
taskStatusBadge: document.getElementById('task-status-badge'),
|
||||
// 批量状态
|
||||
batchProgress: document.getElementById('batch-progress'),
|
||||
batchProgressText: document.getElementById('batch-progress-text'),
|
||||
batchProgressPercent: document.getElementById('batch-progress-percent'),
|
||||
progressBar: document.getElementById('progress-bar'),
|
||||
batchSuccess: document.getElementById('batch-success'),
|
||||
batchFailed: document.getElementById('batch-failed'),
|
||||
batchRemaining: document.getElementById('batch-remaining')
|
||||
batchRemaining: document.getElementById('batch-remaining'),
|
||||
// 已注册账号
|
||||
recentAccountsTable: document.getElementById('recent-accounts-table'),
|
||||
refreshAccountsBtn: document.getElementById('refresh-accounts-btn')
|
||||
};
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initEventListeners();
|
||||
loadSavedProxy();
|
||||
loadAvailableServices();
|
||||
loadRecentAccounts();
|
||||
startAccountsPolling();
|
||||
});
|
||||
|
||||
// 事件监听
|
||||
@@ -71,18 +76,12 @@ function initEventListeners() {
|
||||
elements.clearLogBtn.addEventListener('click', () => {
|
||||
elements.consoleLog.innerHTML = '<div class="log-line info">[系统] 日志已清空</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// 加载保存的代理设置
|
||||
async function loadSavedProxy() {
|
||||
try {
|
||||
const settings = await api.get('/settings');
|
||||
if (settings.proxy?.host) {
|
||||
elements.proxy.value = `${settings.proxy.type}://${settings.proxy.host}:${settings.proxy.port}`;
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略错误
|
||||
}
|
||||
// 刷新账号列表
|
||||
elements.refreshAccountsBtn.addEventListener('click', () => {
|
||||
loadRecentAccounts();
|
||||
toast.info('已刷新');
|
||||
});
|
||||
}
|
||||
|
||||
// 加载可用的邮箱服务
|
||||
@@ -143,7 +142,7 @@ function updateEmailServiceOptions() {
|
||||
|
||||
const option = document.createElement('option');
|
||||
option.value = '';
|
||||
option.textContent = '请先在设置中导入账户';
|
||||
option.textContent = '请先在邮箱服务页面导入账户';
|
||||
option.disabled = true;
|
||||
optgroup.appendChild(option);
|
||||
|
||||
@@ -173,7 +172,7 @@ function updateEmailServiceOptions() {
|
||||
|
||||
const option = document.createElement('option');
|
||||
option.value = '';
|
||||
option.textContent = '请先在设置中添加服务';
|
||||
option.textContent = '请先在邮箱服务页面添加服务';
|
||||
option.disabled = true;
|
||||
optgroup.appendChild(option);
|
||||
|
||||
@@ -223,7 +222,6 @@ async function handleStartRegistration(e) {
|
||||
}
|
||||
|
||||
const [emailServiceType, serviceId] = selectedValue.split(':');
|
||||
const proxy = elements.proxy.value.trim() || null;
|
||||
|
||||
// 禁用开始按钮
|
||||
elements.startBtn.disabled = true;
|
||||
@@ -232,10 +230,9 @@ async function handleStartRegistration(e) {
|
||||
// 清空日志
|
||||
elements.consoleLog.innerHTML = '';
|
||||
|
||||
// 构建请求数据
|
||||
// 构建请求数据(代理从设置中自动获取)
|
||||
const requestData = {
|
||||
email_service_type: emailServiceType,
|
||||
proxy: proxy
|
||||
email_service_type: emailServiceType
|
||||
};
|
||||
|
||||
// 如果选择了数据库中的服务,传递 service_id
|
||||
@@ -365,6 +362,8 @@ function startLogPolling(taskUuid) {
|
||||
if (data.status === 'completed') {
|
||||
addLog('success', '[成功] 注册成功!');
|
||||
toast.success('注册成功!');
|
||||
// 刷新账号列表
|
||||
loadRecentAccounts();
|
||||
} else if (data.status === 'failed') {
|
||||
addLog('error', '[错误] 注册失败');
|
||||
toast.error('注册失败');
|
||||
@@ -401,6 +400,8 @@ function startBatchPolling(batchId) {
|
||||
addLog('info', `[完成] 批量任务完成!成功: ${data.success}, 失败: ${data.failed}`);
|
||||
if (data.success > 0) {
|
||||
toast.success(`批量注册完成,成功 ${data.success} 个`);
|
||||
// 刷新账号列表
|
||||
loadRecentAccounts();
|
||||
} else {
|
||||
toast.warning('批量注册完成,但没有成功注册任何账号');
|
||||
}
|
||||
@@ -421,9 +422,10 @@ function stopBatchPolling() {
|
||||
|
||||
// 显示任务状态
|
||||
function showTaskStatus(task) {
|
||||
elements.taskStatusCard.style.display = 'block';
|
||||
elements.batchStatusCard.style.display = 'none';
|
||||
elements.taskId.textContent = task.task_uuid;
|
||||
elements.taskStatusRow.style.display = 'grid';
|
||||
elements.batchProgressSection.style.display = 'none';
|
||||
elements.taskStatusBadge.style.display = 'inline-flex';
|
||||
elements.taskId.textContent = task.task_uuid.substring(0, 8) + '...';
|
||||
elements.taskEmail.textContent = '-';
|
||||
elements.taskService.textContent = '-';
|
||||
}
|
||||
@@ -446,9 +448,11 @@ function updateTaskStatus(status) {
|
||||
|
||||
// 显示批量状态
|
||||
function showBatchStatus(batch) {
|
||||
elements.batchStatusCard.style.display = 'block';
|
||||
elements.taskStatusCard.style.display = 'none';
|
||||
elements.batchProgress.textContent = `0/${batch.count}`;
|
||||
elements.batchProgressSection.style.display = 'block';
|
||||
elements.taskStatusRow.style.display = 'none';
|
||||
elements.taskStatusBadge.style.display = 'none';
|
||||
elements.batchProgressText.textContent = `0/${batch.count}`;
|
||||
elements.batchProgressPercent.textContent = '0%';
|
||||
elements.progressBar.style.width = '0%';
|
||||
elements.batchSuccess.textContent = '0';
|
||||
elements.batchFailed.textContent = '0';
|
||||
@@ -461,8 +465,9 @@ function showBatchStatus(batch) {
|
||||
|
||||
// 更新批量进度
|
||||
function updateBatchProgress(data) {
|
||||
const progress = (data.completed / data.total * 100).toFixed(0);
|
||||
elements.batchProgress.textContent = data.progress || `${data.completed}/${data.total}`;
|
||||
const progress = ((data.completed / data.total) * 100).toFixed(0);
|
||||
elements.batchProgressText.textContent = `${data.completed}/${data.total}`;
|
||||
elements.batchProgressPercent.textContent = `${progress}%`;
|
||||
elements.progressBar.style.width = `${progress}%`;
|
||||
elements.batchSuccess.textContent = data.success;
|
||||
elements.batchFailed.textContent = data.failed;
|
||||
@@ -485,6 +490,63 @@ function updateBatchProgress(data) {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最近注册的账号
|
||||
async function loadRecentAccounts() {
|
||||
try {
|
||||
const data = await api.get('/accounts?page=1&page_size=10');
|
||||
|
||||
if (data.accounts.length === 0) {
|
||||
elements.recentAccountsTable.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state" style="padding: var(--spacing-md);">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div class="empty-state-title">暂无已注册账号</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
elements.recentAccountsTable.innerHTML = data.accounts.map(account => `
|
||||
<tr data-id="${account.id}">
|
||||
<td>${account.id}</td>
|
||||
<td>
|
||||
<span title="${escapeHtml(account.email)}">${escapeHtml(account.email)}</span>
|
||||
</td>
|
||||
<td class="password-cell">
|
||||
${account.password ? `<span class="password-hidden" title="点击查看">${escapeHtml(account.password.substring(0, 8))}...</span>` : '-'}
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge ${getStatusClass('account', account.status)}" style="font-size: 0.7rem;">
|
||||
${getStatusText('account', account.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(account.email)}')" title="复制邮箱">
|
||||
📋
|
||||
</button>
|
||||
${account.password ? `<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(account.password)}')" title="复制密码">🔑</button>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载账号列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始账号列表轮询
|
||||
function startAccountsPolling() {
|
||||
// 每30秒刷新一次账号列表
|
||||
accountsPollingInterval = setInterval(() => {
|
||||
loadRecentAccounts();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// 添加日志
|
||||
function addLog(type, message) {
|
||||
const line = document.createElement('div');
|
||||
|
||||
503
static/js/email_services.js
Normal file
503
static/js/email_services.js
Normal file
@@ -0,0 +1,503 @@
|
||||
/**
|
||||
* 邮箱服务页面 JavaScript
|
||||
*/
|
||||
|
||||
// 状态
|
||||
let outlookServices = [];
|
||||
let customServices = [];
|
||||
let selectedOutlook = new Set();
|
||||
let selectedCustom = new Set();
|
||||
|
||||
// DOM 元素
|
||||
const elements = {
|
||||
// 统计
|
||||
outlookCount: document.getElementById('outlook-count'),
|
||||
customCount: document.getElementById('custom-count'),
|
||||
tempmailStatus: document.getElementById('tempmail-status'),
|
||||
totalEnabled: document.getElementById('total-enabled'),
|
||||
|
||||
// Outlook 导入
|
||||
toggleOutlookImport: document.getElementById('toggle-outlook-import'),
|
||||
outlookImportBody: document.getElementById('outlook-import-body'),
|
||||
outlookImportData: document.getElementById('outlook-import-data'),
|
||||
outlookImportEnabled: document.getElementById('outlook-import-enabled'),
|
||||
outlookImportPriority: document.getElementById('outlook-import-priority'),
|
||||
outlookImportBtn: document.getElementById('outlook-import-btn'),
|
||||
clearImportBtn: document.getElementById('clear-import-btn'),
|
||||
importResult: document.getElementById('import-result'),
|
||||
|
||||
// Outlook 列表
|
||||
outlookTable: document.getElementById('outlook-accounts-table'),
|
||||
selectAllOutlook: document.getElementById('select-all-outlook'),
|
||||
batchDeleteOutlookBtn: document.getElementById('batch-delete-outlook-btn'),
|
||||
|
||||
// 自定义域名
|
||||
customTable: document.getElementById('custom-services-table'),
|
||||
addCustomBtn: document.getElementById('add-custom-btn'),
|
||||
selectAllCustom: document.getElementById('select-all-custom'),
|
||||
|
||||
// 临时邮箱
|
||||
tempmailForm: document.getElementById('tempmail-form'),
|
||||
tempmailApi: document.getElementById('tempmail-api'),
|
||||
tempmailEnabled: document.getElementById('tempmail-enabled'),
|
||||
testTempmailBtn: document.getElementById('test-tempmail-btn'),
|
||||
|
||||
// 模态框
|
||||
addCustomModal: document.getElementById('add-custom-modal'),
|
||||
addCustomForm: document.getElementById('add-custom-form'),
|
||||
closeCustomModal: document.getElementById('close-custom-modal'),
|
||||
cancelAddCustom: document.getElementById('cancel-add-custom')
|
||||
};
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadStats();
|
||||
loadOutlookServices();
|
||||
loadCustomServices();
|
||||
loadTempmailConfig();
|
||||
initEventListeners();
|
||||
});
|
||||
|
||||
// 事件监听
|
||||
function initEventListeners() {
|
||||
// Outlook 导入展开/收起
|
||||
elements.toggleOutlookImport.addEventListener('click', () => {
|
||||
const isHidden = elements.outlookImportBody.style.display === 'none';
|
||||
elements.outlookImportBody.style.display = isHidden ? 'block' : 'none';
|
||||
elements.toggleOutlookImport.textContent = isHidden ? '收起' : '展开';
|
||||
});
|
||||
|
||||
// Outlook 导入
|
||||
elements.outlookImportBtn.addEventListener('click', handleOutlookImport);
|
||||
elements.clearImportBtn.addEventListener('click', () => {
|
||||
elements.outlookImportData.value = '';
|
||||
elements.importResult.style.display = 'none';
|
||||
});
|
||||
|
||||
// Outlook 全选
|
||||
elements.selectAllOutlook.addEventListener('change', (e) => {
|
||||
const checkboxes = elements.outlookTable.querySelectorAll('input[type="checkbox"][data-id]');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = e.target.checked;
|
||||
const id = parseInt(cb.dataset.id);
|
||||
if (e.target.checked) {
|
||||
selectedOutlook.add(id);
|
||||
} else {
|
||||
selectedOutlook.delete(id);
|
||||
}
|
||||
});
|
||||
updateBatchButtons();
|
||||
});
|
||||
|
||||
// Outlook 批量删除
|
||||
elements.batchDeleteOutlookBtn.addEventListener('click', handleBatchDeleteOutlook);
|
||||
|
||||
// 添加自定义域名
|
||||
elements.addCustomBtn.addEventListener('click', () => {
|
||||
elements.addCustomModal.classList.add('active');
|
||||
});
|
||||
|
||||
elements.closeCustomModal.addEventListener('click', () => {
|
||||
elements.addCustomModal.classList.remove('active');
|
||||
});
|
||||
|
||||
elements.cancelAddCustom.addEventListener('click', () => {
|
||||
elements.addCustomModal.classList.remove('active');
|
||||
});
|
||||
|
||||
elements.addCustomForm.addEventListener('submit', handleAddCustom);
|
||||
|
||||
// 自定义域名全选
|
||||
elements.selectAllCustom.addEventListener('change', (e) => {
|
||||
const checkboxes = elements.customTable.querySelectorAll('input[type="checkbox"][data-id]');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = e.target.checked;
|
||||
const id = parseInt(cb.dataset.id);
|
||||
if (e.target.checked) {
|
||||
selectedCustom.add(id);
|
||||
} else {
|
||||
selectedCustom.delete(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 临时邮箱配置
|
||||
elements.tempmailForm.addEventListener('submit', handleSaveTempmail);
|
||||
elements.testTempmailBtn.addEventListener('click', handleTestTempmail);
|
||||
}
|
||||
|
||||
// 加载统计信息
|
||||
async function loadStats() {
|
||||
try {
|
||||
const data = await api.get('/email-services/stats');
|
||||
elements.outlookCount.textContent = data.outlook_count || 0;
|
||||
elements.customCount.textContent = data.custom_count || 0;
|
||||
elements.tempmailStatus.textContent = data.tempmail_available ? '可用' : '不可用';
|
||||
elements.totalEnabled.textContent = data.enabled_count || 0;
|
||||
} catch (error) {
|
||||
console.error('加载统计信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载 Outlook 服务
|
||||
async function loadOutlookServices() {
|
||||
try {
|
||||
const data = await api.get('/email-services?service_type=outlook');
|
||||
outlookServices = data.services || [];
|
||||
|
||||
if (outlookServices.length === 0) {
|
||||
elements.outlookTable.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div class="empty-state-title">暂无 Outlook 账户</div>
|
||||
<div class="empty-state-description">请使用上方导入功能添加账户</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
elements.outlookTable.innerHTML = outlookServices.map(service => `
|
||||
<tr data-id="${service.id}">
|
||||
<td>
|
||||
<input type="checkbox" data-id="${service.id}"
|
||||
${selectedOutlook.has(service.id) ? 'checked' : ''}>
|
||||
</td>
|
||||
<td>${escapeHtml(service.config?.email || service.name)}</td>
|
||||
<td>
|
||||
<span class="status-badge ${service.config?.has_oauth ? 'active' : 'pending'}">
|
||||
${service.config?.has_oauth ? 'OAuth' : '密码'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge ${service.enabled ? 'active' : 'disabled'}">
|
||||
${service.enabled ? '启用' : '禁用'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${service.priority}</td>
|
||||
<td>${format.date(service.last_used)}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-ghost btn-sm" onclick="toggleService(${service.id}, ${!service.enabled})" title="${service.enabled ? '禁用' : '启用'}">
|
||||
${service.enabled ? '🔇' : '🔊'}
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="testService(${service.id})" title="测试">
|
||||
🔌
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="deleteService(${service.id}, '${escapeHtml(service.name)}')" title="删除">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
// 绑定复选框事件
|
||||
elements.outlookTable.querySelectorAll('input[type="checkbox"][data-id]').forEach(cb => {
|
||||
cb.addEventListener('change', (e) => {
|
||||
const id = parseInt(e.target.dataset.id);
|
||||
if (e.target.checked) {
|
||||
selectedOutlook.add(id);
|
||||
} else {
|
||||
selectedOutlook.delete(id);
|
||||
}
|
||||
updateBatchButtons();
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载 Outlook 服务失败:', error);
|
||||
elements.outlookTable.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">❌</div>
|
||||
<div class="empty-state-title">加载失败</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载自定义域名服务
|
||||
async function loadCustomServices() {
|
||||
try {
|
||||
const data = await api.get('/email-services?service_type=custom_domain');
|
||||
customServices = data.services || [];
|
||||
|
||||
if (customServices.length === 0) {
|
||||
elements.customTable.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div class="empty-state-title">暂无自定义域名服务</div>
|
||||
<div class="empty-state-description">点击"添加服务"按钮创建新服务</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
elements.customTable.innerHTML = customServices.map(service => `
|
||||
<tr data-id="${service.id}">
|
||||
<td>
|
||||
<input type="checkbox" data-id="${service.id}"
|
||||
${selectedCustom.has(service.id) ? 'checked' : ''}>
|
||||
</td>
|
||||
<td>${escapeHtml(service.name)}</td>
|
||||
<td style="font-size: 0.75rem;">${escapeHtml(service.config?.api_url || '-')}</td>
|
||||
<td>
|
||||
<span class="status-badge ${service.enabled ? 'active' : 'disabled'}">
|
||||
${service.enabled ? '启用' : '禁用'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${service.priority}</td>
|
||||
<td>${format.date(service.last_used)}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-ghost btn-sm" onclick="toggleService(${service.id}, ${!service.enabled})" title="${service.enabled ? '禁用' : '启用'}">
|
||||
${service.enabled ? '🔇' : '🔊'}
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="testService(${service.id})" title="测试">
|
||||
🔌
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="deleteService(${service.id}, '${escapeHtml(service.name)}')" title="删除">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
// 绑定复选框事件
|
||||
elements.customTable.querySelectorAll('input[type="checkbox"][data-id]').forEach(cb => {
|
||||
cb.addEventListener('change', (e) => {
|
||||
const id = parseInt(e.target.dataset.id);
|
||||
if (e.target.checked) {
|
||||
selectedCustom.add(id);
|
||||
} else {
|
||||
selectedCustom.delete(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载自定义域名服务失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载临时邮箱配置
|
||||
async function loadTempmailConfig() {
|
||||
try {
|
||||
const settings = await api.get('/settings');
|
||||
if (settings.tempmail) {
|
||||
elements.tempmailApi.value = settings.tempmail.api_url || '';
|
||||
elements.tempmailEnabled.checked = settings.tempmail.enabled !== false;
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
// Outlook 导入
|
||||
async function handleOutlookImport() {
|
||||
const data = elements.outlookImportData.value.trim();
|
||||
if (!data) {
|
||||
toast.error('请输入导入数据');
|
||||
return;
|
||||
}
|
||||
|
||||
elements.outlookImportBtn.disabled = true;
|
||||
elements.outlookImportBtn.textContent = '导入中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/email-services/outlook/batch-import', {
|
||||
data: data,
|
||||
enabled: elements.outlookImportEnabled.checked,
|
||||
priority: parseInt(elements.outlookImportPriority.value) || 0
|
||||
});
|
||||
|
||||
elements.importResult.style.display = 'block';
|
||||
elements.importResult.innerHTML = `
|
||||
<div class="import-stats">
|
||||
<span>✅ 成功导入: <strong>${result.success_count || 0}</strong></span>
|
||||
<span>❌ 失败: <strong>${result.failed_count || 0}</strong></span>
|
||||
</div>
|
||||
${result.errors?.length ? `
|
||||
<div class="import-errors" style="margin-top: var(--spacing-sm);">
|
||||
<strong>错误详情:</strong>
|
||||
<ul>
|
||||
${result.errors.map(e => `<li>${escapeHtml(e)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
if (result.success_count > 0) {
|
||||
toast.success(`成功导入 ${result.success_count} 个账户`);
|
||||
loadOutlookServices();
|
||||
loadStats();
|
||||
elements.outlookImportData.value = '';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
toast.error('导入失败: ' + error.message);
|
||||
} finally {
|
||||
elements.outlookImportBtn.disabled = false;
|
||||
elements.outlookImportBtn.textContent = '📥 开始导入';
|
||||
}
|
||||
}
|
||||
|
||||
// 添加自定义域名服务
|
||||
async function handleAddCustom(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const data = {
|
||||
service_type: 'custom_domain',
|
||||
name: formData.get('name'),
|
||||
config: {
|
||||
api_url: formData.get('api_url'),
|
||||
api_key: formData.get('api_key'),
|
||||
domain: formData.get('domain')
|
||||
},
|
||||
enabled: formData.get('enabled') === 'on',
|
||||
priority: parseInt(formData.get('priority')) || 0
|
||||
};
|
||||
|
||||
try {
|
||||
await api.post('/email-services', data);
|
||||
toast.success('服务添加成功');
|
||||
elements.addCustomModal.classList.remove('active');
|
||||
e.target.reset();
|
||||
loadCustomServices();
|
||||
loadStats();
|
||||
} catch (error) {
|
||||
toast.error('添加失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换服务状态
|
||||
async function toggleService(id, enabled) {
|
||||
try {
|
||||
await api.patch(`/email-services/${id}`, { enabled });
|
||||
toast.success(enabled ? '已启用' : '已禁用');
|
||||
loadOutlookServices();
|
||||
loadCustomServices();
|
||||
loadStats();
|
||||
} catch (error) {
|
||||
toast.error('操作失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 测试服务
|
||||
async function testService(id) {
|
||||
try {
|
||||
const result = await api.post(`/email-services/${id}/test`);
|
||||
if (result.success) {
|
||||
toast.success('测试成功');
|
||||
} else {
|
||||
toast.error('测试失败: ' + (result.error || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('测试失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除服务
|
||||
async function deleteService(id, name) {
|
||||
const confirmed = await confirm(`确定要删除 "${name}" 吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await api.delete(`/email-services/${id}`);
|
||||
toast.success('已删除');
|
||||
selectedOutlook.delete(id);
|
||||
selectedCustom.delete(id);
|
||||
loadOutlookServices();
|
||||
loadCustomServices();
|
||||
loadStats();
|
||||
} catch (error) {
|
||||
toast.error('删除失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除 Outlook
|
||||
async function handleBatchDeleteOutlook() {
|
||||
if (selectedOutlook.size === 0) return;
|
||||
|
||||
const confirmed = await confirm(`确定要删除选中的 ${selectedOutlook.size} 个账户吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const result = await api.request('/email-services/outlook/batch', {
|
||||
method: 'DELETE',
|
||||
body: Array.from(selectedOutlook)
|
||||
});
|
||||
toast.success(`成功删除 ${result.deleted || selectedOutlook.size} 个账户`);
|
||||
selectedOutlook.clear();
|
||||
loadOutlookServices();
|
||||
loadStats();
|
||||
} catch (error) {
|
||||
toast.error('删除失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存临时邮箱配置
|
||||
async function handleSaveTempmail(e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await api.post('/settings/tempmail', {
|
||||
api_url: elements.tempmailApi.value,
|
||||
enabled: elements.tempmailEnabled.checked
|
||||
});
|
||||
toast.success('配置已保存');
|
||||
} catch (error) {
|
||||
toast.error('保存失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 测试临时邮箱
|
||||
async function handleTestTempmail() {
|
||||
elements.testTempmailBtn.disabled = true;
|
||||
elements.testTempmailBtn.textContent = '测试中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/email-services/test-tempmail', {
|
||||
api_url: elements.tempmailApi.value
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success('临时邮箱连接正常');
|
||||
} else {
|
||||
toast.error('连接失败: ' + (result.error || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('测试失败: ' + error.message);
|
||||
} finally {
|
||||
elements.testTempmailBtn.disabled = false;
|
||||
elements.testTempmailBtn.textContent = '🔌 测试连接';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新批量按钮
|
||||
function updateBatchButtons() {
|
||||
const count = selectedOutlook.size;
|
||||
elements.batchDeleteOutlookBtn.disabled = count === 0;
|
||||
elements.batchDeleteOutlookBtn.textContent = count > 0 ? `🗑️ 删除选中 (${count})` : '🗑️ 批量删除';
|
||||
}
|
||||
|
||||
// HTML 转义
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
@@ -28,7 +28,16 @@ const elements = {
|
||||
outlookImportData: document.getElementById('outlook-import-data'),
|
||||
importResult: document.getElementById('import-result'),
|
||||
// 批量操作
|
||||
selectAllServices: document.getElementById('select-all-services')
|
||||
selectAllServices: document.getElementById('select-all-services'),
|
||||
// 代理列表
|
||||
proxiesTable: document.getElementById('proxies-table'),
|
||||
addProxyBtn: document.getElementById('add-proxy-btn'),
|
||||
testAllProxiesBtn: document.getElementById('test-all-proxies-btn'),
|
||||
addProxyModal: document.getElementById('add-proxy-modal'),
|
||||
proxyItemForm: document.getElementById('proxy-item-form'),
|
||||
closeProxyModal: document.getElementById('close-proxy-modal'),
|
||||
cancelProxyBtn: document.getElementById('cancel-proxy-btn'),
|
||||
proxyModalTitle: document.getElementById('proxy-modal-title')
|
||||
};
|
||||
|
||||
// 选中的服务 ID
|
||||
@@ -40,6 +49,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
loadSettings();
|
||||
loadEmailServices();
|
||||
loadDatabaseInfo();
|
||||
loadProxies();
|
||||
initEventListeners();
|
||||
});
|
||||
|
||||
@@ -61,47 +71,69 @@ function initTabs() {
|
||||
// 事件监听
|
||||
function initEventListeners() {
|
||||
// 代理表单
|
||||
elements.proxyForm.addEventListener('submit', handleSaveProxy);
|
||||
if (elements.proxyForm) {
|
||||
elements.proxyForm.addEventListener('submit', handleSaveProxy);
|
||||
}
|
||||
|
||||
// 测试代理
|
||||
elements.testProxyBtn.addEventListener('click', handleTestProxy);
|
||||
if (elements.testProxyBtn) {
|
||||
elements.testProxyBtn.addEventListener('click', handleTestProxy);
|
||||
}
|
||||
|
||||
// 注册配置表单
|
||||
elements.registrationForm.addEventListener('submit', handleSaveRegistration);
|
||||
if (elements.registrationForm) {
|
||||
elements.registrationForm.addEventListener('submit', handleSaveRegistration);
|
||||
}
|
||||
|
||||
// 备份数据库
|
||||
elements.backupBtn.addEventListener('click', handleBackup);
|
||||
if (elements.backupBtn) {
|
||||
elements.backupBtn.addEventListener('click', handleBackup);
|
||||
}
|
||||
|
||||
// 清理数据
|
||||
elements.cleanupBtn.addEventListener('click', handleCleanup);
|
||||
if (elements.cleanupBtn) {
|
||||
elements.cleanupBtn.addEventListener('click', handleCleanup);
|
||||
}
|
||||
|
||||
// 添加邮箱服务
|
||||
elements.addEmailServiceBtn.addEventListener('click', () => {
|
||||
elements.addServiceModal.classList.add('active');
|
||||
loadServiceConfigFields(elements.serviceType.value);
|
||||
});
|
||||
if (elements.addEmailServiceBtn) {
|
||||
elements.addEmailServiceBtn.addEventListener('click', () => {
|
||||
elements.addServiceModal.classList.add('active');
|
||||
loadServiceConfigFields(elements.serviceType.value);
|
||||
});
|
||||
}
|
||||
|
||||
elements.closeServiceModal.addEventListener('click', () => {
|
||||
elements.addServiceModal.classList.remove('active');
|
||||
});
|
||||
|
||||
elements.cancelAddService.addEventListener('click', () => {
|
||||
elements.addServiceModal.classList.remove('active');
|
||||
});
|
||||
|
||||
elements.addServiceModal.addEventListener('click', (e) => {
|
||||
if (e.target === elements.addServiceModal) {
|
||||
if (elements.closeServiceModal) {
|
||||
elements.closeServiceModal.addEventListener('click', () => {
|
||||
elements.addServiceModal.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.cancelAddService) {
|
||||
elements.cancelAddService.addEventListener('click', () => {
|
||||
elements.addServiceModal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.addServiceModal) {
|
||||
elements.addServiceModal.addEventListener('click', (e) => {
|
||||
if (e.target === elements.addServiceModal) {
|
||||
elements.addServiceModal.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 服务类型切换
|
||||
elements.serviceType.addEventListener('change', (e) => {
|
||||
loadServiceConfigFields(e.target.value);
|
||||
});
|
||||
if (elements.serviceType) {
|
||||
elements.serviceType.addEventListener('change', (e) => {
|
||||
loadServiceConfigFields(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加服务表单
|
||||
elements.addServiceForm.addEventListener('submit', handleAddService);
|
||||
if (elements.addServiceForm) {
|
||||
elements.addServiceForm.addEventListener('submit', handleAddService);
|
||||
}
|
||||
|
||||
// Outlook 批量导入展开/折叠
|
||||
if (elements.toggleImportBtn) {
|
||||
@@ -133,6 +165,35 @@ function initEventListeners() {
|
||||
updateSelectedServices();
|
||||
});
|
||||
}
|
||||
|
||||
// 代理列表相关
|
||||
if (elements.addProxyBtn) {
|
||||
elements.addProxyBtn.addEventListener('click', () => openProxyModal());
|
||||
}
|
||||
|
||||
if (elements.testAllProxiesBtn) {
|
||||
elements.testAllProxiesBtn.addEventListener('click', handleTestAllProxies);
|
||||
}
|
||||
|
||||
if (elements.closeProxyModal) {
|
||||
elements.closeProxyModal.addEventListener('click', closeProxyModal);
|
||||
}
|
||||
|
||||
if (elements.cancelProxyBtn) {
|
||||
elements.cancelProxyBtn.addEventListener('click', closeProxyModal);
|
||||
}
|
||||
|
||||
if (elements.addProxyModal) {
|
||||
elements.addProxyModal.addEventListener('click', (e) => {
|
||||
if (e.target === elements.addProxyModal) {
|
||||
closeProxyModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.proxyItemForm) {
|
||||
elements.proxyItemForm.addEventListener('submit', handleSaveProxyItem);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
@@ -162,26 +223,34 @@ async function loadSettings() {
|
||||
|
||||
// 加载邮箱服务
|
||||
async function loadEmailServices() {
|
||||
// 检查元素是否存在
|
||||
if (!elements.emailServicesTable) return;
|
||||
|
||||
try {
|
||||
const data = await api.get('/email-services');
|
||||
renderEmailServices(data.services);
|
||||
} catch (error) {
|
||||
console.error('加载邮箱服务失败:', error);
|
||||
elements.emailServicesTable.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">❌</div>
|
||||
<div class="empty-state-title">加载失败</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
if (elements.emailServicesTable) {
|
||||
elements.emailServicesTable.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">❌</div>
|
||||
<div class="empty-state-title">加载失败</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染邮箱服务
|
||||
function renderEmailServices(services) {
|
||||
// 检查元素是否存在
|
||||
if (!elements.emailServicesTable) return;
|
||||
|
||||
if (services.length === 0) {
|
||||
elements.emailServicesTable.innerHTML = `
|
||||
<tr>
|
||||
@@ -271,9 +340,24 @@ async function handleTestProxy() {
|
||||
elements.testProxyBtn.innerHTML = '<span class="loading-spinner"></span> 测试中...';
|
||||
|
||||
try {
|
||||
// TODO: 实现代理测试 API
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
toast.info('代理测试功能待实现');
|
||||
const data = {
|
||||
enabled: document.getElementById('proxy-enabled').checked,
|
||||
type: document.getElementById('proxy-type').value,
|
||||
host: document.getElementById('proxy-host').value,
|
||||
port: parseInt(document.getElementById('proxy-port').value),
|
||||
username: document.getElementById('proxy-username').value || null,
|
||||
password: document.getElementById('proxy-password').value || null,
|
||||
};
|
||||
|
||||
const result = await api.post('/settings/proxy/test', data);
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('测试失败: ' + error.message);
|
||||
} finally {
|
||||
elements.testProxyBtn.disabled = false;
|
||||
elements.testProxyBtn.textContent = '🔌 测试连接';
|
||||
@@ -543,3 +627,199 @@ function escapeHtml(text) {
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// 代理列表管理
|
||||
// ============================================================================
|
||||
|
||||
// 加载代理列表
|
||||
async function loadProxies() {
|
||||
try {
|
||||
const data = await api.get('/settings/proxies');
|
||||
renderProxies(data.proxies);
|
||||
} catch (error) {
|
||||
console.error('加载代理列表失败:', error);
|
||||
elements.proxiesTable.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">❌</div>
|
||||
<div class="empty-state-title">加载失败</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染代理列表
|
||||
function renderProxies(proxies) {
|
||||
if (!proxies || proxies.length === 0) {
|
||||
elements.proxiesTable.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🌐</div>
|
||||
<div class="empty-state-title">暂无代理</div>
|
||||
<div class="empty-state-description">点击"添加代理"按钮添加代理服务器</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
elements.proxiesTable.innerHTML = proxies.map(proxy => `
|
||||
<tr data-proxy-id="${proxy.id}">
|
||||
<td>${proxy.id}</td>
|
||||
<td>${escapeHtml(proxy.name)}</td>
|
||||
<td><span class="badge">${proxy.type.toUpperCase()}</span></td>
|
||||
<td><code>${escapeHtml(proxy.host)}:${proxy.port}</code></td>
|
||||
<td>
|
||||
<span class="status-badge ${proxy.enabled ? 'active' : 'disabled'}">
|
||||
${proxy.enabled ? '已启用' : '已禁用'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${format.date(proxy.last_used)}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-ghost btn-sm" onclick="testProxyItem(${proxy.id})" title="测试">
|
||||
🔌
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="editProxyItem(${proxy.id})" title="编辑">
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="toggleProxyItem(${proxy.id}, ${!proxy.enabled})" title="${proxy.enabled ? '禁用' : '启用'}">
|
||||
${proxy.enabled ? '🔒' : '🔓'}
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="deleteProxyItem(${proxy.id})" title="删除">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 打开代理模态框
|
||||
function openProxyModal(proxy = null) {
|
||||
elements.proxyModalTitle.textContent = proxy ? '编辑代理' : '添加代理';
|
||||
elements.proxyItemForm.reset();
|
||||
|
||||
document.getElementById('proxy-item-id').value = proxy ? proxy.id : '';
|
||||
|
||||
if (proxy) {
|
||||
document.getElementById('proxy-item-name').value = proxy.name || '';
|
||||
document.getElementById('proxy-item-type').value = proxy.type || 'http';
|
||||
document.getElementById('proxy-item-host').value = proxy.host || '';
|
||||
document.getElementById('proxy-item-port').value = proxy.port || '';
|
||||
document.getElementById('proxy-item-username').value = proxy.username || '';
|
||||
document.getElementById('proxy-item-password').value = '';
|
||||
}
|
||||
|
||||
elements.addProxyModal.classList.add('active');
|
||||
}
|
||||
|
||||
// 关闭代理模态框
|
||||
function closeProxyModal() {
|
||||
elements.addProxyModal.classList.remove('active');
|
||||
elements.proxyItemForm.reset();
|
||||
}
|
||||
|
||||
// 保存代理
|
||||
async function handleSaveProxyItem(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const proxyId = document.getElementById('proxy-item-id').value;
|
||||
const data = {
|
||||
name: document.getElementById('proxy-item-name').value,
|
||||
type: document.getElementById('proxy-item-type').value,
|
||||
host: document.getElementById('proxy-item-host').value,
|
||||
port: parseInt(document.getElementById('proxy-item-port').value),
|
||||
username: document.getElementById('proxy-item-username').value || null,
|
||||
password: document.getElementById('proxy-item-password').value || null,
|
||||
enabled: true
|
||||
};
|
||||
|
||||
try {
|
||||
if (proxyId) {
|
||||
await api.patch(`/settings/proxies/${proxyId}`, data);
|
||||
toast.success('代理已更新');
|
||||
} else {
|
||||
await api.post('/settings/proxies', data);
|
||||
toast.success('代理已添加');
|
||||
}
|
||||
closeProxyModal();
|
||||
loadProxies();
|
||||
} catch (error) {
|
||||
toast.error('保存失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑代理
|
||||
async function editProxyItem(id) {
|
||||
try {
|
||||
const proxy = await api.get(`/settings/proxies/${id}`);
|
||||
openProxyModal(proxy);
|
||||
} catch (error) {
|
||||
toast.error('获取代理信息失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 测试单个代理
|
||||
async function testProxyItem(id) {
|
||||
try {
|
||||
const result = await api.post(`/settings/proxies/${id}/test`);
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('测试失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换代理状态
|
||||
async function toggleProxyItem(id, enabled) {
|
||||
try {
|
||||
const endpoint = enabled ? 'enable' : 'disable';
|
||||
await api.post(`/settings/proxies/${id}/${endpoint}`);
|
||||
toast.success(enabled ? '代理已启用' : '代理已禁用');
|
||||
loadProxies();
|
||||
} catch (error) {
|
||||
toast.error('操作失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除代理
|
||||
async function deleteProxyItem(id) {
|
||||
const confirmed = await confirm('确定要删除此代理吗?');
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await api.delete(`/settings/proxies/${id}`);
|
||||
toast.success('代理已删除');
|
||||
loadProxies();
|
||||
} catch (error) {
|
||||
toast.error('删除失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 测试所有代理
|
||||
async function handleTestAllProxies() {
|
||||
elements.testAllProxiesBtn.disabled = true;
|
||||
elements.testAllProxiesBtn.innerHTML = '<span class="loading-spinner"></span> 测试中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/settings/proxies/test-all');
|
||||
toast.info(`测试完成: 成功 ${result.success}, 失败 ${result.failed}`);
|
||||
loadProxies();
|
||||
} catch (error) {
|
||||
toast.error('测试失败: ' + error.message);
|
||||
} finally {
|
||||
elements.testAllProxiesBtn.disabled = false;
|
||||
elements.testAllProxiesBtn.textContent = '🔌 测试全部';
|
||||
}
|
||||
}
|
||||
|
||||
495
static/js/utils.js
Normal file
495
static/js/utils.js
Normal file
@@ -0,0 +1,495 @@
|
||||
/**
|
||||
* 通用工具库
|
||||
* 包含 Toast 通知、主题切换、工具函数等
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// Toast 通知系统
|
||||
// ============================================
|
||||
|
||||
class ToastManager {
|
||||
constructor() {
|
||||
this.container = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = 'toast-container';
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
|
||||
show(message, type = 'info', duration = 4000) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
|
||||
const icon = this.getIcon(type);
|
||||
toast.innerHTML = `
|
||||
<span class="toast-icon">${icon}</span>
|
||||
<span class="toast-message">${this.escapeHtml(message)}</span>
|
||||
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
|
||||
`;
|
||||
|
||||
this.container.appendChild(toast);
|
||||
|
||||
// 自动移除
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease forwards';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, duration);
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
getIcon(type) {
|
||||
const icons = {
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
warning: '⚠',
|
||||
info: 'ℹ'
|
||||
};
|
||||
return icons[type] || icons.info;
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
success(message, duration) {
|
||||
return this.show(message, 'success', duration);
|
||||
}
|
||||
|
||||
error(message, duration) {
|
||||
return this.show(message, 'error', duration);
|
||||
}
|
||||
|
||||
warning(message, duration) {
|
||||
return this.show(message, 'warning', duration);
|
||||
}
|
||||
|
||||
info(message, duration) {
|
||||
return this.show(message, 'info', duration);
|
||||
}
|
||||
}
|
||||
|
||||
// 全局 Toast 实例
|
||||
const toast = new ToastManager();
|
||||
|
||||
// ============================================
|
||||
// 主题管理
|
||||
// ============================================
|
||||
|
||||
class ThemeManager {
|
||||
constructor() {
|
||||
this.theme = this.loadTheme();
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
loadTheme() {
|
||||
return localStorage.getItem('theme') || 'light';
|
||||
}
|
||||
|
||||
saveTheme(theme) {
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
|
||||
applyTheme() {
|
||||
document.documentElement.setAttribute('data-theme', this.theme);
|
||||
this.updateToggleButtons();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.theme = this.theme === 'light' ? 'dark' : 'light';
|
||||
this.saveTheme(this.theme);
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
setTheme(theme) {
|
||||
this.theme = theme;
|
||||
this.saveTheme(theme);
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
updateToggleButtons() {
|
||||
const buttons = document.querySelectorAll('.theme-toggle');
|
||||
buttons.forEach(btn => {
|
||||
btn.innerHTML = this.theme === 'light' ? '🌙' : '☀️';
|
||||
btn.title = this.theme === 'light' ? '切换到暗色模式' : '切换到亮色模式';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 全局主题实例
|
||||
const theme = new ThemeManager();
|
||||
|
||||
// ============================================
|
||||
// 加载状态管理
|
||||
// ============================================
|
||||
|
||||
class LoadingManager {
|
||||
constructor() {
|
||||
this.activeLoaders = new Set();
|
||||
}
|
||||
|
||||
show(element, text = '加载中...') {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
if (!element) return;
|
||||
|
||||
element.classList.add('loading');
|
||||
element.dataset.originalText = element.innerHTML;
|
||||
element.innerHTML = `<span class="loading-spinner"></span> ${text}`;
|
||||
element.disabled = true;
|
||||
this.activeLoaders.add(element);
|
||||
}
|
||||
|
||||
hide(element) {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
if (!element) return;
|
||||
|
||||
element.classList.remove('loading');
|
||||
if (element.dataset.originalText) {
|
||||
element.innerHTML = element.dataset.originalText;
|
||||
delete element.dataset.originalText;
|
||||
}
|
||||
element.disabled = false;
|
||||
this.activeLoaders.delete(element);
|
||||
}
|
||||
|
||||
hideAll() {
|
||||
this.activeLoaders.forEach(element => this.hide(element));
|
||||
}
|
||||
}
|
||||
|
||||
const loading = new LoadingManager();
|
||||
|
||||
// ============================================
|
||||
// API 请求封装
|
||||
// ============================================
|
||||
|
||||
class ApiClient {
|
||||
constructor(baseUrl = '/api') {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
async request(path, options = {}) {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const finalOptions = { ...defaultOptions, ...options };
|
||||
|
||||
if (finalOptions.body && typeof finalOptions.body === 'object') {
|
||||
finalOptions.body = JSON.stringify(finalOptions.body);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, finalOptions);
|
||||
const data = await response.json().catch(() => ({}));
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(data.detail || `HTTP ${response.status}`);
|
||||
error.response = response;
|
||||
error.data = data;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
// 网络错误处理
|
||||
if (!error.response) {
|
||||
toast.error('网络连接失败,请检查网络');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
get(path, options = {}) {
|
||||
return this.request(path, { ...options, method: 'GET' });
|
||||
}
|
||||
|
||||
post(path, body, options = {}) {
|
||||
return this.request(path, { ...options, method: 'POST', body });
|
||||
}
|
||||
|
||||
put(path, body, options = {}) {
|
||||
return this.request(path, { ...options, method: 'PUT', body });
|
||||
}
|
||||
|
||||
delete(path, options = {}) {
|
||||
return this.request(path, { ...options, method: 'DELETE' });
|
||||
}
|
||||
}
|
||||
|
||||
const api = new ApiClient();
|
||||
|
||||
// ============================================
|
||||
// 事件委托助手
|
||||
// ============================================
|
||||
|
||||
function delegate(element, eventType, selector, handler) {
|
||||
element.addEventListener(eventType, (e) => {
|
||||
const target = e.target.closest(selector);
|
||||
if (target && element.contains(target)) {
|
||||
handler.call(target, e, target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 防抖和节流
|
||||
// ============================================
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
function throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function executedFunction(...args) {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 格式化工具
|
||||
// ============================================
|
||||
|
||||
const format = {
|
||||
date(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
dateShort(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('zh-CN');
|
||||
},
|
||||
|
||||
relativeTime(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (seconds < 60) return '刚刚';
|
||||
if (minutes < 60) return `${minutes} 分钟前`;
|
||||
if (hours < 24) return `${hours} 小时前`;
|
||||
if (days < 7) return `${days} 天前`;
|
||||
return this.dateShort(dateStr);
|
||||
},
|
||||
|
||||
bytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
},
|
||||
|
||||
number(num) {
|
||||
if (num === null || num === undefined) return '-';
|
||||
return num.toLocaleString('zh-CN');
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 状态映射
|
||||
// ============================================
|
||||
|
||||
const statusMap = {
|
||||
account: {
|
||||
active: { text: '活跃', class: 'active' },
|
||||
expired: { text: '过期', class: 'expired' },
|
||||
banned: { text: '封禁', class: 'banned' },
|
||||
failed: { text: '失败', class: 'failed' }
|
||||
},
|
||||
task: {
|
||||
pending: { text: '等待中', class: 'pending' },
|
||||
running: { text: '运行中', class: 'running' },
|
||||
completed: { text: '已完成', class: 'completed' },
|
||||
failed: { text: '失败', class: 'failed' },
|
||||
cancelled: { text: '已取消', class: 'disabled' }
|
||||
},
|
||||
service: {
|
||||
tempmail: 'Tempmail.lol',
|
||||
outlook: 'Outlook',
|
||||
custom_domain: '自定义域名'
|
||||
}
|
||||
};
|
||||
|
||||
function getStatusText(type, status) {
|
||||
return statusMap[type]?.[status]?.text || status;
|
||||
}
|
||||
|
||||
function getStatusClass(type, status) {
|
||||
return statusMap[type]?.[status]?.class || '';
|
||||
}
|
||||
|
||||
function getServiceTypeText(type) {
|
||||
return statusMap.service[type] || type;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 确认对话框
|
||||
// ============================================
|
||||
|
||||
function confirm(message, title = '确认操作') {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal active';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width: 400px;">
|
||||
<div class="modal-header">
|
||||
<h3>${title}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="margin-bottom: var(--spacing-lg);">${message}</p>
|
||||
<div class="form-actions" style="margin-top: 0; padding-top: 0; border-top: none;">
|
||||
<button class="btn btn-secondary" id="confirm-cancel">取消</button>
|
||||
<button class="btn btn-danger" id="confirm-ok">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const cancelBtn = modal.querySelector('#confirm-cancel');
|
||||
const okBtn = modal.querySelector('#confirm-ok');
|
||||
|
||||
cancelBtn.onclick = () => {
|
||||
modal.remove();
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
okBtn.onclick = () => {
|
||||
modal.remove();
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.remove();
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 复制到剪贴板
|
||||
// ============================================
|
||||
|
||||
async function copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast.success('已复制到剪贴板');
|
||||
return true;
|
||||
} catch (err) {
|
||||
toast.error('复制失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 本地存储助手
|
||||
// ============================================
|
||||
|
||||
const storage = {
|
||||
get(key, defaultValue = null) {
|
||||
try {
|
||||
const value = localStorage.getItem(key);
|
||||
return value ? JSON.parse(value) : defaultValue;
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
set(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
remove(key) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 页面初始化
|
||||
// ============================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 初始化主题
|
||||
theme.applyTheme();
|
||||
|
||||
// 全局键盘快捷键
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Ctrl/Cmd + K: 聚焦搜索
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
const searchInput = document.querySelector('#search-input, [type="search"]');
|
||||
if (searchInput) searchInput.focus();
|
||||
}
|
||||
|
||||
// Escape: 关闭模态框
|
||||
if (e.key === 'Escape') {
|
||||
const activeModal = document.querySelector('.modal.active');
|
||||
if (activeModal) activeModal.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 导出全局对象
|
||||
window.toast = toast;
|
||||
window.theme = theme;
|
||||
window.loading = loading;
|
||||
window.api = api;
|
||||
window.format = format;
|
||||
window.confirm = confirm;
|
||||
window.copyToClipboard = copyToClipboard;
|
||||
window.storage = storage;
|
||||
window.delegate = delegate;
|
||||
window.debounce = debounce;
|
||||
window.throttle = throttle;
|
||||
window.getStatusText = getStatusText;
|
||||
window.getStatusClass = getStatusClass;
|
||||
window.getServiceTypeText = getServiceTypeText;
|
||||
@@ -6,6 +6,33 @@
|
||||
<title>账号管理 - OpenAI 注册系统</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📋</text></svg>">
|
||||
<style>
|
||||
.password-cell {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.password-hidden {
|
||||
filter: blur(4px);
|
||||
cursor: pointer;
|
||||
transition: filter 0.2s;
|
||||
}
|
||||
.password-hidden:hover {
|
||||
filter: blur(0);
|
||||
}
|
||||
.token-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.token-status .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.token-status .dot.healthy { background: var(--success-color); }
|
||||
.token-status .dot.warning { background: var(--warning-color); }
|
||||
.token-status .dot.expired { background: var(--danger-color); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@@ -17,6 +44,7 @@
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link">注册</a>
|
||||
<a href="/accounts" class="nav-link active">账号管理</a>
|
||||
<a href="/email-services" class="nav-link">邮箱服务</a>
|
||||
<a href="/settings" class="nav-link">设置</a>
|
||||
</div>
|
||||
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
||||
@@ -77,6 +105,12 @@
|
||||
<button class="btn btn-ghost" id="refresh-btn" title="刷新">
|
||||
🔄 刷新
|
||||
</button>
|
||||
<button class="btn btn-warning" id="batch-refresh-btn" disabled title="批量刷新Token">
|
||||
🔄 刷新Token
|
||||
</button>
|
||||
<button class="btn btn-info" id="batch-validate-btn" disabled title="批量验证Token">
|
||||
✅ 验证Token
|
||||
</button>
|
||||
<button class="btn btn-danger" id="batch-delete-btn" disabled>
|
||||
🗑️ 批量删除
|
||||
</button>
|
||||
@@ -103,15 +137,16 @@
|
||||
<th style="width: 40px;"><input type="checkbox" id="select-all"></th>
|
||||
<th style="width: 60px;">ID</th>
|
||||
<th>邮箱</th>
|
||||
<th style="width: 100px;">密码</th>
|
||||
<th style="width: 120px;">邮箱服务</th>
|
||||
<th style="width: 100px;">状态</th>
|
||||
<th style="width: 160px;">注册时间</th>
|
||||
<th style="width: 80px;">状态</th>
|
||||
<th style="width: 140px;">最后刷新</th>
|
||||
<th style="width: 140px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="accounts-table">
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<td colspan="8">
|
||||
<div class="empty-state">
|
||||
<div class="skeleton skeleton-text" style="width: 60%;"></div>
|
||||
<div class="skeleton skeleton-text" style="width: 80%;"></div>
|
||||
|
||||
240
templates/email_services.html
Normal file
240
templates/email_services.html
Normal file
@@ -0,0 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>邮箱服务 - OpenAI 注册系统</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📧</text></svg>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar">
|
||||
<div class="nav-brand">
|
||||
<h1>OpenAI 注册系统</h1>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link">注册</a>
|
||||
<a href="/accounts" class="nav-link">账号管理</a>
|
||||
<a href="/email-services" class="nav-link active">邮箱服务</a>
|
||||
<a href="/settings" class="nav-link">设置</a>
|
||||
</div>
|
||||
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
||||
🌙
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h2>邮箱服务管理</h2>
|
||||
<p class="subtitle">配置和管理注册所需的邮箱服务</p>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card info">
|
||||
<div class="stat-value" id="outlook-count">0</div>
|
||||
<div class="stat-label">Outlook 账户</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-value" id="custom-count">0</div>
|
||||
<div class="stat-label">自定义域名</div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-value" id="tempmail-status">可用</div>
|
||||
<div class="stat-label">临时邮箱</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="total-enabled">0</div>
|
||||
<div class="stat-label">已启用服务</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outlook 管理 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📥 Outlook 批量导入</h3>
|
||||
<button class="btn btn-ghost btn-sm" id="toggle-outlook-import">展开</button>
|
||||
</div>
|
||||
<div class="card-body" id="outlook-import-body" style="display: none;">
|
||||
<div class="import-info">
|
||||
<p><strong>支持格式:</strong></p>
|
||||
<ul>
|
||||
<li><code>邮箱----密码</code> (密码认证)</li>
|
||||
<li><code>邮箱----密码----client_id----refresh_token</code> (XOAUTH2 认证,推荐)</li>
|
||||
</ul>
|
||||
<p>每行一个账户,使用四个连字符(----)分隔字段。以 # 开头的行将被忽略。</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="outlook-import-data">批量导入数据</label>
|
||||
<textarea id="outlook-import-data" rows="8" placeholder="example@outlook.com----password123 test@outlook.com----password456----client_id----refresh_token"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="outlook-import-enabled" checked>
|
||||
导入后启用
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="outlook-import-priority">优先级</label>
|
||||
<input type="number" id="outlook-import-priority" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-primary" id="outlook-import-btn">📥 开始导入</button>
|
||||
<button type="button" class="btn btn-secondary" id="clear-import-btn">清空</button>
|
||||
</div>
|
||||
<div id="import-result" style="display: none; margin-top: var(--spacing-md);"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义域名管理 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🔗 自定义域名服务</h3>
|
||||
<button class="btn btn-primary btn-sm" id="add-custom-btn">➕ 添加服务</button>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;"><input type="checkbox" id="select-all-custom"></th>
|
||||
<th>名称</th>
|
||||
<th style="width: 200px;">API 地址</th>
|
||||
<th style="width: 100px;">状态</th>
|
||||
<th style="width: 80px;">优先级</th>
|
||||
<th style="width: 160px;">最后使用</th>
|
||||
<th style="width: 180px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="custom-services-table">
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text" style="width: 80%;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outlook 账户列表 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📧 Outlook 账户列表</h3>
|
||||
<button class="btn btn-danger btn-sm" id="batch-delete-outlook-btn" disabled>🗑️ 批量删除</button>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;"><input type="checkbox" id="select-all-outlook"></th>
|
||||
<th>邮箱</th>
|
||||
<th style="width: 100px;">认证方式</th>
|
||||
<th style="width: 100px;">状态</th>
|
||||
<th style="width: 80px;">优先级</th>
|
||||
<th style="width: 160px;">最后使用</th>
|
||||
<th style="width: 140px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="outlook-accounts-table">
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text" style="width: 80%;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 临时邮箱配置 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🌐 临时邮箱配置</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="tempmail-form">
|
||||
<div class="form-group">
|
||||
<label for="tempmail-api">Tempmail.lol API 地址</label>
|
||||
<input type="text" id="tempmail-api" name="api_url" placeholder="https://tempmail.lol/api">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="tempmail-enabled" checked>
|
||||
启用临时邮箱
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">💾 保存设置</button>
|
||||
<button type="button" class="btn btn-secondary" id="test-tempmail-btn">🔌 测试连接</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 添加自定义域名模态框 -->
|
||||
<div class="modal" id="add-custom-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>➕ 添加自定义域名服务</h3>
|
||||
<button class="modal-close" id="close-custom-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="add-custom-form">
|
||||
<div class="form-group">
|
||||
<label for="custom-name">服务名称</label>
|
||||
<input type="text" id="custom-name" name="name" required placeholder="例如:我的域名邮箱">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="custom-api-url">API 地址</label>
|
||||
<input type="text" id="custom-api-url" name="api_url" required placeholder="https://api.example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="custom-api-key">API 密钥 (可选)</label>
|
||||
<input type="text" id="custom-api-key" name="api_key" placeholder="API Key">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="custom-domain">邮箱域名</label>
|
||||
<input type="text" id="custom-domain" name="domain" placeholder="example.com">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="custom-priority">优先级</label>
|
||||
<input type="number" id="custom-priority" name="priority" value="0" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="custom-enabled" name="enabled" checked>
|
||||
启用服务
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">添加</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancel-add-custom">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/utils.js"></script>
|
||||
<script src="/static/js/email_services.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,6 +6,94 @@
|
||||
<title>注册控制台 - OpenAI 注册系统</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚀</text></svg>">
|
||||
<style>
|
||||
/* 两栏布局 */
|
||||
.two-column-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 30% 70%;
|
||||
gap: var(--spacing-lg);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
position: sticky;
|
||||
top: calc(60px + var(--spacing-lg));
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
/* 任务状态行 */
|
||||
.task-status-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--surface-hover);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.task-status-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task-status-item .label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.task-status-item .value {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* 已注册账号列表 */
|
||||
.recent-accounts-table {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.recent-accounts-table table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.recent-accounts-table th,
|
||||
.recent-accounts-table td {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.password-cell {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.password-hidden {
|
||||
filter: blur(4px);
|
||||
cursor: pointer;
|
||||
transition: filter 0.2s;
|
||||
}
|
||||
|
||||
.password-hidden:hover {
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1024px) {
|
||||
.two-column-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@@ -17,6 +105,7 @@
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link active">注册</a>
|
||||
<a href="/accounts" class="nav-link">账号管理</a>
|
||||
<a href="/email-services" class="nav-link">邮箱服务</a>
|
||||
<a href="/settings" class="nav-link">设置</a>
|
||||
</div>
|
||||
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
||||
@@ -24,132 +113,153 @@
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<!-- 主内容区 - 两栏布局 -->
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h2>注册控制台</h2>
|
||||
<p class="subtitle">启动新的 OpenAI/Codex CLI 账号注册任务</p>
|
||||
</div>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📝 新建注册任务</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="registration-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="email-service">邮箱服务</label>
|
||||
<select id="email-service" name="email_service" required>
|
||||
<option value="tempmail">Tempmail.lol (临时邮箱)</option>
|
||||
<option value="outlook">Outlook</option>
|
||||
<option value="custom_domain">自定义域名</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy">代理地址 (可选)</label>
|
||||
<input type="text" id="proxy" name="proxy" placeholder="http://127.0.0.1:7890">
|
||||
</div>
|
||||
<div class="two-column-layout">
|
||||
<!-- 左侧面板 - 注册设置 -->
|
||||
<div class="left-panel">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📝 注册设置</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="reg-mode">注册模式</label>
|
||||
<select id="reg-mode" name="reg_mode">
|
||||
<option value="single">单次注册</option>
|
||||
<option value="batch">批量注册</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="batch-count-group" style="display: none;">
|
||||
<label for="batch-count">注册数量 (1-100)</label>
|
||||
<input type="number" id="batch-count" name="batch_count" min="1" max="100" value="5">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="batch-options" style="display: none;">
|
||||
<div class="form-row">
|
||||
<div class="card-body">
|
||||
<form id="registration-form">
|
||||
<div class="form-group">
|
||||
<label for="interval-min">最小间隔 (秒)</label>
|
||||
<input type="number" id="interval-min" name="interval_min" min="0" max="300" value="5">
|
||||
<label for="email-service">邮箱服务</label>
|
||||
<select id="email-service" name="email_service" required>
|
||||
<option value="tempmail">Tempmail.lol (临时邮箱)</option>
|
||||
<option value="outlook">Outlook</option>
|
||||
<option value="custom_domain">自定义域名</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="interval-max">最大间隔 (秒)</label>
|
||||
<input type="number" id="interval-max" name="interval_max" min="1" max="600" value="30">
|
||||
<label for="reg-mode">注册模式</label>
|
||||
<select id="reg-mode" name="reg_mode">
|
||||
<option value="single">单次注册</option>
|
||||
<option value="batch">批量注册</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="batch-count-group" style="display: none;">
|
||||
<label for="batch-count">注册数量 (1-100)</label>
|
||||
<input type="number" id="batch-count" name="batch_count" min="1" max="100" value="5">
|
||||
</div>
|
||||
|
||||
<div id="batch-options" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="interval-min">最小间隔 (秒)</label>
|
||||
<input type="number" id="interval-min" name="interval_min" min="0" max="300" value="5">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="interval-max">最大间隔 (秒)</label>
|
||||
<input type="number" id="interval-max" name="interval_max" min="1" max="600" value="30">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions" style="flex-direction: column;">
|
||||
<button type="submit" class="btn btn-primary btn-lg" id="start-btn" style="width: 100%;">
|
||||
🚀 开始注册
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancel-btn" disabled style="width: 100%;">
|
||||
取消任务
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板 -->
|
||||
<div class="right-panel">
|
||||
<!-- 监控台 -->
|
||||
<div class="card" id="console-card">
|
||||
<div class="card-header">
|
||||
<h3>💻 监控台</h3>
|
||||
<div style="display: flex; gap: var(--spacing-sm); align-items: center;">
|
||||
<span id="task-status-badge" class="status-badge pending" style="display: none;">等待中</span>
|
||||
<button class="btn btn-ghost btn-sm" id="clear-log-btn">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<!-- 任务状态行 -->
|
||||
<div class="task-status-row" id="task-status-row" style="display: none;">
|
||||
<div class="task-status-item">
|
||||
<div class="label">任务 ID</div>
|
||||
<div class="value" id="task-id">-</div>
|
||||
</div>
|
||||
<div class="task-status-item">
|
||||
<div class="label">邮箱</div>
|
||||
<div class="value" id="task-email">-</div>
|
||||
</div>
|
||||
<div class="task-status-item">
|
||||
<div class="label">邮箱服务</div>
|
||||
<div class="value" id="task-service">-</div>
|
||||
</div>
|
||||
<div class="task-status-item">
|
||||
<div class="label">状态</div>
|
||||
<div class="value" id="task-status">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary btn-lg" id="start-btn">
|
||||
🚀 开始注册
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancel-btn" disabled>
|
||||
取消任务
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 批量进度 -->
|
||||
<div id="batch-progress-section" style="display: none; padding: var(--spacing-md); border-bottom: 1px solid var(--border-light);">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: var(--spacing-sm);">
|
||||
<span id="batch-progress-text">0/0</span>
|
||||
<span id="batch-progress-percent">0%</span>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div id="progress-bar" class="progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="batch-stats" style="margin-top: var(--spacing-sm);">
|
||||
<span>✅ <strong id="batch-success">0</strong></span>
|
||||
<span>❌ <strong id="batch-failed">0</strong></span>
|
||||
<span>⏳ <strong id="batch-remaining">0</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量任务状态 -->
|
||||
<div class="card" id="batch-status-card" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h3>📊 批量任务进度</h3>
|
||||
<span id="batch-progress" class="status-badge">0/0</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="progress-bar-container">
|
||||
<div id="progress-bar" class="progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="batch-stats">
|
||||
<span>✅ 成功: <strong id="batch-success">0</strong></span>
|
||||
<span>❌ 失败: <strong id="batch-failed">0</strong></span>
|
||||
<span>⏳ 剩余: <strong id="batch-remaining">0</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务状态 -->
|
||||
<div class="card" id="task-status-card" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h3>⚡ 任务状态</h3>
|
||||
<span id="task-status-badge" class="status-badge pending">等待中</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="label">任务 ID</span>
|
||||
<span id="task-id" class="value">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">邮箱</span>
|
||||
<span id="task-email" class="value">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">状态</span>
|
||||
<span id="task-status" class="value">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">邮箱服务</span>
|
||||
<span id="task-service" class="value">-</span>
|
||||
<!-- 控制台日志 -->
|
||||
<div id="console-log" class="console-log">
|
||||
<div class="log-line info">[系统] 准备就绪,等待开始注册...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制台日志 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>💻 控制台日志</h3>
|
||||
<button class="btn btn-ghost btn-sm" id="clear-log-btn">清空</button>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<div id="console-log" class="console-log">
|
||||
<div class="log-line info">[系统] 准备就绪,等待开始注册...</div>
|
||||
<!-- 已注册账号列表 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📋 已注册账号</h3>
|
||||
<div style="display: flex; gap: var(--spacing-sm);">
|
||||
<button class="btn btn-ghost btn-sm" id="refresh-accounts-btn">🔄 刷新</button>
|
||||
<a href="/accounts" class="btn btn-secondary btn-sm">查看全部</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<div class="recent-accounts-table">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50px;">ID</th>
|
||||
<th>邮箱</th>
|
||||
<th style="width: 120px;">密码</th>
|
||||
<th style="width: 80px;">状态</th>
|
||||
<th style="width: 80px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recent-accounts-table">
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state" style="padding: var(--spacing-md);">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div class="empty-state-title">暂无已注册账号</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link">注册</a>
|
||||
<a href="/accounts" class="nav-link">账号管理</a>
|
||||
<a href="/email-services" class="nav-link">邮箱服务</a>
|
||||
<a href="/settings" class="nav-link active">设置</a>
|
||||
</div>
|
||||
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
|
||||
@@ -34,16 +35,17 @@
|
||||
<!-- 设置标签页 -->
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="proxy">🌐 代理设置</button>
|
||||
<button class="tab-btn" data-tab="email">📧 邮箱服务</button>
|
||||
<button class="tab-btn" data-tab="registration">⚙️ 注册配置</button>
|
||||
<button class="tab-btn" data-tab="database">💾 数据库</button>
|
||||
</div>
|
||||
|
||||
<!-- 代理设置 -->
|
||||
<div class="tab-content active" id="proxy-tab">
|
||||
<!-- 默认代理配置 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>代理配置</h3>
|
||||
<h3>默认代理配置</h3>
|
||||
<span class="hint">当代理列表为空时使用此配置</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="proxy-form">
|
||||
@@ -93,74 +95,37 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 邮箱服务 -->
|
||||
<div class="tab-content" id="email-tab">
|
||||
<!-- Outlook 批量导入 -->
|
||||
<div class="card" id="outlook-import-card">
|
||||
<!-- 代理列表 -->
|
||||
<div class="card" style="margin-top: var(--spacing-lg);">
|
||||
<div class="card-header">
|
||||
<h3>📥 Outlook 批量导入</h3>
|
||||
<button class="btn btn-ghost btn-sm" id="toggle-import-btn">展开</button>
|
||||
</div>
|
||||
<div class="card-body" id="outlook-import-body" style="display: none;">
|
||||
<div class="import-info">
|
||||
<p><strong>支持格式:</strong></p>
|
||||
<ul>
|
||||
<li><code>邮箱----密码</code> (密码认证)</li>
|
||||
<li><code>邮箱----密码----client_id----refresh_token</code> (XOAUTH2 认证,推荐)</li>
|
||||
</ul>
|
||||
<p>每行一个账户,使用四个连字符(----)分隔字段。以 # 开头的行将被忽略。</p>
|
||||
<h3>代理列表</h3>
|
||||
<div style="display: flex; gap: var(--spacing-sm);">
|
||||
<button class="btn btn-secondary btn-sm" id="test-all-proxies-btn">🔌 测试全部</button>
|
||||
<button class="btn btn-primary btn-sm" id="add-proxy-btn">➕ 添加代理</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="outlook-import-data">批量导入数据</label>
|
||||
<textarea id="outlook-import-data" rows="8" placeholder="example@outlook.com----password123 test@outlook.com----password456----client_id----refresh_token"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="outlook-import-enabled" checked>
|
||||
导入后启用
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="outlook-import-priority">优先级</label>
|
||||
<input type="number" id="outlook-import-priority" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-primary" id="outlook-import-btn">📥 开始导入</button>
|
||||
<button type="button" class="btn btn-secondary" id="clear-import-btn">清空</button>
|
||||
</div>
|
||||
<div id="import-result" style="display: none; margin-top: var(--spacing-md);"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>邮箱服务配置</h3>
|
||||
<button class="btn btn-primary btn-sm" id="add-email-service-btn">➕ 添加服务</button>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;"><input type="checkbox" id="select-all-services"></th>
|
||||
<th style="width: 50px;">ID</th>
|
||||
<th>名称</th>
|
||||
<th style="width: 120px;">类型</th>
|
||||
<th style="width: 100px;">状态</th>
|
||||
<th style="width: 80px;">优先级</th>
|
||||
<th style="width: 160px;">最后使用</th>
|
||||
<th style="width: 180px;">操作</th>
|
||||
<th>类型</th>
|
||||
<th>地址</th>
|
||||
<th style="width: 80px;">状态</th>
|
||||
<th style="width: 120px;">最后使用</th>
|
||||
<th style="width: 150px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="email-services-table">
|
||||
<tbody id="proxies-table">
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text" style="width: 80%;"></div>
|
||||
<div class="empty-state-icon">🌐</div>
|
||||
<div class="empty-state-title">暂无代理</div>
|
||||
<div class="empty-state-description">点击"添加代理"按钮添加代理服务器</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -171,6 +136,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加代理模态框 -->
|
||||
<div class="modal" id="add-proxy-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="proxy-modal-title">添加代理</h3>
|
||||
<button class="modal-close" id="close-proxy-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="proxy-item-form">
|
||||
<input type="hidden" id="proxy-item-id">
|
||||
<div class="form-group">
|
||||
<label for="proxy-item-name">名称</label>
|
||||
<input type="text" id="proxy-item-name" name="name" required placeholder="例如:美国代理 1">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="proxy-item-type">类型</label>
|
||||
<select id="proxy-item-type" name="type">
|
||||
<option value="http">HTTP</option>
|
||||
<option value="socks5">SOCKS5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="proxy-item-host">主机地址</label>
|
||||
<input type="text" id="proxy-item-host" name="host" required placeholder="127.0.0.1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="proxy-item-port">端口</label>
|
||||
<input type="number" id="proxy-item-port" name="port" required placeholder="7890">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="proxy-item-username">用户名 (可选)</label>
|
||||
<input type="text" id="proxy-item-username" name="username" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="proxy-item-password">密码 (可选)</label>
|
||||
<input type="password" id="proxy-item-password" name="password" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="cancel-proxy-btn">取消</button>
|
||||
<button type="submit" class="btn btn-primary">💾 保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 注册配置 -->
|
||||
<div class="tab-content" id="registration-tab">
|
||||
<div class="card">
|
||||
@@ -252,42 +267,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 添加邮箱服务模态框 -->
|
||||
<div class="modal" id="add-service-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>➕ 添加邮箱服务</h3>
|
||||
<button class="modal-close" id="close-service-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="add-service-form">
|
||||
<div class="form-group">
|
||||
<label for="service-type">服务类型</label>
|
||||
<select id="service-type" name="service_type" required>
|
||||
<option value="tempmail">Tempmail.lol</option>
|
||||
<option value="outlook">Outlook</option>
|
||||
<option value="custom_domain">自定义域名</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="service-name">服务名称</label>
|
||||
<input type="text" id="service-name" name="name" required placeholder="例如:我的 Outlook 账号">
|
||||
</div>
|
||||
|
||||
<div id="service-config-fields">
|
||||
<!-- 根据类型动态加载 -->
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">添加</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancel-add-service">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/utils.js"></script>
|
||||
<script src="/static/js/settings.js"></script>
|
||||
</body>
|
||||
|
||||
113
webui.py
Normal file
113
webui.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Web UI 启动入口
|
||||
"""
|
||||
|
||||
import uvicorn
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.core.utils import setup_logging
|
||||
from src.database.init_db import initialize_database
|
||||
from src.config.settings import get_settings
|
||||
|
||||
|
||||
def setup_application():
|
||||
"""设置应用程序"""
|
||||
# 获取配置
|
||||
settings = get_settings()
|
||||
|
||||
# 配置日志
|
||||
setup_logging(
|
||||
log_level=settings.log_level,
|
||||
log_file=settings.log_file
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 初始化数据库
|
||||
try:
|
||||
initialize_database()
|
||||
logger.info("数据库初始化完成")
|
||||
except Exception as e:
|
||||
logger.error(f"数据库初始化失败: {e}")
|
||||
raise
|
||||
|
||||
# 检查数据目录
|
||||
data_dir = project_root / "data"
|
||||
data_dir.mkdir(exist_ok=True)
|
||||
logger.info(f"数据目录: {data_dir}")
|
||||
|
||||
# 检查日志目录
|
||||
logs_dir = project_root / "logs"
|
||||
logs_dir.mkdir(exist_ok=True)
|
||||
logger.info(f"日志目录: {logs_dir}")
|
||||
|
||||
logger.info("应用程序设置完成")
|
||||
return settings
|
||||
|
||||
|
||||
def start_webui():
|
||||
"""启动 Web UI"""
|
||||
# 设置应用程序
|
||||
settings = setup_application()
|
||||
|
||||
# 导入 FastAPI 应用(延迟导入以避免循环依赖)
|
||||
from src.web.app import app
|
||||
|
||||
# 配置 uvicorn
|
||||
uvicorn_config = {
|
||||
"app": "src.web.app:app",
|
||||
"host": settings.webui_host,
|
||||
"port": settings.webui_port,
|
||||
"reload": settings.debug,
|
||||
"log_level": "info" if settings.debug else "warning",
|
||||
"access_log": settings.debug,
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"启动 Web UI 在 http://{settings.webui_host}:{settings.webui_port}")
|
||||
logger.info(f"调试模式: {settings.debug}")
|
||||
|
||||
# 启动服务器
|
||||
uvicorn.run(**uvicorn_config)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="OpenAI/Codex CLI 自动注册系统 Web UI")
|
||||
parser.add_argument("--host", help="监听主机")
|
||||
parser.add_argument("--port", type=int, help="监听端口")
|
||||
parser.add_argument("--debug", action="store_true", help="启用调试模式")
|
||||
parser.add_argument("--reload", action="store_true", help="启用热重载")
|
||||
parser.add_argument("--log-level", help="日志级别")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 更新配置
|
||||
from src.config.settings import update_settings
|
||||
|
||||
updates = {}
|
||||
if args.host:
|
||||
updates["webui_host"] = args.host
|
||||
if args.port:
|
||||
updates["webui_port"] = args.port
|
||||
if args.debug:
|
||||
updates["debug"] = args.debug
|
||||
if args.log_level:
|
||||
updates["log_level"] = args.log_level
|
||||
|
||||
if updates:
|
||||
update_settings(**updates)
|
||||
|
||||
# 启动 Web UI
|
||||
start_webui()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user