fix: proxy exception repair

This commit is contained in:
Chenyme committed 2025-10-14 10:07:01 +08:00
1 parent 050c6be5f6
commit 90a11df85d
7 files changed
+95 -103

No files matched your search

+12 -10
View File
@@ -252,7 +252,7 @@ async def admin_login(request: LoginRequest) -> LoginResponse:
@router.post("/api/logout")
async def admin_logout(authenticated: bool = Depends(verify_admin_session),
async def admin_logout(_: bool = Depends(verify_admin_session),
authorization: Optional[str] = Header(None)) -> Dict[str, Any]:
"""
管理员登出
@@ -279,7 +279,7 @@ async def admin_logout(authenticated: bool = Depends(verify_admin_session),
@router.get("/api/tokens", response_model=TokenListResponse)
async def list_tokens(authenticated: bool = Depends(verify_admin_session)) -> TokenListResponse:
async def list_tokens(_: bool = Depends(verify_admin_session)) -> TokenListResponse:
"""
获取所有Token列表
@@ -337,7 +337,7 @@ async def list_tokens(authenticated: bool = Depends(verify_admin_session)) -> To
@router.post("/api/tokens/add")
async def add_tokens(request: AddTokensRequest,
authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""
批量添加Token
@@ -372,7 +372,7 @@ async def add_tokens(request: AddTokensRequest,
@router.post("/api/tokens/delete")
async def delete_tokens(request: DeleteTokensRequest,
authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""
批量删除Token
@@ -406,7 +406,7 @@ async def delete_tokens(request: DeleteTokensRequest,
@router.get("/api/settings")
async def get_settings(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
async def get_settings(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""获取全局配置"""
try:
logger.debug("[Admin] 获取全局配置")
@@ -429,7 +429,7 @@ class UpdateSettingsRequest(BaseModel):
@router.post("/api/settings")
async def update_settings(request: UpdateSettingsRequest, authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
async def update_settings(request: UpdateSettingsRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""更新全局配置"""
try:
import toml
@@ -484,7 +484,7 @@ def _format_size(size_bytes: int) -> str:
@router.get("/api/cache/size")
async def get_cache_size(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
async def get_cache_size(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""获取缓存大小"""
try:
logger.debug("[Admin] 开始获取缓存大小")
@@ -525,8 +525,10 @@ async def get_cache_size(authenticated: bool = Depends(verify_admin_session)) ->
@router.post("/api/cache/clear")
async def clear_cache(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""清理缓存 - 删除所有临时文件"""
async def clear_cache(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""清理缓存
删除所有临时文件"""
try:
logger.debug("[Admin] 开始清理缓存")
@@ -578,7 +580,7 @@ async def clear_cache(authenticated: bool = Depends(verify_admin_session)) -> Di
@router.get("/api/stats")
async def get_stats(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
async def get_stats(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""
获取统计信息
+7 -7
View File
@@ -22,21 +22,21 @@ router = APIRouter(prefix="/chat", tags=["聊天"])
@router.post("/completions", response_model=None)
async def chat_completions(
request: OpenAIChatRequest,
authenticated: Optional[str] = Depends(auth_manager.verify)
_: Optional[str] = Depends(auth_manager.verify)
):
"""
创建聊天补全
兼容OpenAI聊天API的端点,支持流式和非流式响应。
Args:
request: OpenAI格式的聊天请求
authenticated: 认证状态(由依赖注入)
_: 认证依赖(自动验证)
Returns:
OpenAIChatCompletionResponse: 非流式响应
StreamingResponse: 流式响应
Raises:
HTTPException: 当请求处理失败时
"""
@@ -74,7 +74,7 @@ async def chat_completions(
}
)
except Exception as e:
logger.error(f"[Chat] 聊天请求处理失败: {str(e)}", exc_info=True)
logger.error(f"[Chat] 聊天请求处理失败: {str(e)}")
raise HTTPException(
status_code=500,
detail={
+9 -9
View File
@@ -19,15 +19,15 @@ router = APIRouter(tags=["模型"])
@router.get("/models")
async def list_models(authenticated: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
async def list_models(_: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
"""
获取可用模型列表
返回 OpenAI 兼容的模型列表格式,包含系统支持的所有 Grok 模型的详细信息。
Args:
authenticated: 认证状态(由依赖注入)
_: 认证依赖(自动验证)
Returns:
Dict[str, Any]: 包含模型列表的响应数据
"""
@@ -85,14 +85,14 @@ async def list_models(authenticated: Optional[str] = Depends(auth_manager.verify
@router.get("/models/{model_id}")
async def get_model(model_id: str, authenticated: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
async def get_model(model_id: str, _: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
"""
获取特定模型信息
Args:
model_id (str): 模型ID
authenticated: 认证状态(由依赖注入)
_: 认证依赖(自动验证)
Returns:
Dict[str, Any]: 模型详细信息
"""
+8 -8
View File
@@ -15,8 +15,8 @@ class OpenAIChatRequest(BaseModel):
max_tokens: Optional[int] = Field(None, ge=1, le=100000, description="最大Token数")
top_p: Optional[float] = Field(1.0, ge=0, le=1, description="采样参数")
@field_validator('messages')
@classmethod
@field_validator('messages')
def validate_messages(cls, v):
"""验证消息格式"""
if not v:
@@ -49,8 +49,8 @@ class OpenAIChatRequest(BaseModel):
return v
@field_validator('model')
@classmethod
@field_validator('model')
def validate_model(cls, v):
"""验证模型名称"""
allowed_models = [
@@ -68,16 +68,16 @@ class OpenAIChatCompletionMessage(BaseModel):
"""聊天完成消息"""
role: str = Field(..., description="角色")
content: str = Field(..., description="消息内容")
reference_id: Optional[str] = Field(None, description="参考ID")
annotations: Optional[List[str]] = Field(None, description="注释")
reference_id: Optional[str] = Field(default=None, description="参考ID")
annotations: Optional[List[str]] = Field(default=None, description="注释")
class OpenAIChatCompletionChoice(BaseModel):
"""聊天完成选项"""
index: int = Field(..., description="选项索引")
message: OpenAIChatCompletionMessage = Field(..., description="响应消息")
logprobs: Optional[float] = Field(None, description="对数概率")
finish_reason: str = Field("stop", description="完成原因")
logprobs: Optional[float] = Field(default=None, description="对数概率")
finish_reason: str = Field(default="stop", description="完成原因")
class OpenAIChatCompletionResponse(BaseModel):
@@ -108,8 +108,8 @@ class OpenAIChatCompletionChunkChoice(BaseModel):
class OpenAIChatCompletionChunkResponse(BaseModel):
"""流式聊天完成响应"""
id: str = Field(..., description="响应ID")
object: str = Field("chat.completion.chunk", description="对象类型")
object: str = Field(default="chat.completion.chunk", description="对象类型")
created: int = Field(..., description="创建时间戳")
model: str = Field(..., description="使用的模型")
system_fingerprint: Optional[str] = Field(None, description="系统指纹")
system_fingerprint: Optional[str] = Field(default=None, description="系统指纹")
choices: List[OpenAIChatCompletionChunkChoice] = Field(..., description="响应选项")
+7 -13
View File
@@ -14,12 +14,7 @@ class CacheService:
"""缓存服务基类"""
def __init__(self, cache_type: str):
"""
初始化缓存服务
Args:
cache_type: 缓存类型,'image' 或 'video'
"""
"""初始化缓存服务"""
self.cache_type = cache_type
self.cache_dir = Path(f"data/temp/{cache_type}")
self.cache_dir.mkdir(parents=True, exist_ok=True)
@@ -27,7 +22,6 @@ class CacheService:
@staticmethod
def _get_cache_filename(file_path: str) -> str:
"""将文件路径转换为缓存文件名"""
# 移除开头的斜杠并替换所有斜杠为短横线
filename = file_path.lstrip('/').replace('/', '-')
return filename
@@ -75,7 +69,7 @@ class CacheService:
# 代理配置
proxy_url = setting.grok_config.get("proxy_url")
proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else {}
async with AsyncSession() as session:
logger.debug(f"[{self.cache_type.upper()}Cache] 开始下载: {file_url}")
@@ -115,12 +109,12 @@ class CacheService:
async def cleanup_cache(self):
"""清理缓存目录,确保不超过配置的大小限制"""
try:
# 获取配置的最大缓存大小(MB)
# 获取配置的最大缓存大小
config_key = f"{self.cache_type}_cache_max_size_mb"
max_size_mb = setting.global_config.get(config_key, 500)
max_size_bytes = max_size_mb * 1024 * 1024
# 获取所有缓存文件及其大小和修改时间
# 获取缓存大小和修改时间
files = []
total_size = 0
@@ -138,10 +132,10 @@ class CacheService:
logger.info(f"[{self.cache_type.upper()}Cache] 缓存大小 {total_size / 1024 / 1024:.2f}MB 超过限制 {max_size_mb}MB,开始清理")
# 按修改时间排序(最旧的在前)
# 按修改时间排序
files.sort(key=lambda x: x[2])
# 删除最旧的文件直到总大小低于限制
# 删除文件直到总大小低于限制
for file_path, size, _ in files:
if total_size <= max_size_bytes:
break
@@ -170,7 +164,7 @@ class ImageCacheService(CacheService):
auth_token: 认证令牌
Returns:
缓存文件路径,下载失败返回 None
缓存文件路径,下载失败返回 None`
"""
return await self.download_file(image_path, auth_token, timeout=30.0)
+14 -9
View File
@@ -148,18 +148,23 @@ class GrokClient:
try:
# 构建请求头和代理
headers = GrokClient._build_headers(auth_token)
proxies = GrokClient._get_proxy()
proxy_config = GrokClient._get_proxy()
# 构建请求参数
request_kwargs = {
"headers": headers,
"data": json.dumps(payload),
"impersonate": IMPERSONATE_BROWSER,
"timeout": REQUEST_TIMEOUT,
"stream": True,
"proxies": proxy_config if proxy_config else None
}
# 在线程池中执行同步HTTP请求,避免阻塞事件循环
response = await asyncio.to_thread(
curl_requests.post,
GROK_API_ENDPOINT,
headers=headers,
data=json.dumps(payload),
impersonate=IMPERSONATE_BROWSER,
timeout=REQUEST_TIMEOUT,
stream=True,
**proxies
**request_kwargs
)
logger.debug(f"[Client] API响应状态码: {response.status_code}")
@@ -204,9 +209,9 @@ class GrokClient:
try:
error_data = response.json()
error_message = str(error_data)
except Exception:
except Exception as e:
error_data = response.text
error_message = error_data[:200] if error_data else "未知错误"
error_message = error_data[:200] if error_data else e
# 记录Token失败
asyncio.create_task(token_manager.record_failure(auth_token, response.status_code, error_message))
+38 -47
View File
@@ -3,7 +3,7 @@
import json
import uuid
import time
from typing import Iterator
from typing import AsyncGenerator
from app.core.config import setting
from app.core.exception import GrokApiException
@@ -43,7 +43,7 @@ class GrokResponseProcessor:
# 提取响应数据
grok_resp = data.get("result", {}).get("response", {})
# 检查视频生成响应
# 提取视频数据
if video_resp := grok_resp.get("streamingVideoGenerationResponse"):
if video_url := video_resp.get("videoUrl"):
logger.debug(f"[Processor] 检测到视频生成: {video_url}")
@@ -98,7 +98,7 @@ class GrokResponseProcessor:
model_name = model_response.get("model")
content = model_response.get("message", "")
# 添加生成的图片
# 提取图片数据
if images := model_response.get("generatedImageUrls"):
for img in images:
try:
@@ -114,7 +114,7 @@ class GrokResponseProcessor:
logger.warning(f"[Processor] 缓存图片失败: {e}")
content += f"\n![Generated Image](https://assets.grok.com/{img})"
# 返回OpenAI格式响应
# 返回 OpenAI 响应格式
result = OpenAIChatCompletionResponse(
id=f"chatcmpl-{uuid.uuid4()}",
object="chat.completion",
@@ -143,22 +143,19 @@ class GrokResponseProcessor:
response.close()
@staticmethod
async def process_stream(response, auth_token: str) -> Iterator[str]:
async def process_stream(response, auth_token: str) -> AsyncGenerator[str, None]:
"""处理流式响应"""
# 流式生成状态
is_image = False
is_thinking = False
thinking_finished = False
chunk_index = 0
model = None
filtered_tags = setting.grok_config.get("filtered_tags", "").split(",")
# 视频生成相关状态
is_video = False
video_url = None
filtered_tags = setting.grok_config.get("filtered_tags", "").split(",")
video_progress_started = False
last_video_progress = -1
def make_chunk(content: str, finish: str = None):
def make_chunk(chunk_content: str, finish: str = None):
"""生成OpenAI格式的响应块"""
chunk_data = OpenAIChatCompletionChunkResponse(
id=f"chatcmpl-{uuid.uuid4()}",
@@ -168,8 +165,8 @@ class GrokResponseProcessor:
index=chunk_index,
delta=OpenAIChatCompletionChunkMessage(
role="assistant",
content=content
) if content else {},
content=chunk_content
) if chunk_content else {},
finish_reason=finish
)]
).model_dump()
@@ -204,27 +201,40 @@ class GrokResponseProcessor:
if m := user_resp.get("model"):
model = m
# 检查视频生成响应
# 提取视频数据
if video_resp := grok_resp.get("streamingVideoGenerationResponse"):
is_video = True
progress = video_resp.get("progress", 0)
if progress > last_video_progress:
last_video_progress = progress
# 首次显示进度时添加 <think>
# 添加 <think> 标签
if not video_progress_started:
content = f"<think>视频已生成{progress}%\n"
video_progress_started = True
elif progress < 100:
content = f"视频已生成{progress}%\n"
else:
# 进度100%时关闭think标签
# 进度100%时关闭 <think> 标签并立即处理视频
content = f"视频已生成{progress}%</think>\n"
# 保存视频URL
# 立即下载并缓存视频
if v_url := video_resp.get("videoUrl"):
video_url = v_url
logger.debug(f"[Processor] 视频生成完成: {video_url}")
logger.debug(f"[Processor] 视频生成完成: {v_url}")
full_video_url = f"https://assets.grok.com/{v_url}"
try:
cache_path = await video_cache_service.download_video(f"/{v_url}", auth_token)
if cache_path:
video_path = v_url.replace('/', '-')
base_url = setting.global_config.get("base_url", "")
local_video_url = f"{base_url}/images/{video_path}" if base_url else f"/images/{video_path}"
content += f'<video src="{local_video_url}" controls="controls"></video>'
else:
content += f'<video src="{full_video_url}" controls="controls"></video>'
except Exception as e:
logger.warning(f"[Processor] 缓存视频失败: {e}")
content += f'<video src="{full_video_url}" controls="controls"></video>'
yield make_chunk(content)
chunk_index += 1
@@ -238,16 +248,16 @@ class GrokResponseProcessor:
# 获取token
token = grok_resp.get("token", "")
# 图片模式
# 提取图片数据
if is_image:
if model_resp := grok_resp.get("modelResponse"):
# 生成图片链接并缓存
content = ""
for img in model_resp.get("generatedImageUrls", []):
try:
# 异步下载并缓存图片
# 缓存图片
await image_cache_service.download_image(f"/{img}", auth_token)
# 使用本地缓存路径
# 本地图片路径
img_path = img.replace('/', '-')
base_url = setting.global_config.get("base_url", "")
img_url = f"{base_url}/images/{img_path}" if base_url else f"/images/{img_path}"
@@ -261,7 +271,7 @@ class GrokResponseProcessor:
yield make_chunk(token)
chunk_index += 1
# 对话模式
# 提取对话数据
else:
# 过滤 list 格式的 token
if isinstance(token, list):
@@ -275,7 +285,7 @@ class GrokResponseProcessor:
current_is_thinking = grok_resp.get("isThinking", False)
message_tag = grok_resp.get("messageTag")
# 跳过后续的 thinking
# 跳过后续的 <think> 标签
if thinking_finished and current_is_thinking:
continue
@@ -283,7 +293,7 @@ class GrokResponseProcessor:
if grok_resp.get("toolUsageCardId"):
if web_search := grok_resp.get("webSearchResults"):
if current_is_thinking:
# 添加搜索结果到 token
# 封装搜索结果
for result in web_search.get("results", []):
title = result.get("title", "")
url = result.get("url", "")
@@ -323,27 +333,8 @@ class GrokResponseProcessor:
logger.warning(f"[Processor] 处理chunk出错: {e}")
continue
# 处理视频生成完成后的输出
if is_video and video_url:
full_video_url = f"https://assets.grok.com/{video_url}"
try:
# 下载并缓存视频
cache_path = await video_cache_service.download_video(f"/{video_url}", auth_token)
if cache_path:
video_path = video_url.replace('/', '-')
base_url = setting.global_config.get("base_url", "")
local_video_url = f"{base_url}/images/{video_path}" if base_url else f"/images/{video_path}"
content = f'<video src="{local_video_url}" controls="controls" width="500" height="300"></video>'
else:
content = f'<video src="{full_video_url}" controls="controls" width="500" height="300"></video>'
except Exception as e:
logger.warning(f"[Processor] 缓存视频失败: {e}")
content = f'<video src="{full_video_url}" controls="controls" width="500" height="300"></video>'
yield make_chunk(content, "stop")
else:
# 发送结束块
yield make_chunk("", "stop")
# 发送结束块
yield make_chunk("", "stop")
# 发送流结束标记
yield "data: [DONE]\n\n"