From 90a11df85df05ef9e1b2b22f4e83562c427dbc72 Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:07:01 +0800 Subject: [PATCH 1/4] fix: proxy exception repair --- app/api/admin/manage.py | 22 +++++---- app/api/v1/chat.py | 14 +++--- app/api/v1/models.py | 18 +++---- app/models/openai_schema.py | 16 +++---- app/services/grok/cache.py | 20 +++----- app/services/grok/client.py | 23 +++++---- app/services/grok/processer.py | 85 +++++++++++++++------------------- 7 files changed, 95 insertions(+), 103 deletions(-) diff --git a/app/api/admin/manage.py b/app/api/admin/manage.py index 87d02ed..a6876c2 100644 --- a/app/api/admin/manage.py +++ b/app/api/admin/manage.py @@ -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]: """ 获取统计信息 diff --git a/app/api/v1/chat.py b/app/api/v1/chat.py index 7c2c61d..9e612a8 100644 --- a/app/api/v1/chat.py +++ b/app/api/v1/chat.py @@ -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={ diff --git a/app/api/v1/models.py b/app/api/v1/models.py index f5f1a5a..5c18513 100644 --- a/app/api/v1/models.py +++ b/app/api/v1/models.py @@ -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]: 模型详细信息 """ diff --git a/app/models/openai_schema.py b/app/models/openai_schema.py index 9012d87..37b6eeb 100644 --- a/app/models/openai_schema.py +++ b/app/models/openai_schema.py @@ -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="响应选项") \ No newline at end of file diff --git a/app/services/grok/cache.py b/app/services/grok/cache.py index 28408c8..5dcbfb1 100644 --- a/app/services/grok/cache.py +++ b/app/services/grok/cache.py @@ -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) diff --git a/app/services/grok/client.py b/app/services/grok/client.py index 3fec279..9d4d935 100644 --- a/app/services/grok/client.py +++ b/app/services/grok/client.py @@ -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)) diff --git a/app/services/grok/processer.py b/app/services/grok/processer.py index 525af81..7d66084 100644 --- a/app/services/grok/processer.py +++ b/app/services/grok/processer.py @@ -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 - # 首次显示进度时添加 + # 添加 标签 if not video_progress_started: content = f"视频已生成{progress}%\n" video_progress_started = True elif progress < 100: content = f"视频已生成{progress}%\n" else: - # 进度100%时关闭think标签 + # 进度100%时关闭 标签并立即处理视频 content = f"视频已生成{progress}%\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'' + else: + content += f'' + except Exception as e: + logger.warning(f"[Processor] 缓存视频失败: {e}") + content += f'' 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 + # 跳过后续的 标签 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'' - else: - content = f'' - except Exception as e: - logger.warning(f"[Processor] 缓存视频失败: {e}") - content = f'' - - yield make_chunk(content, "stop") - else: - # 发送结束块 - yield make_chunk("", "stop") + # 发送结束块 + yield make_chunk("", "stop") # 发送流结束标记 yield "data: [DONE]\n\n" From fe0103ab642e02b2f962fdc07a350dc37d706e0b Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:44:41 +0800 Subject: [PATCH 2/4] feat: add timeout mechanism --- app/api/admin/manage.py | 93 +++++++++++++++++++++++++++- app/services/grok/client.py | 5 ++ app/services/grok/processer.py | 110 ++++++++++++++++++++++++++++++--- app/template/admin.html | 42 ++++++++++++- data/setting.toml | 7 ++- 5 files changed, 242 insertions(+), 15 deletions(-) diff --git a/app/api/admin/manage.py b/app/api/admin/manage.py index a6876c2..335878f 100644 --- a/app/api/admin/manage.py +++ b/app/api/admin/manage.py @@ -428,6 +428,13 @@ class UpdateSettingsRequest(BaseModel): grok_config: Optional[Dict[str, Any]] = None +class StreamTimeoutSettings(BaseModel): + """流式超时配置""" + stream_chunk_timeout: int = 120 + stream_first_response_timeout: int = 30 + stream_total_timeout: int = 600 + + @router.post("/api/settings") async def update_settings(request: UpdateSettingsRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]: """更新全局配置""" @@ -526,8 +533,8 @@ async def get_cache_size(_: bool = Depends(verify_admin_session)) -> Dict[str, A @router.post("/api/cache/clear") async def clear_cache(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: - """清理缓存 - + """清理缓存 + 删除所有临时文件""" try: logger.debug("[Admin] 开始清理缓存") @@ -560,7 +567,7 @@ async def clear_cache(_: bool = Depends(verify_admin_session)) -> Dict[str, Any] deleted_count = image_count + video_count logger.debug(f"[Admin] 缓存清理完成 - 图片: {image_count}, 视频: {video_count}, 总计: {deleted_count}") - + return { "success": True, "message": f"成功清理缓存,删除图片 {image_count} 个,视频 {video_count} 个,共 {deleted_count} 个文件", @@ -579,6 +586,86 @@ async def clear_cache(_: bool = Depends(verify_admin_session)) -> Dict[str, Any] ) +@router.post("/api/cache/clear/images") +async def clear_image_cache(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: + """清理图片缓存 + + 仅删除图片缓存文件""" + try: + logger.debug("[Admin] 开始清理图片缓存") + + deleted_count = 0 + + # 清理图片缓存 + if IMAGE_CACHE_DIR.exists(): + for file_path in IMAGE_CACHE_DIR.iterdir(): + if file_path.is_file(): + try: + file_path.unlink() + deleted_count += 1 + logger.debug(f"[Admin] 删除图片缓存: {file_path.name}") + except Exception as e: + logger.error(f"[Admin] 删除图片缓存失败: {file_path.name}, 错误: {str(e)}") + + logger.debug(f"[Admin] 图片缓存清理完成 - 删除 {deleted_count} 个文件") + + return { + "success": True, + "message": f"成功清理图片缓存,删除 {deleted_count} 个文件", + "data": { + "deleted_count": deleted_count, + "type": "images" + } + } + + except Exception as e: + logger.error(f"[Admin] 清理图片缓存异常 - 错误: {str(e)}") + raise HTTPException( + status_code=500, + detail={"error": f"清理图片缓存失败: {str(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] 开始清理视频缓存") + + deleted_count = 0 + + # 清理视频缓存 + if VIDEO_CACHE_DIR.exists(): + for file_path in VIDEO_CACHE_DIR.iterdir(): + if file_path.is_file(): + try: + file_path.unlink() + deleted_count += 1 + logger.debug(f"[Admin] 删除视频缓存: {file_path.name}") + except Exception as e: + logger.error(f"[Admin] 删除视频缓存失败: {file_path.name}, 错误: {str(e)}") + + logger.debug(f"[Admin] 视频缓存清理完成 - 删除 {deleted_count} 个文件") + + return { + "success": True, + "message": f"成功清理视频缓存,删除 {deleted_count} 个文件", + "data": { + "deleted_count": deleted_count, + "type": "videos" + } + } + + except Exception as e: + logger.error(f"[Admin] 清理视频缓存异常 - 错误: {str(e)}") + raise HTTPException( + status_code=500, + detail={"error": f"清理视频缓存失败: {str(e)}", "code": "VIDEO_CACHE_CLEAR_ERROR"} + ) + + @router.get("/api/stats") async def get_stats(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]: """ diff --git a/app/services/grok/client.py b/app/services/grok/client.py index 9d4d935..b275308 100644 --- a/app/services/grok/client.py +++ b/app/services/grok/client.py @@ -180,9 +180,14 @@ class GrokClient: return await GrokClient._process_response(response, auth_token, model, stream) except curl_requests.RequestsError as e: + logger.error(f"[Client] 网络请求错误: {e}") raise GrokApiException(f"网络错误: {e}", "NETWORK_ERROR") from e except json.JSONDecodeError as e: + logger.error(f"[Client] JSON解析错误: {e}") raise GrokApiException(f"JSON解析错误: {e}", "JSON_ERROR") from e + except Exception as e: + logger.error(f"[Client] 未知请求错误: {type(e).__name__}: {e}") + raise GrokApiException(f"请求处理错误: {e}", "REQUEST_ERROR") from e @staticmethod def _build_headers(auth_token: str) -> Dict[str, str]: diff --git a/app/services/grok/processer.py b/app/services/grok/processer.py index 7d66084..171cc14 100644 --- a/app/services/grok/processer.py +++ b/app/services/grok/processer.py @@ -3,6 +3,7 @@ import json import uuid import time +import asyncio from typing import AsyncGenerator from app.core.config import setting @@ -19,12 +20,67 @@ from app.models.openai_schema import ( from app.services.grok.cache import image_cache_service, video_cache_service +class StreamTimeoutManager: + """流式响应超时管理器""" + + def __init__(self, chunk_timeout: int = 120, first_response_timeout: int = 30, total_timeout: int = 600): + """初始化超时管理器 + + Args: + chunk_timeout: 数据块间隔超时(秒) + first_response_timeout: 首次响应超时(秒) + total_timeout: 总超时限制(秒,0表示不限制) + """ + self.chunk_timeout = chunk_timeout + self.first_response_timeout = first_response_timeout + self.total_timeout = total_timeout + + self.start_time = asyncio.get_event_loop().time() + self.last_chunk_time = self.start_time + self.first_chunk_received = False + + def check_timeout(self) -> tuple[bool, str]: + """检查是否超时 + + Returns: + (is_timeout, timeout_message): 是否超时及超时信息 + """ + current_time = asyncio.get_event_loop().time() + + # 检查首次响应超时 + if not self.first_chunk_received: + if current_time - self.start_time > self.first_response_timeout: + return True, f"首次响应超时 ({self.first_response_timeout}秒未收到首个数据块)" + + # 检查总超时 + if self.total_timeout > 0: + if current_time - self.start_time > self.total_timeout: + return True, f"流式响应总超时 ({self.total_timeout}秒)" + + # 检查数据块间隔超时 + if self.first_chunk_received: + if current_time - self.last_chunk_time > self.chunk_timeout: + return True, f"数据块间隔超时 ({self.chunk_timeout}秒无新数据)" + + return False, "" + + def mark_chunk_received(self): + """标记收到数据块""" + self.last_chunk_time = asyncio.get_event_loop().time() + self.first_chunk_received = True + + def get_total_duration(self) -> float: + """获取总耗时(秒)""" + return asyncio.get_event_loop().time() - self.start_time + + class GrokResponseProcessor: """Grok API 响应处理器""" @staticmethod async def process_normal(response, auth_token: str, model: str = None) -> OpenAIChatCompletionResponse: """处理非流式响应""" + response_closed = False try: for chunk in response.iter_lines(): if not chunk: @@ -79,6 +135,7 @@ class GrokResponseProcessor: )], usage=None ) + response_closed = True response.close() return result @@ -130,17 +187,25 @@ class GrokResponseProcessor: )], usage=None ) + response_closed = True response.close() return result raise GrokApiException("无响应数据", "NO_RESPONSE") except json.JSONDecodeError as e: + logger.error(f"[Processor] JSON解析失败: {e}") raise GrokApiException(f"JSON解析失败: {e}", "JSON_ERROR") from e + except Exception as e: + logger.error(f"[Processor] 处理响应时发生未知错误: {type(e).__name__}: {e}") + raise GrokApiException(f"响应处理错误: {e}", "PROCESS_ERROR") from e finally: - # 确保响应对象被关闭 - if hasattr(response, 'close'): - response.close() + # 确保响应对象被关闭,避免双重释放 + if not response_closed and hasattr(response, 'close'): + try: + response.close() + except Exception as e: + logger.warning(f"[Processor] 关闭响应对象时出错: {e}") @staticmethod async def process_stream(response, auth_token: str) -> AsyncGenerator[str, None]: @@ -151,9 +216,17 @@ class GrokResponseProcessor: thinking_finished = False chunk_index = 0 model = None - filtered_tags = setting.grok_config.get("filtered_tags", "").split(",") + filtered_tags = setting.grok_config.get("filtered_tags", "").split(",") video_progress_started = False last_video_progress = -1 + response_closed = False + + # 初始化超时管理器 + timeout_manager = StreamTimeoutManager( + chunk_timeout=setting.grok_config.get("stream_chunk_timeout", 120), + first_response_timeout=setting.grok_config.get("stream_first_response_timeout", 30), + total_timeout=setting.grok_config.get("stream_total_timeout", 600) + ) def make_chunk(chunk_content: str, finish: str = None): """生成OpenAI格式的响应块""" @@ -175,6 +248,14 @@ class GrokResponseProcessor: try: for chunk in response.iter_lines(): + # 超时检查 + is_timeout, timeout_msg = timeout_manager.check_timeout() + if is_timeout: + logger.warning(f"[Processor] {timeout_msg}") + yield make_chunk("", "stop") + yield "data: [DONE]\n\n" + return + logger.debug(f"[Processor] 接收到数据块: {len(chunk)} bytes") if not chunk: continue @@ -235,8 +316,9 @@ class GrokResponseProcessor: except Exception as e: logger.warning(f"[Processor] 缓存视频失败: {e}") content += f'' - + yield make_chunk(content) + timeout_manager.mark_chunk_received() chunk_index += 1 continue @@ -266,9 +348,11 @@ class GrokResponseProcessor: logger.warning(f"[Processor] 缓存图片失败: {e}") content += f"![Generated Image](https://assets.grok.com/{img})\n" yield make_chunk(content.strip(), "stop") + timeout_manager.mark_chunk_received() return elif token: yield make_chunk(token) + timeout_manager.mark_chunk_received() chunk_index += 1 # 提取对话数据 @@ -323,6 +407,7 @@ class GrokResponseProcessor: thinking_finished = True yield make_chunk(content) + timeout_manager.mark_chunk_received() chunk_index += 1 is_thinking = current_is_thinking @@ -335,12 +420,23 @@ class GrokResponseProcessor: # 发送结束块 yield make_chunk("", "stop") - + # 发送流结束标记 yield "data: [DONE]\n\n" + + # 记录流式响应统计 + logger.info(f"[Processor] 流式响应完成,总耗时: {timeout_manager.get_total_duration():.2f}秒") except Exception as e: logger.error(f"[Processor] 流式处理严重错误: {e}") yield make_chunk(f"处理错误: {e}", "error") # 发送流结束标记 - yield "data: [DONE]\n\n" \ No newline at end of file + yield "data: [DONE]\n\n" + finally: + # 确保响应对象被关闭 + if not response_closed and hasattr(response, 'close'): + try: + response.close() + logger.debug("[Processor] 流式响应对象已关闭") + except Exception as e: + logger.warning(f"[Processor] 关闭流式响应对象时出错: {e}") \ No newline at end of file diff --git a/app/template/admin.html b/app/template/admin.html index f83ea1a..9bdd8d0 100644 --- a/app/template/admin.html +++ b/app/template/admin.html @@ -325,15 +325,28 @@
图片: +
视频: +
总计: -
+
+ + +
+
+ + +
+
+ + +
@@ -500,11 +534,13 @@ showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity 0.3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)}, logout=async()=>{if(!confirm('确定要退出登录吗?'))return;try{await apiRequest('/api/logout',{method:'POST'})}catch(e){console.error('登出失败:',e)}finally{localStorage.removeItem('adminToken');location.href='/login'}}, switchTab=t=>{['tokens','settings'].forEach(n=>{$(`panel${n.charAt(0).toUpperCase()+n.slice(1)}`).classList[n===t?'remove':'add']('hidden');$(`tab${n.charAt(0).toUpperCase()+n.slice(1)}`).classList[n===t?'add':'remove']('border-primary','text-primary');$(`tab${n.charAt(0).toUpperCase()+n.slice(1)}`).classList[n===t?'remove':'add']('border-transparent','text-muted-foreground')});t==='settings'&&loadSettings()}, - loadSettings=async()=>{try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(d.success){const g=d.data.global,k=d.data.grok;$('cfgAdminUser').value=g.admin_username||'';$('cfgAdminPass').value='';$('cfgLogLevel').value=g.log_level||'DEBUG';$('cfgImageCacheMaxSize').value=g.image_cache_max_size_mb||500;$('cfgVideoCacheMaxSize').value=g.video_cache_max_size_mb||1000;$('cfgBaseUrl').value=g.base_url||'';$('cfgApiKey').value=k.api_key||'';$('cfgProxyUrl').value=k.proxy_url||'';$('cfgCfClearance').value=k.cf_clearance||'';$('cfgStatsigId').value=k.x_statsig_id||'';$('cfgFilteredTags').value=k.filtered_tags||'';$('cfgTemporary').value=k.temporary!==false?'true':'false';await loadCacheSize()}}catch(e){console.error('加载配置失败:',e);showToast('加载配置失败','error')}}, + loadSettings=async()=>{try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(d.success){const g=d.data.global,k=d.data.grok;$('cfgAdminUser').value=g.admin_username||'';$('cfgAdminPass').value='';$('cfgLogLevel').value=g.log_level||'DEBUG';$('cfgImageCacheMaxSize').value=g.image_cache_max_size_mb||500;$('cfgVideoCacheMaxSize').value=g.video_cache_max_size_mb||1000;$('cfgBaseUrl').value=g.base_url||'';$('cfgApiKey').value=k.api_key||'';$('cfgProxyUrl').value=k.proxy_url||'';$('cfgCfClearance').value=k.cf_clearance||'';$('cfgStatsigId').value=k.x_statsig_id||'';$('cfgFilteredTags').value=k.filtered_tags||'';$('cfgTemporary').value=k.temporary!==false?'true':'false';$('cfgStreamChunkTimeout').value=k.stream_chunk_timeout||120;$('cfgStreamFirstResponseTimeout').value=k.stream_first_response_timeout||30;$('cfgStreamTotalTimeout').value=k.stream_total_timeout||600;await loadCacheSize()}}catch(e){console.error('加载配置失败:',e);showToast('加载配置失败','error')}}, loadCacheSize=async()=>{try{const r=await apiRequest('/api/cache/size');if(!r)return;const d=await r.json();if(d.success){$('imageCacheSize').value=d.data.image_size||'0 MB';$('videoCacheSize').value=d.data.video_size||'0 MB';$('totalCacheSize').value=d.data.total_size||'0 MB'}}catch(e){console.error('加载缓存大小失败:',e);$('imageCacheSize').value='0 MB';$('videoCacheSize').value='0 MB';$('totalCacheSize').value='0 MB'}}, + clearImageCache=async()=>{if(!confirm('确定要清理图片缓存吗?此操作将删除所有图片缓存文件!'))return;try{const r=await apiRequest('/api/cache/clear/images',{method:'POST'});if(!r)return;const d=await r.json();if(d.success){showToast(`图片缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success');await loadCacheSize()}else{showToast('清理失败: '+(d.error||'未知错误'),'error')}}catch(e){showToast('清理失败: '+e.message,'error')}}, + clearVideoCache=async()=>{if(!confirm('确定要清理视频缓存吗?此操作将删除所有视频缓存文件!'))return;try{const r=await apiRequest('/api/cache/clear/videos',{method:'POST'});if(!r)return;const d=await r.json();if(d.success){showToast(`视频缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success');await loadCacheSize()}else{showToast('清理失败: '+(d.error||'未知错误'),'error')}}catch(e){showToast('清理失败: '+e.message,'error')}}, clearCache=async()=>{if(!confirm('确定要清理缓存吗?此操作将删除 /data/temp 目录中的所有文件!'))return;try{const r=await apiRequest('/api/cache/clear',{method:'POST'});if(!r)return;const d=await r.json();if(d.success){showToast(`缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success');await loadCacheSize()}else{showToast('清理失败: '+(d.error||'未知错误'),'error')}}catch(e){showToast('清理失败: '+e.message,'error')}}, saveGlobalSettings=async()=>{const gc={admin_username:$('cfgAdminUser').value,log_level:$('cfgLogLevel').value,image_cache_max_size_mb:parseInt($('cfgImageCacheMaxSize').value)||500,video_cache_max_size_mb:parseInt($('cfgVideoCacheMaxSize').value)||1000,base_url:$('cfgBaseUrl').value};if($('cfgAdminPass').value)gc.admin_password=$('cfgAdminPass').value;try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(!d.success)return showToast('加载配置失败','error');const s=await apiRequest('/api/settings',{method:'POST',body:JSON.stringify({global_config:gc,grok_config:d.data.grok})});if(!s)return;const sd=await s.json();sd.success?(showToast('全局配置保存成功','success'),$('cfgAdminPass').value=''):showToast('保存失败: '+(sd.error||'未知错误'),'error')}catch(e){showToast('保存失败: '+e.message,'error')}}, - saveGrokSettings=async()=>{const kc={api_key:$('cfgApiKey').value,proxy_url:$('cfgProxyUrl').value,cf_clearance:$('cfgCfClearance').value,x_statsig_id:$('cfgStatsigId').value,filtered_tags:$('cfgFilteredTags').value,temporary:$('cfgTemporary').value==='true'};try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(!d.success)return showToast('加载配置失败','error');const s=await apiRequest('/api/settings',{method:'POST',body:JSON.stringify({global_config:d.data.global,grok_config:kc})});if(!s)return;const sd=await s.json();sd.success?showToast('Grok配置保存成功','success'):showToast('保存失败: '+(sd.error||'未知错误'),'error')}catch(e){showToast('保存失败: '+e.message,'error')}}; + saveGrokSettings=async()=>{const kc={api_key:$('cfgApiKey').value,proxy_url:$('cfgProxyUrl').value,cf_clearance:$('cfgCfClearance').value,x_statsig_id:$('cfgStatsigId').value,filtered_tags:$('cfgFilteredTags').value,temporary:$('cfgTemporary').value==='true',stream_chunk_timeout:parseInt($('cfgStreamChunkTimeout').value)||120,stream_first_response_timeout:parseInt($('cfgStreamFirstResponseTimeout').value)||30,stream_total_timeout:parseInt($('cfgStreamTotalTimeout').value)||600};try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(!d.success)return showToast('加载配置失败','error');const s=await apiRequest('/api/settings',{method:'POST',body:JSON.stringify({global_config:d.data.global,grok_config:kc})});if(!s)return;const sd=await s.json();sd.success?showToast('Grok配置保存成功','success'):showToast('保存失败: '+(sd.error||'未知错误'),'error')}catch(e){showToast('保存失败: '+e.message,'error')}}; window.addEventListener('DOMContentLoaded',()=>{checkAuth();refreshTokens();setInterval(()=>{loadStats();updateRemaining()},30000)}); diff --git a/data/setting.toml b/data/setting.toml index 379d11e..a384137 100644 --- a/data/setting.toml +++ b/data/setting.toml @@ -2,15 +2,18 @@ admin_username = "admin" admin_password = "admin" log_level = "INFO" -# 图片缓存最大大小(MB) image_cache_max_size_mb = 500 -# 视频缓存最大大小(MB) video_cache_max_size_mb = 1000 base_url = "" + + [grok] api_key = "" proxy_url = "" +stream_chunk_timeout = 120 +stream_first_response_timeout = 30 +stream_total_timeout = 600 cf_clearance = "" temporary = true filtered_tags = "xaiartifact,xai:tool_usage_card,grok:render" From cb6e4656d55582245a9a187fd4398c6d504e4a24 Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:56:52 +0800 Subject: [PATCH 3/4] fix: logger error --- app/api/v1/chat.py | 2 +- app/core/exception.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/v1/chat.py b/app/api/v1/chat.py index 9e612a8..87460e5 100644 --- a/app/api/v1/chat.py +++ b/app/api/v1/chat.py @@ -62,7 +62,7 @@ async def chat_completions( return result except GrokApiException as e: - logger.error(f"[Chat] Grok API错误: {str(e)}", extra={"details": e.details}) + logger.error(f"[Chat] Grok API错误: {str(e)} - 详情: {e.details}") raise HTTPException( status_code=500, detail={ diff --git a/app/core/exception.py b/app/core/exception.py index 52ac613..a5c179b 100644 --- a/app/core/exception.py +++ b/app/core/exception.py @@ -101,7 +101,7 @@ async def grok_api_exception_handler(_: Request, exc: GrokApiException) -> JSONR ) -async def global_exception_handler(_: Request) -> JSONResponse: +async def global_exception_handler(_: Request, exc: Exception) -> JSONResponse: """处理未捕获异常""" return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, From 6c98cff2582b08a5bab46a64012f639f084a56ed Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:42:07 +0800 Subject: [PATCH 4/4] docs: update readme.md --- readme.md | 193 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 118 insertions(+), 75 deletions(-) diff --git a/readme.md b/readme.md index 4c8526c..7e3fcc3 100644 --- a/readme.md +++ b/readme.md @@ -1,63 +1,68 @@ # Grok2API -基于 FastAPI 重构的 Grok2API,适配最新的Web调用格式,支持流式对话、图像生成、图像编辑、联网搜索、深度思考,支持号池并发调用和自动负载均衡。 +基于 **FastAPI** 重构的 Grok2API,全面适配最新 Web 调用格式,支持流式对话、图像生成、图像编辑、联网搜索、深度思考,号池并发与自动负载均衡一体化。 -### 接口说明 -> 同 OpenAI 官方接口一致,API 请求使用通过 Authorization header 进行认证 - -| 方法 | 端点 | 描述 | 认证 | -|------|------|------|------| -| POST | `/v1/chat/completions` | 创建聊天对话,支持流式和非流式响应,兼容OpenAI格式 | ✅ | -| GET | `/v1/models` | 获取支持的所有模型列表 | ✅ | -| GET | `/v1/models/{model_id}` | 获取特定模型信息 | ✅ | -| GET | `/images/{img_path}` | 获取生成的图片文件 | ❌ | - - -
-其余管理接口 - -| 方法 | 端点 | 描述 | 认证 | -|------|------|------|------| -| GET | /login | 管理员登录页面 | ❌ | -| GET | /manage | 管理控制台页面 | ❌ | -| POST | /api/login | 管理员登录认证 | ❌ | -| POST | /api/logout | 管理员登出 | ✅ | -| GET | /api/tokens | 获取Token列表 | ✅ | -| POST | /api/tokens/add | 批量添加Token | ✅ | -| POST | /api/tokens/delete | 批量删除Token | ✅ | -| GET | /api/settings | 获取系统配置 | ✅ | -| POST | /api/settings | 更新系统配置 | ✅ | -| GET | /api/cache/size | 获取缓存大小 | ✅ | -| POST | /api/cache/clear | 清理图片缓存 | ✅ | -| GET | /api/stats | 获取统计信息 | ✅ | - -
-
-### 使用说明 +## 使用说明 -#### 调用次数说明 +### 调用次数与配额 -- 普通账号(Basic)可免费使用 **80 次/20 小时**,Super 账号配额尚未确定(我没有号就没测)。 -- 系统自动监控并负载均衡各账号调用次数,可在**管理页面**直观查看各账户实时用量和状态。 +- **普通账号(Basic)**:免费使用 **80 次 / 20 小时** +- **Super 账号**:配额待定(作者未测) +- 系统自动负载均衡各账号调用次数,可在**管理页面**实时查看用量与状态 -#### 图像生成说明 +### 图像生成功能 -- 只需在对话内容中输入如“给我画一个月亮”即可自动触发生成图片。 -- 每次生成会以 **Markdown 格式返回两张图片**,计 4 次额度。 -- **由于 Grok 的图片直链受限(403),系统会自动将图片缓存到本地。要确保图片能正常显示,请务必正确设置 Base Url。** +- 在对话内容中输入如“给我画一个月亮”自动触发图片生成 +- 每次以 **Markdown 格式返回两张图片**,共消耗 4 次额度 +- **注意:Grok 的图片直链受 403 限制,系统自动缓存图片到本地。必须正确设置 `Base Url` 以确保图片能正常显示!** -#### 关于 x_statsig_id - -- `x_statsig_id` 是 Grok 用于识别机器人请求的 Token,目前网络上已经有相关的逆向分析资料,有兴趣可自行查阅。 -- 如果您不确定如何获取 x_statsig_id,建议不要修改配置文件中的该参数,保持默认值即可,无需关注。 -- 项目最初借鉴了 [VeroFess/grok2api](https://github.com/VeroFess/grok2api) 的实现,曾通过 Camoufox 尝试绕过原项目的 Playwright 获取 x_statsig_id 时触发的 403。起初尚可使用,但近期 Grok 已对未登录用户的 x_statsig_id 做了限制,因此已弃用改方法,当前直接使用固定值以兼容调用。 - -#### docker-compose 部署 +### 视频生成功能 +- 选择 `grok-imagine-0.9` 模型,传入图片和提示词即可(方式和 OpenAI 的图片分析调用格式一致) +- 返回格式为 `` +- **注意:Grok 的视频直链受 403 限制,系统自动缓存图片到本地。必须正确设置 `Base Url` 以确保视频能正常显示!** ``` +curl https://你的服务器地址/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $GROK2API_API_KEY" \ + -d '{ + "model": "grok-imagine-0.9", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "让太阳升起来" + }, + { + "type": "image_url", + "image_url": { + "url": "https://your-image.jpg" + } + } + ] + } + ] + }' +``` + +### 关于 `x_statsig_id` + +- `x_statsig_id` 是 Grok 用于反机器人的 Token,有逆向资料可参考 +- **建议新手勿修改配置,保留默认值即可** +- 尝试用 Camoufox 绕过 403 自动获 id,但 grok 现已限制非登陆的`x_statsig_id`,故弃用,采用固定值以兼容所有请求 + +
+ +## 如何部署 + +### docker-compose + +```yaml services: grok2api: image: ghcr.io/chenyme/grok2api:latest @@ -73,45 +78,83 @@ volumes:
-### 可用模型 -> 所有模型均可进行图像生成,每次计次为 4 次 +## 接口说明 -| 模型名称 | 调用计次 | 可用账户 | 图像生成/编辑 | 深度思考 | 联网搜索 | -|----------------------|---------|--------------|--------------|----------|----------| -| `grok-3-fast` | 1 | Basic/Super | ✅ | ❌ | ✅ | -| `grok-4-fast` | 1 | Basic/Super | ✅ | ✅ | ✅ | -| `grok-4-fast-expert` | 4 | Basic/Super | ✅ | ✅ | ✅ | -| `grok-4-expert` | 4 | Basic/Super | ✅ | ✅ | ✅ | -| `grok-4-heavy` | 1 | Super | ✅ | ✅ | ✅ | +> 与 OpenAI 官方接口完全兼容,API 请求需通过 **Authorization header** 认证 + +| 方法 | 端点 | 描述 | 是否需要认证 | +|-------|------------------------------|------------------------------------|------| +| POST | `/v1/chat/completions` | 创建聊天对话(流式/非流式) | ✅ | +| GET | `/v1/models` | 获取全部支持模型 | ✅ | +| GET | `/images/{img_path}` | 获取生成图片文件 | ❌ |
-### 参数说明 -> 所有参数请在启动服务后,进入 `/login` 路由后登陆管理进行配置 +
+管理与统计接口(展开查看更多) -| 参数名 | 位置 | 必填 | 说明 | 默认值 | -|---------|---------|------|------|--------| -| admin_username | global | 否 | 管理后台登录用户名 | "admin" | -| admin_password | global | 否 | 管理后台登录密码 | "admin" | -| log_level | global | 否 | 日志级别:DEBUG/INFO/WARNING/ERROR | "INFO" | -| temp_max_size_mb | global | 否 | 图片缓存目录最大容量(MB),超过后自动清理 | 500 | -| base_url | global | 否 | 服务基础URL,用于生成图片链接,保护服务器IP | "" | -| api_key | grok | 否 | API访问密钥,提高安全性,可留空 | "" | -| proxy_url | grok | 否 | HTTP代理服务器地址,访问Grok服务 | "" | -| cf_clearance | grok | 否 | Cloudflare安全令牌,绕过人机验证 | "" | -| x_statsig_id | grok | 是 | Grok反机器人检测唯一标识符 | "ZTpUeXBlRXJyb3I6IENhbm5vdCByZWFkIHByb3BlcnRpZXMgb2YgdW5kZWZpbmVkIChyZWFkaW5nICdjaGlsZE5vZGVzJyk=" | -| filtered_tags | grok | 否 | 过滤响应标签,多个用逗号分隔 | "xaiartifact,xai:tool_usage_card,grok:render" | -| temporary | grok | 否 | 会话模式:true(临时)/false | true | +| 方法 | 端点 | 描述 | 认证 | +|-------|-------------------------|--------------------|------| +| GET | /login | 管理员登录页面 | ❌ | +| GET | /manage | 管理控制台页面 | ❌ | +| POST | /api/login | 管理员登录认证 | ❌ | +| POST | /api/logout | 管理员登出 | ✅ | +| GET | /api/tokens | 获取 Token 列表 | ✅ | +| POST | /api/tokens/add | 批量添加 Token | ✅ | +| POST | /api/tokens/delete | 批量删除 Token | ✅ | +| GET | /api/settings | 获取系统配置 | ✅ | +| POST | /api/settings | 更新系统配置 | ✅ | +| GET | /api/cache/size | 获取缓存大小 | ✅ | +| POST | /api/cache/clear | 清理所有缓存 | ✅ | +| POST | /api/cache/clear/images | 清理图片缓存 | ✅ | +| POST | /api/cache/clear/videos | 清理视频缓存 | ✅ | +| GET | /api/stats | 获取统计信息 | ✅ | + +

-### 注意事项 -⚠️ 本项目仅供学习和研究目的,请遵守相关使用条款。 +## 可用模型一览 + +| 模型名称 | 计次 | 账户类型 | 图像生成/编辑 | 深度思考 | 联网搜索 | 视频生成 | +|------------------------|--------|--------------|--------------|----------|----------|----------| +| `grok-3-fast` | 1 | Basic/Super | ✅ | ❌ | ✅ | ❌ | +| `grok-4-fast` | 1 | Basic/Super | ✅ | ✅ | ✅ | ❌ | +| `grok-4-fast-expert` | 4 | Basic/Super | ✅ | ✅ | ✅ | ❌ | +| `grok-4-expert` | 4 | Basic/Super | ✅ | ✅ | ✅ | ❌ | +| `grok-4-heavy` | 1 | Super | ✅ | ✅ | ✅ | ❌ | +| `grok-imagine-0.9` | - | Basic/Super | ❌ | ❌ | ❌ | ✅ |
---- +## 配置参数说明 + +> 服务启动后,登录 `/login` 管理后台进行参数配置 + +| 参数名 | 作用域 | 必填 | 说明 | 默认值 | +|----------------------------|---------|------|-----------------------------------------|--------| +| admin_username | global | 否 | 管理后台登录用户名 | "admin"| +| admin_password | global | 否 | 管理后台登录密码 | "admin"| +| log_level | global | 否 | 日志级别:DEBUG/INFO/... | "INFO" | +| image_cache_max_size_mb | global | 否 | 图片缓存最大容量(MB) | 500 | +| video_cache_max_size_mb | global | 否 | 视频缓存最大容量(MB) | 1000 | +| base_url | global | 否 | 服务基础URL/图片访问基准 | "" | +| api_key | grok | 否 | API 密钥(可选加强安全) | "" | +| proxy_url | grok | 否 | HTTP代理服务器地址 | "" | +| stream_chunk_timeout | grok | 否 | 流式分块超时时间(秒) | 120 | +| stream_first_response_timeout | grok | 否 | 流式首次响应超时时间(秒) | 30 | +| stream_total_timeout | grok | 否 | 流式总超时时间(秒) | 600 | +| cf_clearance | grok | 否 | Cloudflare安全令牌 | "" | +| x_statsig_id | grok | 是 | 反机器人唯一标识符 | "ZTpUeXBlRXJyb3I6IENhbm5vdCByZWFkIHByb3BlcnRpZXMgb2YgdW5kZWZpbmVkIChyZWFkaW5nICdjaGlsZE5vZGVzJyk=" | +| filtered_tags | grok | 否 | 过滤响应标签(逗号分隔) | "xaiartifact,xai:tool_usage_card,grok:render" | +| temporary | grok | 否 | 会话模式 true(临时)/false | true |
-此项目基于其他 Grok2API 项目进行学习重构,感谢 [LINUX DO](https://linux.do)、[VeroFess/grok2api](https://github.com/VeroFess/grok2api)、[xLmiler/grok2api_python](https://github.com/xLmiler/grok2api_python) 的项目大佬 \ No newline at end of file +## ⚠️ 注意事项 + +本项目仅供学习与研究,请遵守相关使用条款! + +
+ +> 本项目基于以下项目学习重构,特别感谢:[LINUX DO](https://linux.do)、[VeroFess/grok2api](https://github.com/VeroFess/grok2api)、[xLmiler/grok2api_python](https://github.com/xLmiler/grok2api_python) \ No newline at end of file