mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-06-30 03:31:33 +08:00
2
This commit is contained in:
7
src/web/__init__.py
Normal file
7
src/web/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Web UI 应用模块
|
||||
"""
|
||||
|
||||
from .app import app, create_app
|
||||
|
||||
__all__ = ['app', 'create_app']
|
||||
104
src/web/app.py
Normal file
104
src/web/app.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
FastAPI 应用主文件
|
||||
轻量级 Web UI,支持注册、账号管理、设置
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from ..config.settings import get_settings
|
||||
from .routes import api_router
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 获取项目根目录
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
|
||||
# 静态文件和模板目录
|
||||
STATIC_DIR = PROJECT_ROOT / "static"
|
||||
TEMPLATES_DIR = PROJECT_ROOT / "templates"
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""创建 FastAPI 应用实例"""
|
||||
settings = get_settings()
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.app_name,
|
||||
version=settings.app_version,
|
||||
description="OpenAI/Codex CLI 自动注册系统 Web UI",
|
||||
docs_url="/api/docs" if settings.debug else None,
|
||||
redoc_url="/api/redoc" if settings.debug else None,
|
||||
)
|
||||
|
||||
# CORS 中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 挂载静态文件
|
||||
if STATIC_DIR.exists():
|
||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||
logger.info(f"静态文件目录: {STATIC_DIR}")
|
||||
else:
|
||||
# 创建静态目录
|
||||
STATIC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||
logger.info(f"创建静态文件目录: {STATIC_DIR}")
|
||||
|
||||
# 创建模板目录
|
||||
if not TEMPLATES_DIR.exists():
|
||||
TEMPLATES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"创建模板目录: {TEMPLATES_DIR}")
|
||||
|
||||
# 注册 API 路由
|
||||
app.include_router(api_router, prefix="/api")
|
||||
|
||||
# 模板引擎
|
||||
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request):
|
||||
"""首页 - 注册页面"""
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
@app.get("/accounts", response_class=HTMLResponse)
|
||||
async def accounts_page(request: Request):
|
||||
"""账号管理页面"""
|
||||
return templates.TemplateResponse("accounts.html", {"request": request})
|
||||
|
||||
@app.get("/settings", response_class=HTMLResponse)
|
||||
async def settings_page(request: Request):
|
||||
"""设置页面"""
|
||||
return templates.TemplateResponse("settings.html", {"request": request})
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""应用启动事件"""
|
||||
logger.info("=" * 50)
|
||||
logger.info(f"{settings.app_name} v{settings.app_version} 启动中...")
|
||||
logger.info(f"调试模式: {settings.debug}")
|
||||
logger.info(f"数据库: {settings.database_url}")
|
||||
logger.info("=" * 50)
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
"""应用关闭事件"""
|
||||
logger.info("应用关闭")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# 创建全局应用实例
|
||||
app = create_app()
|
||||
18
src/web/routes/__init__.py
Normal file
18
src/web/routes/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
API 路由模块
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .accounts import router as accounts_router
|
||||
from .registration import router as registration_router
|
||||
from .settings import router as settings_router
|
||||
from .email_services import router as email_services_router
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# 注册各模块路由
|
||||
api_router.include_router(accounts_router, prefix="/accounts", tags=["accounts"])
|
||||
api_router.include_router(registration_router, prefix="/registration", tags=["registration"])
|
||||
api_router.include_router(settings_router, prefix="/settings", tags=["settings"])
|
||||
api_router.include_router(email_services_router, prefix="/email-services", tags=["email-services"])
|
||||
369
src/web/routes/accounts.py
Normal file
369
src/web/routes/accounts.py
Normal file
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
账号管理 API 路由
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, BackgroundTasks
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...database import crud
|
||||
from ...database.session import get_db
|
||||
from ...database.models import Account
|
||||
from ...config.constants import AccountStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============== Pydantic Models ==============
|
||||
|
||||
class AccountResponse(BaseModel):
|
||||
"""账号响应模型"""
|
||||
id: int
|
||||
email: str
|
||||
email_service: str
|
||||
account_id: Optional[str] = None
|
||||
workspace_id: Optional[str] = None
|
||||
registered_at: Optional[str] = None
|
||||
status: str
|
||||
proxy_used: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AccountListResponse(BaseModel):
|
||||
"""账号列表响应"""
|
||||
total: int
|
||||
accounts: List[AccountResponse]
|
||||
|
||||
|
||||
class AccountUpdateRequest(BaseModel):
|
||||
"""账号更新请求"""
|
||||
status: Optional[str] = None
|
||||
metadata: Optional[dict] = None
|
||||
|
||||
|
||||
class BatchDeleteRequest(BaseModel):
|
||||
"""批量删除请求"""
|
||||
ids: List[int]
|
||||
|
||||
|
||||
class BatchUpdateRequest(BaseModel):
|
||||
"""批量更新请求"""
|
||||
ids: List[int]
|
||||
status: str
|
||||
|
||||
|
||||
# ============== Helper Functions ==============
|
||||
|
||||
def account_to_response(account: Account) -> AccountResponse:
|
||||
"""转换 Account 模型为响应模型"""
|
||||
return AccountResponse(
|
||||
id=account.id,
|
||||
email=account.email,
|
||||
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,
|
||||
status=account.status,
|
||||
proxy_used=account.proxy_used,
|
||||
created_at=account.created_at.isoformat() if account.created_at else None,
|
||||
updated_at=account.updated_at.isoformat() if account.updated_at else None,
|
||||
)
|
||||
|
||||
|
||||
# ============== API Endpoints ==============
|
||||
|
||||
@router.get("", response_model=AccountListResponse)
|
||||
async def list_accounts(
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||||
status: Optional[str] = Query(None, description="状态筛选"),
|
||||
email_service: Optional[str] = Query(None, description="邮箱服务筛选"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词"),
|
||||
):
|
||||
"""
|
||||
获取账号列表
|
||||
|
||||
支持分页、状态筛选、邮箱服务筛选和搜索
|
||||
"""
|
||||
with get_db() as db:
|
||||
# 构建查询
|
||||
query = db.query(Account)
|
||||
|
||||
# 状态筛选
|
||||
if status:
|
||||
query = query.filter(Account.status == status)
|
||||
|
||||
# 邮箱服务筛选
|
||||
if email_service:
|
||||
query = query.filter(Account.email_service == email_service)
|
||||
|
||||
# 搜索
|
||||
if search:
|
||||
search_pattern = f"%{search}%"
|
||||
query = query.filter(
|
||||
(Account.email.ilike(search_pattern)) |
|
||||
(Account.account_id.ilike(search_pattern))
|
||||
)
|
||||
|
||||
# 统计总数
|
||||
total = query.count()
|
||||
|
||||
# 分页
|
||||
offset = (page - 1) * page_size
|
||||
accounts = query.order_by(Account.created_at.desc()).offset(offset).limit(page_size).all()
|
||||
|
||||
return AccountListResponse(
|
||||
total=total,
|
||||
accounts=[account_to_response(acc) for acc in accounts]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{account_id}", response_model=AccountResponse)
|
||||
async def get_account(account_id: int):
|
||||
"""获取单个账号详情"""
|
||||
with get_db() as db:
|
||||
account = crud.get_account_by_id(db, account_id)
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
return account_to_response(account)
|
||||
|
||||
|
||||
@router.get("/{account_id}/tokens")
|
||||
async def get_account_tokens(account_id: int):
|
||||
"""获取账号的 Token 信息"""
|
||||
with get_db() as db:
|
||||
account = crud.get_account_by_id(db, account_id)
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
|
||||
return {
|
||||
"id": account.id,
|
||||
"email": account.email,
|
||||
"access_token": account.access_token[:50] + "..." if account.access_token else None,
|
||||
"refresh_token": account.refresh_token[:50] + "..." if account.refresh_token else None,
|
||||
"id_token": account.id_token[:50] + "..." if account.id_token else None,
|
||||
"has_tokens": bool(account.access_token and account.refresh_token),
|
||||
}
|
||||
|
||||
|
||||
@router.patch("/{account_id}", response_model=AccountResponse)
|
||||
async def update_account(account_id: int, request: AccountUpdateRequest):
|
||||
"""更新账号状态"""
|
||||
with get_db() as db:
|
||||
account = crud.get_account_by_id(db, account_id)
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
|
||||
update_data = {}
|
||||
if request.status:
|
||||
if request.status not in [e.value for e in AccountStatus]:
|
||||
raise HTTPException(status_code=400, detail="无效的状态值")
|
||||
update_data["status"] = request.status
|
||||
|
||||
if request.metadata:
|
||||
current_metadata = account.metadata or {}
|
||||
current_metadata.update(request.metadata)
|
||||
update_data["metadata"] = current_metadata
|
||||
|
||||
account = crud.update_account(db, account_id, **update_data)
|
||||
return account_to_response(account)
|
||||
|
||||
|
||||
@router.delete("/{account_id}")
|
||||
async def delete_account(account_id: int):
|
||||
"""删除单个账号"""
|
||||
with get_db() as db:
|
||||
account = crud.get_account_by_id(db, account_id)
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
|
||||
crud.delete_account(db, account_id)
|
||||
return {"success": True, "message": f"账号 {account.email} 已删除"}
|
||||
|
||||
|
||||
@router.post("/batch-delete")
|
||||
async def batch_delete_accounts(request: BatchDeleteRequest):
|
||||
"""批量删除账号"""
|
||||
with get_db() as db:
|
||||
deleted_count = 0
|
||||
errors = []
|
||||
|
||||
for account_id in request.ids:
|
||||
try:
|
||||
account = crud.get_account_by_id(db, account_id)
|
||||
if account:
|
||||
crud.delete_account(db, account_id)
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
errors.append(f"ID {account_id}: {str(e)}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"deleted_count": deleted_count,
|
||||
"errors": errors if errors else None
|
||||
}
|
||||
|
||||
|
||||
@router.post("/batch-update")
|
||||
async def batch_update_accounts(request: BatchUpdateRequest):
|
||||
"""批量更新账号状态"""
|
||||
if request.status not in [e.value for e in AccountStatus]:
|
||||
raise HTTPException(status_code=400, detail="无效的状态值")
|
||||
|
||||
with get_db() as db:
|
||||
updated_count = 0
|
||||
errors = []
|
||||
|
||||
for account_id in request.ids:
|
||||
try:
|
||||
account = crud.get_account_by_id(db, account_id)
|
||||
if account:
|
||||
crud.update_account(db, account_id, status=request.status)
|
||||
updated_count += 1
|
||||
except Exception as e:
|
||||
errors.append(f"ID {account_id}: {str(e)}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"updated_count": updated_count,
|
||||
"errors": errors if errors else None
|
||||
}
|
||||
|
||||
|
||||
@router.get("/export/json")
|
||||
async def export_accounts_json(
|
||||
status: Optional[str] = Query(None, description="状态筛选"),
|
||||
email_service: Optional[str] = Query(None, description="邮箱服务筛选"),
|
||||
):
|
||||
"""导出账号为 JSON 格式"""
|
||||
with get_db() as db:
|
||||
query = db.query(Account)
|
||||
|
||||
if status:
|
||||
query = query.filter(Account.status == status)
|
||||
if email_service:
|
||||
query = query.filter(Account.email_service == email_service)
|
||||
|
||||
accounts = query.all()
|
||||
|
||||
export_data = []
|
||||
for acc in accounts:
|
||||
export_data.append({
|
||||
"email": acc.email,
|
||||
"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,
|
||||
"email_service": acc.email_service,
|
||||
"registered_at": acc.registered_at.isoformat() if acc.registered_at else None,
|
||||
"status": acc.status,
|
||||
})
|
||||
|
||||
# 生成文件名
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"accounts_{timestamp}.json"
|
||||
|
||||
# 返回 JSON 响应
|
||||
import io
|
||||
content = json.dumps(export_data, ensure_ascii=False, indent=2)
|
||||
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type="application/json",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/export/csv")
|
||||
async def export_accounts_csv(
|
||||
status: Optional[str] = Query(None, description="状态筛选"),
|
||||
email_service: Optional[str] = Query(None, description="邮箱服务筛选"),
|
||||
):
|
||||
"""导出账号为 CSV 格式"""
|
||||
import csv
|
||||
import io
|
||||
|
||||
with get_db() as db:
|
||||
query = db.query(Account)
|
||||
|
||||
if status:
|
||||
query = query.filter(Account.status == status)
|
||||
if email_service:
|
||||
query = query.filter(Account.email_service == email_service)
|
||||
|
||||
accounts = query.all()
|
||||
|
||||
# 创建 CSV 内容
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
# 写入表头
|
||||
writer.writerow([
|
||||
"ID", "Email", "Account ID", "Workspace ID",
|
||||
"Access Token", "Refresh Token", "ID Token",
|
||||
"Email Service", "Status", "Registered At"
|
||||
])
|
||||
|
||||
# 写入数据
|
||||
for acc in accounts:
|
||||
writer.writerow([
|
||||
acc.id,
|
||||
acc.email,
|
||||
acc.account_id or "",
|
||||
acc.workspace_id or "",
|
||||
acc.access_token or "",
|
||||
acc.refresh_token or "",
|
||||
acc.id_token or "",
|
||||
acc.email_service,
|
||||
acc.status,
|
||||
acc.registered_at.isoformat() if acc.registered_at else ""
|
||||
])
|
||||
|
||||
# 生成文件名
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"accounts_{timestamp}.csv"
|
||||
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats/summary")
|
||||
async def get_accounts_stats():
|
||||
"""获取账号统计信息"""
|
||||
with get_db() as db:
|
||||
from sqlalchemy import func
|
||||
|
||||
# 总数
|
||||
total = db.query(func.count(Account.id)).scalar()
|
||||
|
||||
# 按状态统计
|
||||
status_stats = db.query(
|
||||
Account.status,
|
||||
func.count(Account.id)
|
||||
).group_by(Account.status).all()
|
||||
|
||||
# 按邮箱服务统计
|
||||
service_stats = db.query(
|
||||
Account.email_service,
|
||||
func.count(Account.id)
|
||||
).group_by(Account.email_service).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"by_status": {status: count for status, count in status_stats},
|
||||
"by_email_service": {service: count for service, count in service_stats}
|
||||
}
|
||||
437
src/web/routes/email_services.py
Normal file
437
src/web/routes/email_services.py
Normal file
@@ -0,0 +1,437 @@
|
||||
"""
|
||||
邮箱服务配置 API 路由
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...database import crud
|
||||
from ...database.session import get_db
|
||||
from ...database.models import EmailService as EmailServiceModel
|
||||
from ...services import EmailServiceFactory, EmailServiceType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============== Pydantic Models ==============
|
||||
|
||||
class EmailServiceCreate(BaseModel):
|
||||
"""创建邮箱服务请求"""
|
||||
service_type: str
|
||||
name: str
|
||||
config: Dict[str, Any]
|
||||
enabled: bool = True
|
||||
priority: int = 0
|
||||
|
||||
|
||||
class EmailServiceUpdate(BaseModel):
|
||||
"""更新邮箱服务请求"""
|
||||
name: Optional[str] = None
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
enabled: Optional[bool] = None
|
||||
priority: Optional[int] = None
|
||||
|
||||
|
||||
class EmailServiceResponse(BaseModel):
|
||||
"""邮箱服务响应"""
|
||||
id: int
|
||||
service_type: str
|
||||
name: str
|
||||
enabled: bool
|
||||
priority: int
|
||||
last_used: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class EmailServiceListResponse(BaseModel):
|
||||
"""邮箱服务列表响应"""
|
||||
total: int
|
||||
services: List[EmailServiceResponse]
|
||||
|
||||
|
||||
class ServiceTestResult(BaseModel):
|
||||
"""服务测试结果"""
|
||||
success: bool
|
||||
message: str
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class OutlookBatchImportRequest(BaseModel):
|
||||
"""Outlook 批量导入请求"""
|
||||
data: str # 多行数据,每行格式: 邮箱----密码 或 邮箱----密码----client_id----refresh_token
|
||||
enabled: bool = True
|
||||
priority: int = 0
|
||||
|
||||
|
||||
class OutlookBatchImportResponse(BaseModel):
|
||||
"""Outlook 批量导入响应"""
|
||||
total: int
|
||||
success: int
|
||||
failed: int
|
||||
accounts: List[Dict[str, Any]]
|
||||
errors: List[str]
|
||||
|
||||
|
||||
# ============== Helper Functions ==============
|
||||
|
||||
def service_to_response(service: EmailServiceModel) -> EmailServiceResponse:
|
||||
"""转换服务模型为响应"""
|
||||
return EmailServiceResponse(
|
||||
id=service.id,
|
||||
service_type=service.service_type,
|
||||
name=service.name,
|
||||
enabled=service.enabled,
|
||||
priority=service.priority,
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
# ============== API Endpoints ==============
|
||||
|
||||
@router.get("/types")
|
||||
async def get_service_types():
|
||||
"""获取支持的邮箱服务类型"""
|
||||
return {
|
||||
"types": [
|
||||
{
|
||||
"value": "tempmail",
|
||||
"label": "Tempmail.lol",
|
||||
"description": "临时邮箱服务,无需配置",
|
||||
"config_fields": [
|
||||
{"name": "base_url", "label": "API 地址", "default": "https://api.tempmail.lol/v2", "required": False},
|
||||
{"name": "timeout", "label": "超时时间", "default": 30, "required": False},
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "outlook",
|
||||
"label": "Outlook",
|
||||
"description": "Outlook 邮箱,需要配置账户信息",
|
||||
"config_fields": [
|
||||
{"name": "email", "label": "邮箱地址", "required": True},
|
||||
{"name": "password", "label": "密码", "required": True},
|
||||
{"name": "client_id", "label": "OAuth Client ID", "required": False},
|
||||
{"name": "refresh_token", "label": "OAuth Refresh Token", "required": False},
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "custom_domain",
|
||||
"label": "自定义域名",
|
||||
"description": "自定义域名邮箱服务",
|
||||
"config_fields": [
|
||||
{"name": "base_url", "label": "API 地址", "required": True},
|
||||
{"name": "api_key", "label": "API Key", "required": True},
|
||||
{"name": "default_domain", "label": "默认域名", "required": False},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("", response_model=EmailServiceListResponse)
|
||||
async def list_email_services(
|
||||
service_type: Optional[str] = Query(None, description="服务类型筛选"),
|
||||
enabled_only: bool = Query(False, description="只显示启用的服务"),
|
||||
):
|
||||
"""获取邮箱服务列表"""
|
||||
with get_db() as db:
|
||||
query = db.query(EmailServiceModel)
|
||||
|
||||
if service_type:
|
||||
query = query.filter(EmailServiceModel.service_type == service_type)
|
||||
|
||||
if enabled_only:
|
||||
query = query.filter(EmailServiceModel.enabled == True)
|
||||
|
||||
services = query.order_by(EmailServiceModel.priority.asc(), EmailServiceModel.id.asc()).all()
|
||||
|
||||
return EmailServiceListResponse(
|
||||
total=len(services),
|
||||
services=[service_to_response(s) for s in services]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{service_id}", response_model=EmailServiceResponse)
|
||||
async def get_email_service(service_id: int):
|
||||
"""获取单个邮箱服务详情"""
|
||||
with get_db() as db:
|
||||
service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="服务不存在")
|
||||
return service_to_response(service)
|
||||
|
||||
|
||||
@router.post("", response_model=EmailServiceResponse)
|
||||
async def create_email_service(request: EmailServiceCreate):
|
||||
"""创建邮箱服务配置"""
|
||||
# 验证服务类型
|
||||
try:
|
||||
EmailServiceType(request.service_type)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail=f"无效的服务类型: {request.service_type}")
|
||||
|
||||
with get_db() as db:
|
||||
# 检查名称是否重复
|
||||
existing = db.query(EmailServiceModel).filter(EmailServiceModel.name == request.name).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="服务名称已存在")
|
||||
|
||||
service = EmailServiceModel(
|
||||
service_type=request.service_type,
|
||||
name=request.name,
|
||||
config=request.config,
|
||||
enabled=request.enabled,
|
||||
priority=request.priority
|
||||
)
|
||||
db.add(service)
|
||||
db.commit()
|
||||
db.refresh(service)
|
||||
|
||||
return service_to_response(service)
|
||||
|
||||
|
||||
@router.patch("/{service_id}", response_model=EmailServiceResponse)
|
||||
async def update_email_service(service_id: int, request: EmailServiceUpdate):
|
||||
"""更新邮箱服务配置"""
|
||||
with get_db() as db:
|
||||
service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="服务不存在")
|
||||
|
||||
update_data = {}
|
||||
if request.name is not None:
|
||||
update_data["name"] = request.name
|
||||
if request.config is not None:
|
||||
update_data["config"] = request.config
|
||||
if request.enabled is not None:
|
||||
update_data["enabled"] = request.enabled
|
||||
if request.priority is not None:
|
||||
update_data["priority"] = request.priority
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(service, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(service)
|
||||
|
||||
return service_to_response(service)
|
||||
|
||||
|
||||
@router.delete("/{service_id}")
|
||||
async def delete_email_service(service_id: int):
|
||||
"""删除邮箱服务配置"""
|
||||
with get_db() as db:
|
||||
service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="服务不存在")
|
||||
|
||||
db.delete(service)
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "message": f"服务 {service.name} 已删除"}
|
||||
|
||||
|
||||
@router.post("/{service_id}/test", response_model=ServiceTestResult)
|
||||
async def test_email_service(service_id: int):
|
||||
"""测试邮箱服务是否可用"""
|
||||
with get_db() as db:
|
||||
service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="服务不存在")
|
||||
|
||||
try:
|
||||
service_type = EmailServiceType(service.service_type)
|
||||
email_service = EmailServiceFactory.create(service_type, service.config, name=service.name)
|
||||
|
||||
health = email_service.check_health()
|
||||
|
||||
if health:
|
||||
return ServiceTestResult(
|
||||
success=True,
|
||||
message="服务连接正常",
|
||||
details=email_service.get_service_info() if hasattr(email_service, 'get_service_info') else None
|
||||
)
|
||||
else:
|
||||
return ServiceTestResult(
|
||||
success=False,
|
||||
message="服务连接失败"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"测试邮箱服务失败: {e}")
|
||||
return ServiceTestResult(
|
||||
success=False,
|
||||
message=f"测试失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{service_id}/enable")
|
||||
async def enable_email_service(service_id: int):
|
||||
"""启用邮箱服务"""
|
||||
with get_db() as db:
|
||||
service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="服务不存在")
|
||||
|
||||
service.enabled = True
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "message": f"服务 {service.name} 已启用"}
|
||||
|
||||
|
||||
@router.post("/{service_id}/disable")
|
||||
async def disable_email_service(service_id: int):
|
||||
"""禁用邮箱服务"""
|
||||
with get_db() as db:
|
||||
service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="服务不存在")
|
||||
|
||||
service.enabled = False
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "message": f"服务 {service.name} 已禁用"}
|
||||
|
||||
|
||||
@router.post("/reorder")
|
||||
async def reorder_services(service_ids: List[int]):
|
||||
"""重新排序邮箱服务优先级"""
|
||||
with get_db() as db:
|
||||
for index, service_id in enumerate(service_ids):
|
||||
service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first()
|
||||
if service:
|
||||
service.priority = index
|
||||
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "message": "优先级已更新"}
|
||||
|
||||
|
||||
@router.post("/outlook/batch-import", response_model=OutlookBatchImportResponse)
|
||||
async def batch_import_outlook(request: OutlookBatchImportRequest):
|
||||
"""
|
||||
批量导入 Outlook 邮箱账户
|
||||
|
||||
支持两种格式:
|
||||
- 格式一(密码认证):邮箱----密码
|
||||
- 格式二(XOAUTH2 认证):邮箱----密码----client_id----refresh_token
|
||||
|
||||
每行一个账户,使用四个连字符(----)分隔字段
|
||||
"""
|
||||
lines = request.data.strip().split("\n")
|
||||
total = len(lines)
|
||||
success = 0
|
||||
failed = 0
|
||||
accounts = []
|
||||
errors = []
|
||||
|
||||
with get_db() as db:
|
||||
for i, line in enumerate(lines):
|
||||
line = line.strip()
|
||||
|
||||
# 跳过空行和注释
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
parts = line.split("----")
|
||||
|
||||
# 验证格式
|
||||
if len(parts) < 2:
|
||||
failed += 1
|
||||
errors.append(f"行 {i+1}: 格式错误,至少需要邮箱和密码")
|
||||
continue
|
||||
|
||||
email = parts[0].strip()
|
||||
password = parts[1].strip()
|
||||
|
||||
# 验证邮箱格式
|
||||
if "@" not in email:
|
||||
failed += 1
|
||||
errors.append(f"行 {i+1}: 无效的邮箱地址: {email}")
|
||||
continue
|
||||
|
||||
# 检查是否已存在
|
||||
existing = db.query(EmailServiceModel).filter(
|
||||
EmailServiceModel.service_type == "outlook",
|
||||
EmailServiceModel.name == email
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
failed += 1
|
||||
errors.append(f"行 {i+1}: 邮箱已存在: {email}")
|
||||
continue
|
||||
|
||||
# 构建配置
|
||||
config = {
|
||||
"email": email,
|
||||
"password": password
|
||||
}
|
||||
|
||||
# 检查是否有 OAuth 信息(格式二)
|
||||
if len(parts) >= 4:
|
||||
client_id = parts[2].strip()
|
||||
refresh_token = parts[3].strip()
|
||||
if client_id and refresh_token:
|
||||
config["client_id"] = client_id
|
||||
config["refresh_token"] = refresh_token
|
||||
|
||||
# 创建服务记录
|
||||
try:
|
||||
service = EmailServiceModel(
|
||||
service_type="outlook",
|
||||
name=email,
|
||||
config=config,
|
||||
enabled=request.enabled,
|
||||
priority=request.priority
|
||||
)
|
||||
db.add(service)
|
||||
db.commit()
|
||||
db.refresh(service)
|
||||
|
||||
accounts.append({
|
||||
"id": service.id,
|
||||
"email": email,
|
||||
"has_oauth": bool(config.get("client_id")),
|
||||
"name": email
|
||||
})
|
||||
success += 1
|
||||
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
errors.append(f"行 {i+1}: 创建失败: {str(e)}")
|
||||
db.rollback()
|
||||
|
||||
return OutlookBatchImportResponse(
|
||||
total=total,
|
||||
success=success,
|
||||
failed=failed,
|
||||
accounts=accounts,
|
||||
errors=errors
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/outlook/batch")
|
||||
async def batch_delete_outlook(service_ids: List[int]):
|
||||
"""批量删除 Outlook 邮箱服务"""
|
||||
deleted = 0
|
||||
with get_db() as db:
|
||||
for service_id in service_ids:
|
||||
service = db.query(EmailServiceModel).filter(
|
||||
EmailServiceModel.id == service_id,
|
||||
EmailServiceModel.service_type == "outlook"
|
||||
).first()
|
||||
if service:
|
||||
db.delete(service)
|
||||
deleted += 1
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "deleted": deleted, "message": f"已删除 {deleted} 个服务"}
|
||||
500
src/web/routes/registration.py
Normal file
500
src/web/routes/registration.py
Normal file
@@ -0,0 +1,500 @@
|
||||
"""
|
||||
注册任务 API 路由
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import uuid
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict
|
||||
|
||||
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 ...core.register import RegistrationEngine, RegistrationResult
|
||||
from ...services import EmailServiceFactory, EmailServiceType
|
||||
from ...config.settings import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
# 任务存储(简单的内存存储,生产环境应使用 Redis)
|
||||
running_tasks: dict = {}
|
||||
# 批量任务存储
|
||||
batch_tasks: Dict[str, dict] = {}
|
||||
|
||||
|
||||
# ============== Pydantic Models ==============
|
||||
|
||||
class RegistrationTaskCreate(BaseModel):
|
||||
"""创建注册任务请求"""
|
||||
email_service_type: str = "tempmail"
|
||||
proxy: Optional[str] = None
|
||||
email_service_config: Optional[dict] = None
|
||||
|
||||
|
||||
class BatchRegistrationRequest(BaseModel):
|
||||
"""批量注册请求"""
|
||||
count: int = 1 # 注册数量
|
||||
email_service_type: str = "tempmail"
|
||||
proxy: Optional[str] = None
|
||||
email_service_config: Optional[dict] = None
|
||||
interval_min: int = 5 # 最小间隔秒数
|
||||
interval_max: int = 30 # 最大间隔秒数
|
||||
|
||||
|
||||
class BatchRegistrationResponse(BaseModel):
|
||||
"""批量注册响应"""
|
||||
batch_id: str
|
||||
count: int
|
||||
tasks: List[RegistrationTaskResponse]
|
||||
|
||||
|
||||
class RegistrationTaskResponse(BaseModel):
|
||||
"""注册任务响应"""
|
||||
id: int
|
||||
task_uuid: str
|
||||
status: str
|
||||
email_service_id: Optional[int] = None
|
||||
proxy: Optional[str] = None
|
||||
logs: Optional[str] = None
|
||||
result: Optional[dict] = None
|
||||
error_message: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
started_at: Optional[str] = None
|
||||
completed_at: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TaskListResponse(BaseModel):
|
||||
"""任务列表响应"""
|
||||
total: int
|
||||
tasks: List[RegistrationTaskResponse]
|
||||
|
||||
|
||||
# ============== Helper Functions ==============
|
||||
|
||||
def task_to_response(task: RegistrationTask) -> RegistrationTaskResponse:
|
||||
"""转换任务模型为响应"""
|
||||
return RegistrationTaskResponse(
|
||||
id=task.id,
|
||||
task_uuid=task.task_uuid,
|
||||
status=task.status,
|
||||
email_service_id=task.email_service_id,
|
||||
proxy=task.proxy,
|
||||
logs=task.logs,
|
||||
result=task.result,
|
||||
error_message=task.error_message,
|
||||
created_at=task.created_at.isoformat() if task.created_at else None,
|
||||
started_at=task.started_at.isoformat() if task.started_at else None,
|
||||
completed_at=task.completed_at.isoformat() if task.completed_at else None,
|
||||
)
|
||||
|
||||
|
||||
async def run_registration_task(task_uuid: str, email_service_type: str, proxy: Optional[str], email_service_config: Optional[dict]):
|
||||
"""异步执行注册任务"""
|
||||
with get_db() as db:
|
||||
try:
|
||||
# 更新任务状态为运行中
|
||||
task = crud.update_registration_task(
|
||||
db, task_uuid,
|
||||
status="running",
|
||||
started_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
if not task:
|
||||
logger.error(f"任务不存在: {task_uuid}")
|
||||
return
|
||||
|
||||
# 创建邮箱服务
|
||||
service_type = EmailServiceType(email_service_type)
|
||||
settings = get_settings()
|
||||
|
||||
if service_type == EmailServiceType.TEMPMAIL:
|
||||
config = {
|
||||
"base_url": settings.tempmail_base_url,
|
||||
"timeout": settings.tempmail_timeout,
|
||||
"max_retries": settings.tempmail_max_retries,
|
||||
"proxy_url": proxy,
|
||||
}
|
||||
elif service_type == EmailServiceType.CUSTOM_DOMAIN:
|
||||
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,
|
||||
}
|
||||
else:
|
||||
config = email_service_config or {}
|
||||
|
||||
email_service = EmailServiceFactory.create(service_type, config)
|
||||
|
||||
# 创建注册引擎
|
||||
def log_callback(msg):
|
||||
with get_db() as db_inner:
|
||||
crud.append_task_log(db_inner, task_uuid, msg)
|
||||
|
||||
engine = RegistrationEngine(
|
||||
email_service=email_service,
|
||||
proxy_url=proxy,
|
||||
callback_logger=log_callback,
|
||||
task_uuid=task_uuid
|
||||
)
|
||||
|
||||
# 执行注册
|
||||
result = engine.run()
|
||||
|
||||
if result.success:
|
||||
# 保存到数据库
|
||||
engine.save_to_database(result)
|
||||
|
||||
# 更新任务状态
|
||||
crud.update_registration_task(
|
||||
db, task_uuid,
|
||||
status="completed",
|
||||
completed_at=datetime.utcnow(),
|
||||
result=result.to_dict()
|
||||
)
|
||||
|
||||
logger.info(f"注册任务完成: {task_uuid}, 邮箱: {result.email}")
|
||||
else:
|
||||
# 更新任务状态为失败
|
||||
crud.update_registration_task(
|
||||
db, task_uuid,
|
||||
status="failed",
|
||||
completed_at=datetime.utcnow(),
|
||||
error_message=result.error_message
|
||||
)
|
||||
|
||||
logger.warning(f"注册任务失败: {task_uuid}, 原因: {result.error_message}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"注册任务异常: {task_uuid}, 错误: {e}")
|
||||
|
||||
try:
|
||||
with get_db() as db:
|
||||
crud.update_registration_task(
|
||||
db, task_uuid,
|
||||
status="failed",
|
||||
completed_at=datetime.utcnow(),
|
||||
error_message=str(e)
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
async def run_batch_registration(
|
||||
batch_id: str,
|
||||
task_uuids: List[str],
|
||||
email_service_type: str,
|
||||
proxy: Optional[str],
|
||||
email_service_config: Optional[dict],
|
||||
interval_min: int,
|
||||
interval_max: int
|
||||
):
|
||||
"""异步执行批量注册任务"""
|
||||
batch_tasks[batch_id] = {
|
||||
"total": len(task_uuids),
|
||||
"completed": 0,
|
||||
"success": 0,
|
||||
"failed": 0,
|
||||
"cancelled": False,
|
||||
"task_uuids": task_uuids,
|
||||
"current_index": 0
|
||||
}
|
||||
|
||||
try:
|
||||
for i, task_uuid in enumerate(task_uuids):
|
||||
# 检查是否已取消
|
||||
if batch_tasks[batch_id]["cancelled"]:
|
||||
# 取消剩余任务
|
||||
with get_db() as db:
|
||||
for remaining_uuid in task_uuids[i:]:
|
||||
crud.update_registration_task(db, remaining_uuid, status="cancelled")
|
||||
logger.info(f"批量任务 {batch_id} 已取消")
|
||||
break
|
||||
|
||||
batch_tasks[batch_id]["current_index"] = i
|
||||
|
||||
# 运行单个注册任务
|
||||
await run_registration_task(
|
||||
task_uuid, email_service_type, proxy, email_service_config
|
||||
)
|
||||
|
||||
# 更新统计
|
||||
with get_db() as db:
|
||||
task = crud.get_registration_task(db, task_uuid)
|
||||
if task:
|
||||
batch_tasks[batch_id]["completed"] += 1
|
||||
if task.status == "completed":
|
||||
batch_tasks[batch_id]["success"] += 1
|
||||
elif task.status == "failed":
|
||||
batch_tasks[batch_id]["failed"] += 1
|
||||
|
||||
# 如果不是最后一个任务,等待随机间隔
|
||||
if i < len(task_uuids) - 1 and not batch_tasks[batch_id]["cancelled"]:
|
||||
wait_time = random.randint(interval_min, interval_max)
|
||||
logger.info(f"批量任务 {batch_id}: 等待 {wait_time} 秒后继续下一个任务")
|
||||
await asyncio.sleep(wait_time)
|
||||
|
||||
logger.info(f"批量任务 {batch_id} 完成: 成功 {batch_tasks[batch_id]['success']}, 失败 {batch_tasks[batch_id]['failed']}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"批量任务 {batch_id} 异常: {e}")
|
||||
finally:
|
||||
batch_tasks[batch_id]["finished"] = True
|
||||
|
||||
|
||||
# ============== API Endpoints ==============
|
||||
|
||||
@router.post("/start", response_model=RegistrationTaskResponse)
|
||||
async def start_registration(
|
||||
request: RegistrationTaskCreate,
|
||||
background_tasks: BackgroundTasks
|
||||
):
|
||||
"""
|
||||
启动注册任务
|
||||
|
||||
- email_service_type: 邮箱服务类型 (tempmail, outlook, custom_domain)
|
||||
- proxy: 代理地址
|
||||
- email_service_config: 邮箱服务配置(outlook 需要提供账户信息)
|
||||
"""
|
||||
# 验证邮箱服务类型
|
||||
try:
|
||||
EmailServiceType(request.email_service_type)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"无效的邮箱服务类型: {request.email_service_type}"
|
||||
)
|
||||
|
||||
# 创建任务
|
||||
task_uuid = str(uuid.uuid4())
|
||||
|
||||
with get_db() as db:
|
||||
task = crud.create_registration_task(
|
||||
db,
|
||||
task_uuid=task_uuid,
|
||||
proxy=request.proxy
|
||||
)
|
||||
|
||||
# 在后台运行注册任务
|
||||
background_tasks.add_task(
|
||||
run_registration_task,
|
||||
task_uuid,
|
||||
request.email_service_type,
|
||||
request.proxy,
|
||||
request.email_service_config
|
||||
)
|
||||
|
||||
return task_to_response(task)
|
||||
|
||||
|
||||
@router.post("/batch", response_model=BatchRegistrationResponse)
|
||||
async def start_batch_registration(
|
||||
request: BatchRegistrationRequest,
|
||||
background_tasks: BackgroundTasks
|
||||
):
|
||||
"""
|
||||
启动批量注册任务
|
||||
|
||||
- count: 注册数量 (1-100)
|
||||
- email_service_type: 邮箱服务类型
|
||||
- proxy: 代理地址
|
||||
- interval_min: 最小间隔秒数
|
||||
- interval_max: 最大间隔秒数
|
||||
"""
|
||||
# 验证参数
|
||||
if request.count < 1 or request.count > 100:
|
||||
raise HTTPException(status_code=400, detail="注册数量必须在 1-100 之间")
|
||||
|
||||
try:
|
||||
EmailServiceType(request.email_service_type)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"无效的邮箱服务类型: {request.email_service_type}"
|
||||
)
|
||||
|
||||
if request.interval_min < 0 or request.interval_max < request.interval_min:
|
||||
raise HTTPException(status_code=400, detail="间隔时间参数无效")
|
||||
|
||||
# 创建批量任务
|
||||
batch_id = str(uuid.uuid4())
|
||||
task_uuids = []
|
||||
|
||||
with get_db() as db:
|
||||
for _ in range(request.count):
|
||||
task_uuid = str(uuid.uuid4())
|
||||
task = crud.create_registration_task(
|
||||
db,
|
||||
task_uuid=task_uuid,
|
||||
proxy=request.proxy
|
||||
)
|
||||
task_uuids.append(task_uuid)
|
||||
|
||||
# 获取所有任务
|
||||
with get_db() as db:
|
||||
tasks = [crud.get_registration_task(db, uuid) for uuid in task_uuids]
|
||||
|
||||
# 在后台运行批量注册
|
||||
background_tasks.add_task(
|
||||
run_batch_registration,
|
||||
batch_id,
|
||||
task_uuids,
|
||||
request.email_service_type,
|
||||
request.proxy,
|
||||
request.email_service_config,
|
||||
request.interval_min,
|
||||
request.interval_max
|
||||
)
|
||||
|
||||
return BatchRegistrationResponse(
|
||||
batch_id=batch_id,
|
||||
count=request.count,
|
||||
tasks=[task_to_response(t) for t in tasks if t]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/batch/{batch_id}")
|
||||
async def get_batch_status(batch_id: str):
|
||||
"""获取批量任务状态"""
|
||||
if batch_id not in batch_tasks:
|
||||
raise HTTPException(status_code=404, detail="批量任务不存在")
|
||||
|
||||
batch = batch_tasks[batch_id]
|
||||
return {
|
||||
"batch_id": batch_id,
|
||||
"total": batch["total"],
|
||||
"completed": batch["completed"],
|
||||
"success": batch["success"],
|
||||
"failed": batch["failed"],
|
||||
"current_index": batch["current_index"],
|
||||
"cancelled": batch["cancelled"],
|
||||
"finished": batch.get("finished", False),
|
||||
"progress": f"{batch['completed']}/{batch['total']}"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/batch/{batch_id}/cancel")
|
||||
async def cancel_batch(batch_id: str):
|
||||
"""取消批量任务"""
|
||||
if batch_id not in batch_tasks:
|
||||
raise HTTPException(status_code=404, detail="批量任务不存在")
|
||||
|
||||
batch = batch_tasks[batch_id]
|
||||
if batch.get("finished"):
|
||||
raise HTTPException(status_code=400, detail="批量任务已完成")
|
||||
|
||||
batch["cancelled"] = True
|
||||
return {"success": True, "message": "批量任务取消请求已提交"}
|
||||
|
||||
|
||||
@router.get("/tasks", response_model=TaskListResponse)
|
||||
async def list_tasks(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
status: Optional[str] = Query(None),
|
||||
):
|
||||
"""获取任务列表"""
|
||||
with get_db() as db:
|
||||
query = db.query(RegistrationTask)
|
||||
|
||||
if status:
|
||||
query = query.filter(RegistrationTask.status == status)
|
||||
|
||||
total = query.count()
|
||||
offset = (page - 1) * page_size
|
||||
tasks = query.order_by(RegistrationTask.created_at.desc()).offset(offset).limit(page_size).all()
|
||||
|
||||
return TaskListResponse(
|
||||
total=total,
|
||||
tasks=[task_to_response(t) for t in tasks]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tasks/{task_uuid}", response_model=RegistrationTaskResponse)
|
||||
async def get_task(task_uuid: str):
|
||||
"""获取任务详情"""
|
||||
with get_db() as db:
|
||||
task = crud.get_registration_task(db, task_uuid)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
return task_to_response(task)
|
||||
|
||||
|
||||
@router.get("/tasks/{task_uuid}/logs")
|
||||
async def get_task_logs(task_uuid: str):
|
||||
"""获取任务日志"""
|
||||
with get_db() as db:
|
||||
task = crud.get_registration_task(db, task_uuid)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
logs = task.logs or ""
|
||||
return {
|
||||
"task_uuid": task_uuid,
|
||||
"status": task.status,
|
||||
"logs": logs.split("\n") if logs else []
|
||||
}
|
||||
|
||||
|
||||
@router.post("/tasks/{task_uuid}/cancel")
|
||||
async def cancel_task(task_uuid: str):
|
||||
"""取消任务"""
|
||||
with get_db() as db:
|
||||
task = crud.get_registration_task(db, task_uuid)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
if task.status not in ["pending", "running"]:
|
||||
raise HTTPException(status_code=400, detail="任务已完成或已取消")
|
||||
|
||||
task = crud.update_registration_task(db, task_uuid, status="cancelled")
|
||||
|
||||
return {"success": True, "message": "任务已取消"}
|
||||
|
||||
|
||||
@router.delete("/tasks/{task_uuid}")
|
||||
async def delete_task(task_uuid: str):
|
||||
"""删除任务"""
|
||||
with get_db() as db:
|
||||
task = crud.get_registration_task(db, task_uuid)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
if task.status == "running":
|
||||
raise HTTPException(status_code=400, detail="无法删除运行中的任务")
|
||||
|
||||
crud.delete_registration_task(db, task_uuid)
|
||||
|
||||
return {"success": True, "message": "任务已删除"}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_registration_stats():
|
||||
"""获取注册统计信息"""
|
||||
with get_db() as db:
|
||||
from sqlalchemy import func
|
||||
|
||||
# 按状态统计
|
||||
status_stats = db.query(
|
||||
RegistrationTask.status,
|
||||
func.count(RegistrationTask.id)
|
||||
).group_by(RegistrationTask.status).all()
|
||||
|
||||
# 今日注册数
|
||||
today = datetime.utcnow().date()
|
||||
today_count = db.query(func.count(RegistrationTask.id)).filter(
|
||||
func.date(RegistrationTask.created_at) == today
|
||||
).scalar()
|
||||
|
||||
return {
|
||||
"by_status": {status: count for status, count in status_stats},
|
||||
"today_count": today_count
|
||||
}
|
||||
294
src/web/routes/settings.py
Normal file
294
src/web/routes/settings.py
Normal file
@@ -0,0 +1,294 @@
|
||||
"""
|
||||
设置 API 路由
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...database import crud
|
||||
from ...database.session import get_db
|
||||
from ...config.settings import get_settings, update_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============== Pydantic Models ==============
|
||||
|
||||
class SettingItem(BaseModel):
|
||||
"""设置项"""
|
||||
key: str
|
||||
value: str
|
||||
description: Optional[str] = None
|
||||
category: str = "general"
|
||||
|
||||
|
||||
class SettingUpdateRequest(BaseModel):
|
||||
"""设置更新请求"""
|
||||
value: str
|
||||
|
||||
|
||||
class ProxySettings(BaseModel):
|
||||
"""代理设置"""
|
||||
enabled: bool = False
|
||||
type: str = "http" # http, socks5
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 7890
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
|
||||
|
||||
class RegistrationSettings(BaseModel):
|
||||
"""注册设置"""
|
||||
max_retries: int = 3
|
||||
timeout: int = 120
|
||||
default_password_length: int = 12
|
||||
sleep_min: int = 5
|
||||
sleep_max: int = 30
|
||||
|
||||
|
||||
class WebUISettings(BaseModel):
|
||||
"""Web UI 设置"""
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
debug: bool = False
|
||||
|
||||
|
||||
class AllSettings(BaseModel):
|
||||
"""所有设置"""
|
||||
proxy: ProxySettings
|
||||
registration: RegistrationSettings
|
||||
webui: WebUISettings
|
||||
|
||||
|
||||
# ============== API Endpoints ==============
|
||||
|
||||
@router.get("")
|
||||
async def get_all_settings():
|
||||
"""获取所有设置"""
|
||||
settings = get_settings()
|
||||
|
||||
return {
|
||||
"proxy": {
|
||||
"enabled": settings.proxy_enabled,
|
||||
"type": settings.proxy_type,
|
||||
"host": settings.proxy_host,
|
||||
"port": settings.proxy_port,
|
||||
"username": settings.proxy_username,
|
||||
"has_password": bool(settings.proxy_password),
|
||||
},
|
||||
"registration": {
|
||||
"max_retries": settings.registration_max_retries,
|
||||
"timeout": settings.registration_timeout,
|
||||
"default_password_length": settings.registration_default_password_length,
|
||||
"sleep_min": settings.registration_sleep_min,
|
||||
"sleep_max": settings.registration_sleep_max,
|
||||
},
|
||||
"webui": {
|
||||
"host": settings.webui_host,
|
||||
"port": settings.webui_port,
|
||||
"debug": settings.debug,
|
||||
},
|
||||
"tempmail": {
|
||||
"base_url": settings.tempmail_base_url,
|
||||
"timeout": settings.tempmail_timeout,
|
||||
"max_retries": settings.tempmail_max_retries,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/proxy")
|
||||
async def get_proxy_settings():
|
||||
"""获取代理设置"""
|
||||
settings = get_settings()
|
||||
|
||||
return {
|
||||
"enabled": settings.proxy_enabled,
|
||||
"type": settings.proxy_type,
|
||||
"host": settings.proxy_host,
|
||||
"port": settings.proxy_port,
|
||||
"username": settings.proxy_username,
|
||||
"has_password": bool(settings.proxy_password),
|
||||
"proxy_url": settings.proxy_url,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/proxy")
|
||||
async def update_proxy_settings(request: ProxySettings):
|
||||
"""更新代理设置"""
|
||||
update_dict = {
|
||||
"proxy_enabled": request.enabled,
|
||||
"proxy_type": request.type,
|
||||
"proxy_host": request.host,
|
||||
"proxy_port": request.port,
|
||||
"proxy_username": request.username,
|
||||
}
|
||||
|
||||
if request.password:
|
||||
update_dict["proxy_password"] = request.password
|
||||
|
||||
update_settings(**update_dict)
|
||||
|
||||
return {"success": True, "message": "代理设置已更新"}
|
||||
|
||||
|
||||
@router.get("/registration")
|
||||
async def get_registration_settings():
|
||||
"""获取注册设置"""
|
||||
settings = get_settings()
|
||||
|
||||
return {
|
||||
"max_retries": settings.registration_max_retries,
|
||||
"timeout": settings.registration_timeout,
|
||||
"default_password_length": settings.registration_default_password_length,
|
||||
"sleep_min": settings.registration_sleep_min,
|
||||
"sleep_max": settings.registration_sleep_max,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/registration")
|
||||
async def update_registration_settings(request: RegistrationSettings):
|
||||
"""更新注册设置"""
|
||||
update_settings(
|
||||
registration_max_retries=request.max_retries,
|
||||
registration_timeout=request.timeout,
|
||||
registration_default_password_length=request.default_password_length,
|
||||
registration_sleep_min=request.sleep_min,
|
||||
registration_sleep_max=request.sleep_max,
|
||||
)
|
||||
|
||||
return {"success": True, "message": "注册设置已更新"}
|
||||
|
||||
|
||||
@router.get("/database")
|
||||
async def get_database_info():
|
||||
"""获取数据库信息"""
|
||||
settings = get_settings()
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
db_path = settings.database_url
|
||||
if db_path.startswith("sqlite:///"):
|
||||
db_path = db_path[10:]
|
||||
|
||||
db_file = Path(db_path) if os.path.isabs(db_path) else Path(db_path)
|
||||
db_size = db_file.stat().st_size if db_file.exists() else 0
|
||||
|
||||
with get_db() as db:
|
||||
from ...database.models import Account, EmailService, RegistrationTask
|
||||
|
||||
account_count = db.query(Account).count()
|
||||
service_count = db.query(EmailService).count()
|
||||
task_count = db.query(RegistrationTask).count()
|
||||
|
||||
return {
|
||||
"database_url": settings.database_url,
|
||||
"database_size_bytes": db_size,
|
||||
"database_size_mb": round(db_size / (1024 * 1024), 2),
|
||||
"accounts_count": account_count,
|
||||
"email_services_count": service_count,
|
||||
"tasks_count": task_count,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/database/backup")
|
||||
async def backup_database():
|
||||
"""备份数据库"""
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
db_path = settings.database_url
|
||||
if db_path.startswith("sqlite:///"):
|
||||
db_path = db_path[10:]
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
raise HTTPException(status_code=404, detail="数据库文件不存在")
|
||||
|
||||
# 创建备份目录
|
||||
backup_dir = Path(db_path).parent / "backups"
|
||||
backup_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 生成备份文件名
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_path = backup_dir / f"database_backup_{timestamp}.db"
|
||||
|
||||
# 复制数据库文件
|
||||
shutil.copy2(db_path, backup_path)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "数据库备份成功",
|
||||
"backup_path": str(backup_path)
|
||||
}
|
||||
|
||||
|
||||
@router.post("/database/cleanup")
|
||||
async def cleanup_database(
|
||||
days: int = 30,
|
||||
keep_failed: bool = True
|
||||
):
|
||||
"""清理过期数据"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
with get_db() as db:
|
||||
from ...database.models import RegistrationTask
|
||||
from sqlalchemy import delete
|
||||
|
||||
# 删除旧任务
|
||||
conditions = [RegistrationTask.created_at < cutoff_date]
|
||||
if not keep_failed:
|
||||
conditions.append(RegistrationTask.status != "failed")
|
||||
else:
|
||||
conditions.append(RegistrationTask.status.in_(["completed", "cancelled"]))
|
||||
|
||||
result = db.execute(
|
||||
delete(RegistrationTask).where(*conditions)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
deleted_count = result.rowcount
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"已清理 {deleted_count} 条过期任务记录",
|
||||
"deleted_count": deleted_count
|
||||
}
|
||||
|
||||
|
||||
@router.get("/logs")
|
||||
async def get_recent_logs(
|
||||
lines: int = 100,
|
||||
level: str = "INFO"
|
||||
):
|
||||
"""获取最近日志"""
|
||||
settings = get_settings()
|
||||
|
||||
log_file = settings.log_file
|
||||
if not log_file:
|
||||
return {"logs": [], "message": "日志文件未配置"}
|
||||
|
||||
from pathlib import Path
|
||||
log_path = Path(log_file)
|
||||
|
||||
if not log_path.exists():
|
||||
return {"logs": [], "message": "日志文件不存在"}
|
||||
|
||||
try:
|
||||
with open(log_path, "r", encoding="utf-8") as f:
|
||||
all_lines = f.readlines()
|
||||
recent_lines = all_lines[-lines:]
|
||||
|
||||
return {
|
||||
"logs": [line.strip() for line in recent_lines],
|
||||
"total_lines": len(all_lines)
|
||||
}
|
||||
except Exception as e:
|
||||
return {"logs": [], "error": str(e)}
|
||||
Reference in New Issue
Block a user