This commit is contained in:
cnlimiter
2026-03-14 20:36:03 +08:00
parent 0688f4ca7e
commit 6891b9f11d
22 changed files with 3882 additions and 299 deletions

288
README.md Normal file
View 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
View 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
View 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"]

View File

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

View File

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

View File

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

View File

@@ -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):
"""设置页面"""

View File

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

View File

@@ -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)}"}

View File

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

View File

@@ -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": "代理已禁用"}

View File

@@ -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');

View File

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

View File

@@ -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
View 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()">&times;</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;

View File

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

View 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&#10;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">&times;</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>

View File

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

View File

@@ -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&#10;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">&times;</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">&times;</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
View 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()