From 16aa0122cf815160b786cbbdb3a5499c0c47527f Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Wed, 22 Oct 2025 17:32:16 +0800 Subject: [PATCH] feat: support test SSO --- app/api/admin/manage.py | 159 ++++++++++++++++++++++++++++++++++++- app/services/grok/token.py | 32 +++++++- app/template/admin.html | 95 +++++++++++++++++----- 3 files changed, 264 insertions(+), 22 deletions(-) diff --git a/app/api/admin/manage.py b/app/api/admin/manage.py index faba124..25445ec 100644 --- a/app/api/admin/manage.py +++ b/app/api/admin/manage.py @@ -70,6 +70,7 @@ class TokenInfo(BaseModel): heavy_remaining_queries: int status: str # "未使用"、"限流中"、"失效"、"正常" tags: List[str] = [] # 标签列表 + note: str = "" # 备注 class TokenListResponse(BaseModel): @@ -302,7 +303,8 @@ async def list_tokens(_: bool = Depends(verify_admin_session)) -> TokenListRespo remaining_queries=data.get("remainingQueries", -1), heavy_remaining_queries=data.get("heavyremainingQueries", -1), status=get_token_status(data, "sso"), - tags=data.get("tags", []) # 向后兼容,如果没有tags字段则返回空列表 + tags=data.get("tags", []), # 向后兼容,如果没有tags字段则返回空列表 + note=data.get("note", "") # 向后兼容,如果没有note字段则返回空字符串 )) # 处理Super Token @@ -315,7 +317,8 @@ async def list_tokens(_: bool = Depends(verify_admin_session)) -> TokenListRespo remaining_queries=data.get("remainingQueries", -1), heavy_remaining_queries=data.get("heavyremainingQueries", -1), status=get_token_status(data, "ssoSuper"), - tags=data.get("tags", []) # 向后兼容,如果没有tags字段则返回空列表 + tags=data.get("tags", []), # 向后兼容,如果没有tags字段则返回空列表 + note=data.get("note", "") # 向后兼容,如果没有note字段则返回空字符串 )) normal_count = len(normal_tokens) @@ -802,3 +805,155 @@ async def get_all_tags(_: bool = Depends(verify_admin_session)) -> Dict[str, Any status_code=500, detail={"error": f"获取标签失败: {str(e)}", "code": "GET_TAGS_ERROR"} ) + + +class UpdateTokenNoteRequest(BaseModel): + """更新Token备注请求""" + token: str + token_type: str + note: str + + +@router.post("/api/tokens/note") +async def update_token_note( + request: UpdateTokenNoteRequest, + _: bool = Depends(verify_admin_session) +) -> Dict[str, Any]: + """ + 更新Token备注 + + 为指定Token添加或修改备注信息。 + """ + try: + logger.debug(f"[Admin] 更新Token备注 - Token: {request.token[:10]}...") + + # 验证并转换token类型 + 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备注更新成功 - Token: {request.token[:10]}...") + + return { + "success": True, + "message": "备注更新成功", + "note": request.note + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"[Admin] Token备注更新异常 - Token: {request.token[:10]}..., 错误: {str(e)}") + raise HTTPException( + status_code=500, + detail={"error": f"更新备注失败: {str(e)}", "code": "UPDATE_NOTE_ERROR"} + ) + + +class TestTokenRequest(BaseModel): + """测试Token请求""" + token: str + token_type: str + + +@router.post("/api/tokens/test") +async def test_token( + request: TestTokenRequest, + _: bool = Depends(verify_admin_session) +) -> Dict[str, Any]: + """ + 测试Token可用性 + + 通过发送速率限制检查请求来验证Token是否有效。 + 根据不同的HTTP状态码进行相应的处理: + - 401: Token失效 + - 403: 服务器被block,不改变Token状态 + - 其他错误: 设置为限流状态 + """ + try: + logger.debug(f"[Admin] 测试Token - Token: {request.token[:10]}...") + + # 验证并转换token类型 + token_type = validate_token_type(request.token_type) + + # 构造完整的auth token + auth_token = f"sso-rw={request.token};sso={request.token}" + + # 使用check_limits方法测试token + result = await token_manager.check_limits(auth_token, "grok-4-fast") + + if result: + logger.debug(f"[Admin] Token测试成功 - Token: {request.token[:10]}...") + return { + "success": True, + "message": "Token有效", + "data": { + "valid": True, + "remaining_queries": result.get("remainingTokens", -1), + "limit": result.get("limit", -1) + } + } + else: + # 测试失败,check_limits方法已经调用了record_failure处理错误 + # 现在检查token的状态来判断错误类型 + logger.warning(f"[Admin] Token测试失败 - Token: {request.token[:10]}...") + + # 检查token当前状态 + 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": + # Token被标记为失效(401错误) + return { + "success": False, + "message": "Token已失效", + "data": { + "valid": False, + "error_type": "expired", + "error_code": 401 + } + } + elif token_data.get("remainingQueries") == 0: + # Token被设置为限流状态(其他错误) + return { + "success": False, + "message": "Token已被限流", + "data": { + "valid": False, + "error_type": "limited", + "error_code": "other" + } + } + else: + # 可能是403错误或其他网络问题,token状态未变 + return { + "success": False, + "message": "服务器被block或网络错误", + "data": { + "valid": False, + "error_type": "blocked", + "error_code": 403 + } + } + else: + # 找不到token数据 + 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测试异常 - Token: {request.token[:10]}..., 错误: {str(e)}") + raise HTTPException( + status_code=500, + detail={"error": f"测试Token失败: {str(e)}", "code": "TEST_TOKEN_ERROR"} + ) diff --git a/app/services/grok/token.py b/app/services/grok/token.py index f1cb5c5..f93f714 100644 --- a/app/services/grok/token.py +++ b/app/services/grok/token.py @@ -135,7 +135,8 @@ class GrokTokenManager: "failedCount": 0, "lastFailureTime": None, "lastFailureReason": None, - "tags": [] + "tags": [], + "note": "" } added_count += 1 @@ -175,6 +176,21 @@ class GrokTokenManager: await self._save_data() logger.info(f"[Token] 成功更新Token {token[:10]}... 的标签: {cleaned_tags}") + + async def update_token_note(self, token: str, token_type: TokenType, note: str) -> None: + """更新Token的备注""" + if token not in self.token_data[token_type.value]: + logger.warning(f"[Token] Token不存在: {token[:10]}...") + raise GrokApiException( + "Token不存在", + "TOKEN_NOT_FOUND", + {"token": token[:10]} + ) + + self.token_data[token_type.value][token]["note"] = note.strip() + + await self._save_data() + logger.info(f"[Token] 成功更新Token {token[:10]}... 的备注") def get_tokens(self) -> Dict[str, Any]: """获取所有Token数据""" @@ -308,6 +324,20 @@ class GrokTokenManager: return rate_limit_data else: logger.warning(f"[Token] 获取速率限制失败,状态码: {response.status_code}") + + # 根据HTTP状态码处理不同的错误 + sso_value = self._extract_sso(auth_token) + if sso_value: + if response.status_code == 401: + # Token失效 + await self.record_failure(auth_token, 401, "Token authentication failed") + elif response.status_code == 403: + # 服务器被block,不改变token状态,但记录失败信息 + await self.record_failure(auth_token, 403, "Server blocked (Cloudflare)") + else: + # 其他错误,设置为限流状态 + await self.record_failure(auth_token, response.status_code, f"Rate limit or other error: {response.status_code}") + return None except Exception as e: diff --git a/app/template/admin.html b/app/template/admin.html index ea59833..306df46 100644 --- a/app/template/admin.html +++ b/app/template/admin.html @@ -21,8 +21,53 @@ .hover-card-content.bottom::after{bottom:100%;border-bottom-color:hsl(0 0% 3.9%)} .hover-card-trigger:hover+.hover-card-content{opacity:1;visibility:visible} [title]{position:relative} - [title]:hover::after{content:attr(title);position:absolute;bottom:100%;left:50%;transform:translateX(-50%) translateY(-4px);background:hsl(0 0% 3.9%);color:hsl(0 0% 98%);padding:8px 12px;border-radius:4px;font-size:11px;white-space:pre-line;max-width:500px;min-width:300px;word-wrap:break-word;z-index:1000;pointer-events:none;box-shadow:0 2px 8px rgba(0,0,0,.15)} - [title]:hover::before{content:'';position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:4px solid transparent;border-top-color:hsl(0 0% 3.9%);z-index:1000} + [title]:hover::after{ + content:attr(title); + position:absolute; + bottom:100%; + left:50%; + transform:translateX(-50%) translateY(-8px); + background:hsl(0 0% 11%); + color:hsl(0 0% 98%); + padding:8px 12px; + border-radius:6px; + font-size:12px; + font-weight:500; + line-height:1.4; + white-space:pre-line; + max-width:350px; + width:max-content; + word-wrap:break-word; + z-index:1000; + pointer-events:none; + box-shadow:0 8px 20px rgba(0,0,0,0.15), 0 2px 8px rgba(0,0,0,0.1); + opacity:0; + visibility:hidden; + transition:all 0.2s ease; + animation:tooltipFadeIn 0.2s ease forwards; + } + [title]:hover::before{ + content:''; + position:absolute; + bottom:100%; + left:50%; + transform:translateX(-50%) translateY(-4px); + border:6px solid transparent; + border-top-color:hsl(0 0% 11%); + z-index:1000; + opacity:0; + visibility:hidden; + transition:opacity 0.2s ease, visibility 0.2s ease; + } + [title]:hover::after, + [title]:hover::before{ + opacity:1; + visibility:visible; + } + @keyframes tooltipFadeIn{ + from{opacity:0;transform:translateX(-50%) translateY(-4px)} + to{opacity:1;transform:translateX(-50%) translateY(-8px)} + }