| | """管理接口 - Token管理和系统配置""" |
| |
|
| | import secrets |
| | import time |
| | from typing import Dict, Any, List, Optional |
| | from datetime import datetime, timedelta |
| | from pathlib import Path |
| | from fastapi import APIRouter, HTTPException, Depends, Header, Query |
| | from fastapi.responses import HTMLResponse |
| | from pydantic import BaseModel |
| |
|
| | from app.core.config import setting |
| | from app.core.logger import logger |
| | from app.services.grok.token import token_manager |
| | from app.services.request_stats import request_stats |
| | from app.models.grok_models import TokenType |
| |
|
| |
|
| | router = APIRouter(tags=["管理"]) |
| |
|
| | |
| | STATIC_DIR = Path(__file__).parents[2] / "template" |
| | TEMP_DIR = Path(__file__).parents[3] / "data" / "temp" |
| | IMAGE_CACHE_DIR = TEMP_DIR / "image" |
| | VIDEO_CACHE_DIR = TEMP_DIR / "video" |
| | SESSION_EXPIRE_HOURS = 24 |
| | BYTES_PER_KB = 1024 |
| | BYTES_PER_MB = 1024 * 1024 |
| |
|
| | |
| | _sessions: Dict[str, datetime] = {} |
| |
|
| |
|
| | |
| |
|
| | class LoginRequest(BaseModel): |
| | username: str |
| | password: str |
| |
|
| |
|
| | class LoginResponse(BaseModel): |
| | success: bool |
| | token: Optional[str] = None |
| | message: str |
| |
|
| |
|
| | class AddTokensRequest(BaseModel): |
| | tokens: List[str] |
| | token_type: str |
| |
|
| |
|
| | class DeleteTokensRequest(BaseModel): |
| | tokens: List[str] |
| | token_type: str |
| |
|
| |
|
| | class TokenInfo(BaseModel): |
| | token: str |
| | token_type: str |
| | created_time: Optional[int] = None |
| | remaining_queries: int |
| | heavy_remaining_queries: int |
| | status: str |
| | tags: List[str] = [] |
| | note: str = "" |
| | cooldown_until: Optional[int] = None |
| | cooldown_remaining: int = 0 |
| | last_failure_time: Optional[int] = None |
| | last_failure_reason: str = "" |
| | limit_reason: str = "" |
| |
|
| |
|
| | class TokenListResponse(BaseModel): |
| | success: bool |
| | data: List[TokenInfo] |
| | total: int |
| |
|
| |
|
| | class UpdateSettingsRequest(BaseModel): |
| | global_config: Optional[Dict[str, Any]] = None |
| | grok_config: Optional[Dict[str, Any]] = None |
| |
|
| |
|
| | class UpdateTokenTagsRequest(BaseModel): |
| | token: str |
| | token_type: str |
| | tags: List[str] |
| |
|
| |
|
| | class UpdateTokenNoteRequest(BaseModel): |
| | token: str |
| | token_type: str |
| | note: str |
| |
|
| |
|
| | class TestTokenRequest(BaseModel): |
| | token: str |
| | token_type: str |
| |
|
| |
|
| | |
| |
|
| | def validate_token_type(token_type_str: str) -> TokenType: |
| | """验证Token类型""" |
| | if token_type_str not in ["sso", "ssoSuper"]: |
| | raise HTTPException( |
| | status_code=400, |
| | detail={"error": "无效的Token类型", "code": "INVALID_TYPE"} |
| | ) |
| | return TokenType.NORMAL if token_type_str == "sso" else TokenType.SUPER |
| |
|
| |
|
| | def parse_created_time(created_time) -> Optional[int]: |
| | """解析创建时间""" |
| | if isinstance(created_time, str): |
| | return int(created_time) if created_time else None |
| | elif isinstance(created_time, int): |
| | return created_time |
| | return None |
| |
|
| |
|
| | def _get_cooldown_remaining_ms(token_data: Dict[str, Any], now_ms: Optional[int] = None) -> int: |
| | """获取冷却剩余时间(毫秒).""" |
| | cooldown_until = token_data.get("cooldownUntil") |
| | if not cooldown_until: |
| | return 0 |
| |
|
| | try: |
| | now = now_ms if now_ms is not None else int(time.time() * 1000) |
| | remaining = int(cooldown_until) - now |
| | return remaining if remaining > 0 else 0 |
| | except (TypeError, ValueError): |
| | return 0 |
| |
|
| |
|
| | def _is_token_in_cooldown(token_data: Dict[str, Any], now_ms: Optional[int] = None) -> bool: |
| | """判断Token是否处于429冷却中.""" |
| | return _get_cooldown_remaining_ms(token_data, now_ms) > 0 |
| |
|
| |
|
| | def calculate_token_stats(tokens: Dict[str, Any], token_type: str) -> Dict[str, int]: |
| | """计算Token统计.""" |
| | total = len(tokens) |
| | expired = sum(1 for t in tokens.values() if t.get("status") == "expired") |
| | now_ms = int(time.time() * 1000) |
| | cooldown = 0 |
| | exhausted = 0 |
| | unused = 0 |
| | active = 0 |
| |
|
| | for token_data in tokens.values(): |
| | if token_data.get("status") == "expired": |
| | continue |
| |
|
| | if _is_token_in_cooldown(token_data, now_ms): |
| | cooldown += 1 |
| | continue |
| |
|
| | remaining = token_data.get("remainingQueries", -1) |
| | heavy_remaining = token_data.get("heavyremainingQueries", -1) |
| |
|
| | if token_type == "normal": |
| | if remaining == -1: |
| | unused += 1 |
| | elif remaining == 0: |
| | exhausted += 1 |
| | else: |
| | active += 1 |
| | else: |
| | if remaining == -1 and heavy_remaining == -1: |
| | unused += 1 |
| | elif remaining == 0 or heavy_remaining == 0: |
| | exhausted += 1 |
| | else: |
| | active += 1 |
| |
|
| | limited = cooldown + exhausted |
| | return { |
| | "total": total, |
| | "unused": unused, |
| | "limited": limited, |
| | "cooldown": cooldown, |
| | "exhausted": exhausted, |
| | "expired": expired, |
| | "active": active |
| | } |
| |
|
| |
|
| | def verify_admin_session(authorization: Optional[str] = Header(None)) -> bool: |
| | """验证管理员会话""" |
| | if not authorization or not authorization.startswith("Bearer "): |
| | raise HTTPException(status_code=401, detail={"error": "未授权访问", "code": "UNAUTHORIZED"}) |
| | |
| | token = authorization[7:] |
| | |
| | if token not in _sessions: |
| | raise HTTPException(status_code=401, detail={"error": "会话无效", "code": "SESSION_INVALID"}) |
| | |
| | if datetime.now() > _sessions[token]: |
| | del _sessions[token] |
| | raise HTTPException(status_code=401, detail={"error": "会话已过期", "code": "SESSION_EXPIRED"}) |
| | |
| | return True |
| |
|
| |
|
| | def get_token_status(token_data: Dict[str, Any], token_type: str) -> str: |
| | """获取Token状态.""" |
| | if token_data.get("status") == "expired": |
| | return "失效" |
| |
|
| | if _is_token_in_cooldown(token_data): |
| | return "冷却中" |
| |
|
| | remaining = token_data.get("remainingQueries", -1) |
| | heavy_remaining = token_data.get("heavyremainingQueries", -1) |
| |
|
| | if token_type == "ssoSuper": |
| | if remaining == -1 and heavy_remaining == -1: |
| | return "未使用" |
| | if remaining == 0 or heavy_remaining == 0: |
| | return "额度耗尽" |
| | return "正常" |
| |
|
| | if remaining == -1: |
| | return "未使用" |
| | if remaining == 0: |
| | return "额度耗尽" |
| | return "正常" |
| |
|
| |
|
| | def _calculate_dir_size(directory: Path) -> int: |
| | """计算目录大小""" |
| | total = 0 |
| | for file_path in directory.iterdir(): |
| | if file_path.is_file(): |
| | try: |
| | total += file_path.stat().st_size |
| | except Exception as e: |
| | logger.warning(f"[Admin] 无法获取文件大小: {file_path.name}, {e}") |
| | return total |
| |
|
| |
|
| | def _format_size(size_bytes: int) -> str: |
| | """格式化文件大小""" |
| | size_mb = size_bytes / BYTES_PER_MB |
| | if size_mb < 1: |
| | return f"{size_bytes / BYTES_PER_KB:.1f} KB" |
| | return f"{size_mb:.1f} MB" |
| |
|
| |
|
| | |
| |
|
| | @router.get("/login", response_class=HTMLResponse) |
| | async def login_page(): |
| | """登录页面""" |
| | login_html = STATIC_DIR / "login.html" |
| | if login_html.exists(): |
| | return login_html.read_text(encoding="utf-8") |
| | raise HTTPException(status_code=404, detail="登录页面不存在") |
| |
|
| |
|
| | @router.get("/manage", response_class=HTMLResponse) |
| | async def manage_page(): |
| | """管理页面""" |
| | admin_html = STATIC_DIR / "admin.html" |
| | if admin_html.exists(): |
| | return admin_html.read_text(encoding="utf-8") |
| | raise HTTPException(status_code=404, detail="管理页面不存在") |
| |
|
| |
|
| | |
| |
|
| | @router.post("/api/login", response_model=LoginResponse) |
| | async def admin_login(request: LoginRequest) -> LoginResponse: |
| | """管理员登录""" |
| | try: |
| | logger.debug(f"[Admin] 登录尝试: {request.username}") |
| |
|
| | expected_user = setting.global_config.get("admin_username", "") |
| | expected_pass = setting.global_config.get("admin_password", "") |
| |
|
| | if request.username != expected_user or request.password != expected_pass: |
| | logger.warning(f"[Admin] 登录失败: {request.username}") |
| | return LoginResponse(success=False, message="用户名或密码错误") |
| |
|
| | session_token = secrets.token_urlsafe(32) |
| | _sessions[session_token] = datetime.now() + timedelta(hours=SESSION_EXPIRE_HOURS) |
| |
|
| | logger.debug(f"[Admin] 登录成功: {request.username}") |
| | return LoginResponse(success=True, token=session_token, message="登录成功") |
| |
|
| | except Exception as e: |
| | logger.error(f"[Admin] 登录异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"登录失败: {e}", "code": "LOGIN_ERROR"}) |
| |
|
| |
|
| | @router.post("/api/logout") |
| | async def admin_logout(_: bool = Depends(verify_admin_session), authorization: Optional[str] = Header(None)) -> Dict[str, Any]: |
| | """管理员登出""" |
| | try: |
| | if authorization and authorization.startswith("Bearer "): |
| | token = authorization[7:] |
| | if token in _sessions: |
| | del _sessions[token] |
| | logger.debug("[Admin] 登出成功") |
| | return {"success": True, "message": "登出成功"} |
| |
|
| | logger.warning("[Admin] 登出失败: 无效会话") |
| | return {"success": False, "message": "无效的会话"} |
| |
|
| | except Exception as e: |
| | logger.error(f"[Admin] 登出异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"登出失败: {e}", "code": "LOGOUT_ERROR"}) |
| |
|
| |
|
| | @router.get("/api/tokens", response_model=TokenListResponse) |
| | async def list_tokens(_: bool = Depends(verify_admin_session)) -> TokenListResponse: |
| | """获取Token列表""" |
| | try: |
| | logger.debug("[Admin] 获取Token列表") |
| |
|
| | all_tokens = token_manager.get_tokens() |
| | token_list: List[TokenInfo] = [] |
| | now_ms = int(time.time() * 1000) |
| |
|
| | |
| | for token, data in all_tokens.get(TokenType.NORMAL.value, {}).items(): |
| | cooldown_remaining_ms = _get_cooldown_remaining_ms(data, now_ms) |
| | cooldown_until = data.get("cooldownUntil") if cooldown_remaining_ms else None |
| | limit_reason = "cooldown" if cooldown_remaining_ms else "" |
| | if not limit_reason and data.get("remainingQueries", -1) == 0: |
| | limit_reason = "exhausted" |
| | token_list.append(TokenInfo( |
| | token=token, |
| | token_type="sso", |
| | created_time=parse_created_time(data.get("createdTime")), |
| | remaining_queries=data.get("remainingQueries", -1), |
| | heavy_remaining_queries=data.get("heavyremainingQueries", -1), |
| | status=get_token_status(data, "sso"), |
| | tags=data.get("tags", []), |
| | note=data.get("note", ""), |
| | cooldown_until=cooldown_until, |
| | cooldown_remaining=(cooldown_remaining_ms + 999) // 1000 if cooldown_remaining_ms else 0, |
| | last_failure_time=data.get("lastFailureTime") or None, |
| | last_failure_reason=data.get("lastFailureReason") or "", |
| | limit_reason=limit_reason |
| | )) |
| |
|
| | |
| | for token, data in all_tokens.get(TokenType.SUPER.value, {}).items(): |
| | cooldown_remaining_ms = _get_cooldown_remaining_ms(data, now_ms) |
| | cooldown_until = data.get("cooldownUntil") if cooldown_remaining_ms else None |
| | limit_reason = "cooldown" if cooldown_remaining_ms else "" |
| | if not limit_reason and (data.get("remainingQueries", -1) == 0 or data.get("heavyremainingQueries", -1) == 0): |
| | limit_reason = "exhausted" |
| | token_list.append(TokenInfo( |
| | token=token, |
| | token_type="ssoSuper", |
| | created_time=parse_created_time(data.get("createdTime")), |
| | remaining_queries=data.get("remainingQueries", -1), |
| | heavy_remaining_queries=data.get("heavyremainingQueries", -1), |
| | status=get_token_status(data, "ssoSuper"), |
| | tags=data.get("tags", []), |
| | note=data.get("note", ""), |
| | cooldown_until=cooldown_until, |
| | cooldown_remaining=(cooldown_remaining_ms + 999) // 1000 if cooldown_remaining_ms else 0, |
| | last_failure_time=data.get("lastFailureTime") or None, |
| | last_failure_reason=data.get("lastFailureReason") or "", |
| | limit_reason=limit_reason |
| | )) |
| |
|
| | logger.debug(f"[Admin] Token列表获取成功: {len(token_list)}个") |
| | return TokenListResponse(success=True, data=token_list, total=len(token_list)) |
| |
|
| | except Exception as e: |
| | logger.error(f"[Admin] 获取Token列表异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"获取失败: {e}", "code": "LIST_ERROR"}) |
| |
|
| |
|
| | @router.post("/api/tokens/add") |
| | async def add_tokens(request: AddTokensRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """批量添加Token""" |
| | try: |
| | logger.debug(f"[Admin] 添加Token: {request.token_type}, {len(request.tokens)}个") |
| |
|
| | token_type = validate_token_type(request.token_type) |
| | await token_manager.add_token(request.tokens, token_type) |
| |
|
| | logger.debug(f"[Admin] Token添加成功: {len(request.tokens)}个") |
| | return {"success": True, "message": f"成功添加 {len(request.tokens)} 个Token", "count": len(request.tokens)} |
| |
|
| | except HTTPException: |
| | raise |
| | except Exception as e: |
| | logger.error(f"[Admin] Token添加异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"添加失败: {e}", "code": "ADD_ERROR"}) |
| |
|
| |
|
| | @router.post("/api/tokens/delete") |
| | async def delete_tokens(request: DeleteTokensRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """批量删除Token""" |
| | try: |
| | logger.debug(f"[Admin] 删除Token: {request.token_type}, {len(request.tokens)}个") |
| |
|
| | token_type = validate_token_type(request.token_type) |
| | await token_manager.delete_token(request.tokens, token_type) |
| |
|
| | logger.debug(f"[Admin] Token删除成功: {len(request.tokens)}个") |
| | return {"success": True, "message": f"成功删除 {len(request.tokens)} 个Token", "count": len(request.tokens)} |
| |
|
| | except HTTPException: |
| | raise |
| | except Exception as e: |
| | logger.error(f"[Admin] Token删除异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"删除失败: {e}", "code": "DELETE_ERROR"}) |
| |
|
| |
|
| | @router.get("/api/settings") |
| | async def get_settings(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """获取配置""" |
| | try: |
| | logger.debug("[Admin] 获取配置") |
| | return {"success": True, "data": {"global": setting.global_config, "grok": setting.grok_config}} |
| | except Exception as e: |
| | logger.error(f"[Admin] 获取配置失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"获取失败: {e}", "code": "GET_SETTINGS_ERROR"}) |
| |
|
| |
|
| | @router.post("/api/settings") |
| | async def update_settings(request: UpdateSettingsRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """更新配置""" |
| | try: |
| | logger.debug("[Admin] 更新配置") |
| | await setting.save(global_config=request.global_config, grok_config=request.grok_config) |
| | logger.debug("[Admin] 配置更新成功") |
| | return {"success": True, "message": "配置更新成功"} |
| | except Exception as e: |
| | logger.error(f"[Admin] 更新配置失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"更新失败: {e}", "code": "UPDATE_SETTINGS_ERROR"}) |
| |
|
| |
|
| | @router.get("/api/cache/size") |
| | async def get_cache_size(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """获取缓存大小""" |
| | try: |
| | logger.debug("[Admin] 获取缓存大小") |
| |
|
| | image_size = _calculate_dir_size(IMAGE_CACHE_DIR) if IMAGE_CACHE_DIR.exists() else 0 |
| | video_size = _calculate_dir_size(VIDEO_CACHE_DIR) if VIDEO_CACHE_DIR.exists() else 0 |
| | total_size = image_size + video_size |
| |
|
| | logger.debug(f"[Admin] 缓存大小: 图片{_format_size(image_size)}, 视频{_format_size(video_size)}") |
| | |
| | return { |
| | "success": True, |
| | "data": { |
| | "image_size": _format_size(image_size), |
| | "video_size": _format_size(video_size), |
| | "total_size": _format_size(total_size), |
| | "image_size_bytes": image_size, |
| | "video_size_bytes": video_size, |
| | "total_size_bytes": total_size |
| | } |
| | } |
| |
|
| | except Exception as e: |
| | logger.error(f"[Admin] 获取缓存大小异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"获取失败: {e}", "code": "CACHE_SIZE_ERROR"}) |
| |
|
| |
|
| | @router.get("/api/cache/list") |
| | async def list_cache_files( |
| | cache_type: str = Query("image", alias="type"), |
| | limit: int = 50, |
| | offset: int = 0, |
| | _: bool = Depends(verify_admin_session) |
| | ) -> Dict[str, Any]: |
| | """List cached files for admin preview.""" |
| | try: |
| | cache_type = cache_type.lower() |
| | if cache_type not in ("image", "video"): |
| | raise HTTPException(status_code=400, detail={"error": "Invalid cache type", "code": "INVALID_CACHE_TYPE"}) |
| |
|
| | if limit < 1: |
| | limit = 1 |
| | if limit > 200: |
| | limit = 200 |
| | if offset < 0: |
| | offset = 0 |
| |
|
| | cache_dir = IMAGE_CACHE_DIR if cache_type == "image" else VIDEO_CACHE_DIR |
| | if not cache_dir.exists(): |
| | return {"success": True, "data": {"total": 0, "items": [], "offset": offset, "limit": limit, "has_more": False}} |
| |
|
| | files = [] |
| | for file_path in cache_dir.iterdir(): |
| | if not file_path.is_file(): |
| | continue |
| | try: |
| | stat = file_path.stat() |
| | except Exception as e: |
| | logger.warning(f"[Admin] Skip cache file: {file_path.name}, {e}") |
| | continue |
| | files.append((file_path, stat.st_mtime, stat.st_size)) |
| |
|
| | files.sort(key=lambda item: item[1], reverse=True) |
| | total = len(files) |
| | sliced = files[offset:offset + limit] |
| |
|
| | items = [ |
| | { |
| | "name": file_path.name, |
| | "size": _format_size(size), |
| | "size_bytes": size, |
| | "mtime": int(mtime * 1000), |
| | "url": f"/images/{file_path.name}", |
| | "type": cache_type |
| | } |
| | for file_path, mtime, size in sliced |
| | ] |
| |
|
| | return { |
| | "success": True, |
| | "data": { |
| | "total": total, |
| | "items": items, |
| | "offset": offset, |
| | "limit": limit, |
| | "has_more": offset + limit < total |
| | } |
| | } |
| |
|
| | except HTTPException: |
| | raise |
| | except Exception as e: |
| | logger.error(f"[Admin] 获取缓存列表异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"获取失败: {e}", "code": "CACHE_LIST_ERROR"}) |
| |
|
| |
|
| | @router.post("/api/cache/clear") |
| | async def clear_cache(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """清理所有缓存""" |
| | try: |
| | logger.debug("[Admin] 清理缓存") |
| |
|
| | image_count = 0 |
| | video_count = 0 |
| |
|
| | |
| | if IMAGE_CACHE_DIR.exists(): |
| | for file_path in IMAGE_CACHE_DIR.iterdir(): |
| | if file_path.is_file(): |
| | try: |
| | file_path.unlink() |
| | image_count += 1 |
| | except Exception as e: |
| | logger.error(f"[Admin] 删除失败: {file_path.name}, {e}") |
| |
|
| | |
| | if VIDEO_CACHE_DIR.exists(): |
| | for file_path in VIDEO_CACHE_DIR.iterdir(): |
| | if file_path.is_file(): |
| | try: |
| | file_path.unlink() |
| | video_count += 1 |
| | except Exception as e: |
| | logger.error(f"[Admin] 删除失败: {file_path.name}, {e}") |
| |
|
| | total = image_count + video_count |
| | logger.debug(f"[Admin] 缓存清理完成: 图片{image_count}, 视频{video_count}") |
| |
|
| | return { |
| | "success": True, |
| | "message": f"成功清理缓存,删除图片 {image_count} 个,视频 {video_count} 个,共 {total} 个文件", |
| | "data": {"deleted_count": total, "image_count": image_count, "video_count": video_count} |
| | } |
| |
|
| | except Exception as e: |
| | logger.error(f"[Admin] 清理缓存异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"清理失败: {e}", "code": "CACHE_CLEAR_ERROR"}) |
| |
|
| |
|
| | @router.post("/api/cache/clear/images") |
| | async def clear_image_cache(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """清理图片缓存""" |
| | try: |
| | logger.debug("[Admin] 清理图片缓存") |
| |
|
| | count = 0 |
| | if IMAGE_CACHE_DIR.exists(): |
| | for file_path in IMAGE_CACHE_DIR.iterdir(): |
| | if file_path.is_file(): |
| | try: |
| | file_path.unlink() |
| | count += 1 |
| | except Exception as e: |
| | logger.error(f"[Admin] 删除失败: {file_path.name}, {e}") |
| |
|
| | logger.debug(f"[Admin] 图片缓存清理完成: {count}个") |
| | return {"success": True, "message": f"成功清理图片缓存,删除 {count} 个文件", "data": {"deleted_count": count, "type": "images"}} |
| |
|
| | except Exception as e: |
| | logger.error(f"[Admin] 清理图片缓存异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"清理失败: {e}", "code": "IMAGE_CACHE_CLEAR_ERROR"}) |
| |
|
| |
|
| | @router.post("/api/cache/clear/videos") |
| | async def clear_video_cache(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """清理视频缓存""" |
| | try: |
| | logger.debug("[Admin] 清理视频缓存") |
| |
|
| | count = 0 |
| | if VIDEO_CACHE_DIR.exists(): |
| | for file_path in VIDEO_CACHE_DIR.iterdir(): |
| | if file_path.is_file(): |
| | try: |
| | file_path.unlink() |
| | count += 1 |
| | except Exception as e: |
| | logger.error(f"[Admin] 删除失败: {file_path.name}, {e}") |
| |
|
| | logger.debug(f"[Admin] 视频缓存清理完成: {count}个") |
| | return {"success": True, "message": f"成功清理视频缓存,删除 {count} 个文件", "data": {"deleted_count": count, "type": "videos"}} |
| |
|
| | except Exception as e: |
| | logger.error(f"[Admin] 清理视频缓存异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"清理失败: {e}", "code": "VIDEO_CACHE_CLEAR_ERROR"}) |
| |
|
| |
|
| | @router.get("/api/stats") |
| | async def get_stats(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """获取统计信息""" |
| | try: |
| | logger.debug("[Admin] 开始获取统计信息") |
| |
|
| | all_tokens = token_manager.get_tokens() |
| | normal_stats = calculate_token_stats(all_tokens.get(TokenType.NORMAL.value, {}), "normal") |
| | super_stats = calculate_token_stats(all_tokens.get(TokenType.SUPER.value, {}), "super") |
| | total = normal_stats["total"] + super_stats["total"] |
| |
|
| | logger.debug(f"[Admin] 统计信息获取成功 - 普通Token: {normal_stats['total']}, Super Token: {super_stats['total']}, 总计: {total}") |
| | return {"success": True, "data": {"normal": normal_stats, "super": super_stats, "total": total}} |
| |
|
| | except Exception as e: |
| | logger.error(f"[Admin] 获取统计信息异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"获取失败: {e}", "code": "STATS_ERROR"}) |
| |
|
| |
|
| | @router.get("/api/storage/mode") |
| | async def get_storage_mode(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """获取存储模式""" |
| | try: |
| | logger.debug("[Admin] 获取存储模式") |
| | import os |
| | mode = os.getenv("STORAGE_MODE", "file").upper() |
| | return {"success": True, "data": {"mode": mode}} |
| | except Exception as e: |
| | logger.error(f"[Admin] 获取存储模式异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"获取失败: {e}", "code": "STORAGE_MODE_ERROR"}) |
| |
|
| |
|
| | @router.post("/api/tokens/tags") |
| | async def update_token_tags(request: UpdateTokenTagsRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """更新Token标签""" |
| | try: |
| | logger.debug(f"[Admin] 更新Token标签: {request.token[:10]}..., {request.tags}") |
| |
|
| | token_type = validate_token_type(request.token_type) |
| | await token_manager.update_token_tags(request.token, token_type, request.tags) |
| |
|
| | logger.debug(f"[Admin] Token标签更新成功: {request.token[:10]}...") |
| | return {"success": True, "message": "标签更新成功", "tags": request.tags} |
| |
|
| | except HTTPException: |
| | raise |
| | except Exception as e: |
| | logger.error(f"[Admin] Token标签更新异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"更新失败: {e}", "code": "UPDATE_TAGS_ERROR"}) |
| |
|
| |
|
| | @router.get("/api/tokens/tags/all") |
| | async def get_all_tags(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """获取所有标签""" |
| | try: |
| | logger.debug("[Admin] 获取所有标签") |
| |
|
| | all_tokens = token_manager.get_tokens() |
| | tags_set = set() |
| |
|
| | for token_type_data in all_tokens.values(): |
| | for token_data in token_type_data.values(): |
| | tags = token_data.get("tags", []) |
| | if isinstance(tags, list): |
| | tags_set.update(tags) |
| |
|
| | tags_list = sorted(list(tags_set)) |
| | logger.debug(f"[Admin] 标签获取成功: {len(tags_list)}个") |
| | return {"success": True, "data": tags_list} |
| |
|
| | except Exception as e: |
| | logger.error(f"[Admin] 获取标签异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"获取失败: {e}", "code": "GET_TAGS_ERROR"}) |
| |
|
| |
|
| | @router.post("/api/tokens/note") |
| | async def update_token_note(request: UpdateTokenNoteRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """更新Token备注""" |
| | try: |
| | logger.debug(f"[Admin] 更新Token备注: {request.token[:10]}...") |
| |
|
| | token_type = validate_token_type(request.token_type) |
| | await token_manager.update_token_note(request.token, token_type, request.note) |
| |
|
| | logger.debug(f"[Admin] Token备注更新成功: {request.token[:10]}...") |
| | return {"success": True, "message": "备注更新成功", "note": request.note} |
| |
|
| | except HTTPException: |
| | raise |
| | except Exception as e: |
| | logger.error(f"[Admin] Token备注更新异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"更新失败: {e}", "code": "UPDATE_NOTE_ERROR"}) |
| |
|
| |
|
| | @router.post("/api/tokens/test") |
| | async def test_token(request: TestTokenRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """测试Token可用性""" |
| | try: |
| | logger.debug(f"[Admin] 测试Token: {request.token[:10]}...") |
| |
|
| | token_type = validate_token_type(request.token_type) |
| | auth_token = f"sso-rw={request.token};sso={request.token}" |
| |
|
| | result = await token_manager.check_limits(auth_token, "grok-4-fast") |
| |
|
| | if result: |
| | logger.debug(f"[Admin] Token测试成功: {request.token[:10]}...") |
| | return { |
| | "success": True, |
| | "message": "Token有效", |
| | "data": { |
| | "valid": True, |
| | "remaining_queries": result.get("remainingTokens", -1), |
| | "limit": result.get("limit", -1) |
| | } |
| | } |
| | else: |
| | logger.warning(f"[Admin] Token测试失败: {request.token[:10]}...") |
| |
|
| | all_tokens = token_manager.get_tokens() |
| | token_data = all_tokens.get(token_type.value, {}).get(request.token) |
| |
|
| | if token_data: |
| | if token_data.get("status") == "expired": |
| | return {"success": False, "message": "Token已失效", "data": {"valid": False, "error_type": "expired", "error_code": 401}} |
| | cooldown_remaining_ms = _get_cooldown_remaining_ms(token_data) |
| | if cooldown_remaining_ms: |
| | return { |
| | "success": False, |
| | "message": "Token处于冷却中", |
| | "data": { |
| | "valid": False, |
| | "error_type": "cooldown", |
| | "error_code": 429, |
| | "cooldown_remaining": (cooldown_remaining_ms + 999) // 1000 |
| | } |
| | } |
| |
|
| | exhausted = token_data.get("remainingQueries") == 0 |
| | if token_type == TokenType.SUPER and token_data.get("heavyremainingQueries") == 0: |
| | exhausted = True |
| | if exhausted: |
| | return { |
| | "success": False, |
| | "message": "Token额度耗尽", |
| | "data": {"valid": False, "error_type": "exhausted", "error_code": "quota_exhausted"} |
| | } |
| | else: |
| | return {"success": False, "message": "服务器被block或网络错误", "data": {"valid": False, "error_type": "blocked", "error_code": 403}} |
| | else: |
| | return {"success": False, "message": "Token数据异常", "data": {"valid": False, "error_type": "unknown", "error_code": "data_error"}} |
| |
|
| | except HTTPException: |
| | raise |
| | except Exception as e: |
| | logger.error(f"[Admin] Token测试异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"测试失败: {e}", "code": "TEST_TOKEN_ERROR"}) |
| |
|
| |
|
| | @router.post("/api/tokens/refresh-all") |
| | async def refresh_all_tokens(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """一键刷新所有Token的剩余次数(后台执行)""" |
| | import asyncio |
| | |
| | try: |
| | |
| | progress = token_manager.get_refresh_progress() |
| | if progress.get("running"): |
| | return { |
| | "success": False, |
| | "message": "刷新任务正在进行中", |
| | "data": progress |
| | } |
| | |
| | |
| | logger.info("[Admin] 启动后台刷新任务") |
| | asyncio.create_task(token_manager.refresh_all_limits()) |
| | |
| | |
| | return { |
| | "success": True, |
| | "message": "刷新任务已启动", |
| | "data": {"started": True} |
| | } |
| | except Exception as e: |
| | logger.error(f"[Admin] 刷新Token异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"刷新失败: {e}", "code": "REFRESH_ALL_ERROR"}) |
| |
|
| |
|
| | @router.get("/api/tokens/refresh-progress") |
| | async def get_refresh_progress(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """获取Token刷新进度""" |
| | try: |
| | progress = token_manager.get_refresh_progress() |
| | return {"success": True, "data": progress} |
| | except Exception as e: |
| | logger.error(f"[Admin] 获取刷新进度异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"获取进度失败: {e}"}) |
| |
|
| |
|
| | @router.get("/api/request-stats") |
| | async def get_request_stats(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """获取请求统计数据""" |
| | try: |
| | stats = request_stats.get_stats(hours=24, days=7) |
| | return {"success": True, "data": stats} |
| | except Exception as e: |
| | logger.error(f"[Admin] 获取请求统计异常: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"获取统计失败: {e}"}) |
| |
|
| |
|
| | |
| |
|
| | class AddKeyRequest(BaseModel): |
| | name: str |
| |
|
| |
|
| | class UpdateKeyNameRequest(BaseModel): |
| | key: str |
| | name: str |
| |
|
| |
|
| | class UpdateKeyStatusRequest(BaseModel): |
| | key: str |
| | is_active: bool |
| |
|
| |
|
| | class BatchAddKeyRequest(BaseModel): |
| | name_prefix: str |
| | count: int |
| |
|
| |
|
| | class BatchDeleteKeyRequest(BaseModel): |
| | keys: List[str] |
| |
|
| |
|
| | class BatchUpdateKeyStatusRequest(BaseModel): |
| | keys: List[str] |
| | is_active: bool |
| |
|
| |
|
| | @router.get("/api/keys") |
| | async def list_keys(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """获取 Key 列表""" |
| | try: |
| | from app.services.api_keys import api_key_manager |
| | if not api_key_manager._loaded: |
| | await api_key_manager.init() |
| | |
| | keys = api_key_manager.get_all_keys() |
| | |
| | |
| | global_key = setting.grok_config.get("api_key") |
| | result_keys = [] |
| | |
| | |
| | for k in keys: |
| | result_keys.append({ |
| | **k, |
| | "display_key": f"{k['key'][:6]}...{k['key'][-4:]}" |
| | }) |
| | |
| | return { |
| | "success": True, |
| | "data": result_keys, |
| | "global_key_set": bool(global_key) |
| | } |
| | except Exception as e: |
| | logger.error(f"[Admin] 获取Key列表失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"获取失败: {e}"}) |
| |
|
| |
|
| | @router.post("/api/keys/add") |
| | async def add_key(request: AddKeyRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """添加 Key""" |
| | try: |
| | from app.services.api_keys import api_key_manager |
| | new_key = await api_key_manager.add_key(request.name) |
| | return {"success": True, "data": new_key, "message": "Key创建成功"} |
| | except Exception as e: |
| | logger.error(f"[Admin] 添加Key失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"添加失败: {e}"}) |
| |
|
| |
|
| | @router.post("/api/keys/delete") |
| | async def delete_key(request: Dict[str, str], _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """删除 Key""" |
| | try: |
| | from app.services.api_keys import api_key_manager |
| | key = request.get("key") |
| | if not key: |
| | raise ValueError("Key cannot be empty") |
| | |
| | if await api_key_manager.delete_key(key): |
| | return {"success": True, "message": "Key删除成功"} |
| | return {"success": False, "message": "Key不存在"} |
| | except Exception as e: |
| | logger.error(f"[Admin] 删除Key失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"删除失败: {e}"}) |
| |
|
| |
|
| | @router.post("/api/keys/status") |
| | async def update_key_status(request: UpdateKeyStatusRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """更新 Key 状态""" |
| | try: |
| | from app.services.api_keys import api_key_manager |
| | if await api_key_manager.update_key_status(request.key, request.is_active): |
| | return {"success": True, "message": "状态更新成功"} |
| | return {"success": False, "message": "Key不存在"} |
| | except Exception as e: |
| | logger.error(f"[Admin] 更新Key状态失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"更新失败: {e}"}) |
| | |
| |
|
| | @router.post("/api/keys/name") |
| | async def update_key_name(request: UpdateKeyNameRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """更新 Key 备注""" |
| | try: |
| | from app.services.api_keys import api_key_manager |
| | if await api_key_manager.update_key_name(request.key, request.name): |
| | return {"success": True, "message": "备注更新成功"} |
| | return {"success": False, "message": "Key不存在"} |
| | except Exception as e: |
| | logger.error(f"[Admin] 更新Key备注失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"更新失败: {e}"}) |
| |
|
| |
|
| | @router.post("/api/keys/batch-add") |
| | async def batch_add_keys(request: BatchAddKeyRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """批量添加 Key""" |
| | try: |
| | from app.services.api_keys import api_key_manager |
| | new_keys = await api_key_manager.batch_add_keys(request.name_prefix, request.count) |
| | return {"success": True, "data": new_keys, "message": f"成功创建 {len(new_keys)} 个 Key"} |
| | except Exception as e: |
| | logger.error(f"[Admin] 批量添加Key失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"批量添加失败: {e}"}) |
| |
|
| |
|
| | @router.post("/api/keys/batch-delete") |
| | async def batch_delete_keys(request: BatchDeleteKeyRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """批量删除 Key""" |
| | try: |
| | from app.services.api_keys import api_key_manager |
| | deleted_count = await api_key_manager.batch_delete_keys(request.keys) |
| | return {"success": True, "message": f"成功删除 {deleted_count} 个 Key"} |
| | except Exception as e: |
| | logger.error(f"[Admin] 批量删除Key失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"批量删除失败: {e}"}) |
| |
|
| |
|
| | @router.post("/api/keys/batch-status") |
| | async def batch_update_key_status(request: BatchUpdateKeyStatusRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """批量更新 Key 状态""" |
| | try: |
| | from app.services.api_keys import api_key_manager |
| | updated_count = await api_key_manager.batch_update_keys_status(request.keys, request.is_active) |
| | return {"success": True, "message": f"成功更新 {updated_count} 个 Key 状态"} |
| | except Exception as e: |
| | logger.error(f"[Admin] 批量更新Key状态失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"批量更新失败: {e}"}) |
| |
|
| |
|
| | |
| |
|
| | @router.get("/api/logs") |
| | async def get_logs(limit: int = 1000, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """获取请求日志""" |
| | try: |
| | from app.services.request_logger import request_logger |
| | logs = await request_logger.get_logs(limit) |
| | return {"success": True, "data": logs} |
| | except Exception as e: |
| | logger.error(f"[Admin] 获取日志失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"获取失败: {e}"}) |
| |
|
| | @router.post("/api/logs/clear") |
| | async def clear_logs(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: |
| | """清空日志""" |
| | try: |
| | from app.services.request_logger import request_logger |
| | await request_logger.clear_logs() |
| | return {"success": True, "message": "日志已清空"} |
| | except Exception as e: |
| | logger.error(f"[Admin] 清空日志失败: {e}") |
| | raise HTTPException(status_code=500, detail={"error": f"清空失败: {e}"}) |
| |
|
| |
|