11 files changed
+453
-191
No files matched your search
+100
-11
@@ -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] 获取全局配置")
|
||||
@@ -428,8 +428,15 @@ 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, 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 +491,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 +532,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] 开始清理缓存")
|
||||
|
||||
@@ -558,7 +567,7 @@ async def clear_cache(authenticated: bool = Depends(verify_admin_session)) -> Di
|
||||
|
||||
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} 个文件",
|
||||
@@ -577,8 +586,88 @@ async def clear_cache(authenticated: bool = Depends(verify_admin_session)) -> Di
|
||||
)
|
||||
|
||||
|
||||
@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(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
async def get_stats(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
|
||||
"""
|
||||
获取统计信息
|
||||
|
||||
|
||||
+8
-8
@@ -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: 当请求处理失败时
|
||||
"""
|
||||
@@ -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={
|
||||
@@ -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={
|
||||
|
||||
@@ -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]: 模型详细信息
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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="响应选项")
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}")
|
||||
@@ -175,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]:
|
||||
@@ -204,9 +214,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))
|
||||
|
||||
+139
-52
@@ -3,7 +3,8 @@
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
from typing import Iterator
|
||||
import asyncio
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from app.core.config import setting
|
||||
from app.core.exception import GrokApiException
|
||||
@@ -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:
|
||||
@@ -43,7 +99,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}")
|
||||
@@ -79,6 +135,7 @@ class GrokResponseProcessor:
|
||||
)],
|
||||
usage=None
|
||||
)
|
||||
response_closed = True
|
||||
response.close()
|
||||
return result
|
||||
|
||||
@@ -98,7 +155,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 +171,7 @@ class GrokResponseProcessor:
|
||||
logger.warning(f"[Processor] 缓存图片失败: {e}")
|
||||
content += f"\n"
|
||||
|
||||
# 返回OpenAI格式响应
|
||||
# 返回 OpenAI 响应格式
|
||||
result = OpenAIChatCompletionResponse(
|
||||
id=f"chatcmpl-{uuid.uuid4()}",
|
||||
object="chat.completion",
|
||||
@@ -130,35 +187,48 @@ 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) -> 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
|
||||
video_progress_started = False
|
||||
last_video_progress = -1
|
||||
response_closed = False
|
||||
|
||||
def make_chunk(content: str, finish: str = None):
|
||||
# 初始化超时管理器
|
||||
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格式的响应块"""
|
||||
chunk_data = OpenAIChatCompletionChunkResponse(
|
||||
id=f"chatcmpl-{uuid.uuid4()}",
|
||||
@@ -168,8 +238,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()
|
||||
@@ -178,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
|
||||
@@ -204,29 +282,43 @@ 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)
|
||||
timeout_manager.mark_chunk_received()
|
||||
chunk_index += 1
|
||||
|
||||
continue
|
||||
@@ -238,16 +330,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}"
|
||||
@@ -256,12 +348,14 @@ class GrokResponseProcessor:
|
||||
logger.warning(f"[Processor] 缓存图片失败: {e}")
|
||||
content += f"\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
|
||||
|
||||
# 对话模式
|
||||
# 提取对话数据
|
||||
else:
|
||||
# 过滤 list 格式的 token
|
||||
if isinstance(token, list):
|
||||
@@ -275,7 +369,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 +377,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", "")
|
||||
@@ -313,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
|
||||
|
||||
@@ -323,33 +418,25 @@ 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"
|
||||
|
||||
# 记录流式响应统计
|
||||
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"
|
||||
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}")
|
||||
+39
-3
@@ -325,15 +325,28 @@
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="text-sm text-muted-foreground w-12">图片:</span>
|
||||
<input id="imageCacheSize" readonly class="flex h-9 flex-1 rounded-md border border-input bg-muted px-3 py-2 text-sm" placeholder="0 MB">
|
||||
<button onclick="clearImageCache()" class="inline-flex items-center justify-center rounded-md text-sm font-medium bg-destructive text-destructive-foreground hover:bg-destructive/90 h-9 px-3 transition-colors" title="仅清除图片缓存">
|
||||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<circle cx="9" cy="9" r="2"/>
|
||||
<path d="M21 15l-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="text-sm text-muted-foreground w-12">视频:</span>
|
||||
<input id="videoCacheSize" readonly class="flex h-9 flex-1 rounded-md border border-input bg-muted px-3 py-2 text-sm" placeholder="0 MB">
|
||||
<button onclick="clearVideoCache()" class="inline-flex items-center justify-center rounded-md text-sm font-medium bg-destructive text-destructive-foreground hover:bg-destructive/90 h-9 px-3 transition-colors" title="仅清除视频缓存">
|
||||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="text-sm text-muted-foreground w-12">总计:</span>
|
||||
<input id="totalCacheSize" readonly class="flex h-9 flex-1 rounded-md border border-input bg-muted px-3 py-2 text-sm font-medium" placeholder="0 MB">
|
||||
<button onclick="clearCache()" class="inline-flex items-center justify-center rounded-md text-sm font-medium bg-orange-600 text-white hover:bg-orange-700 h-9 px-3 transition-colors">
|
||||
<button onclick="clearCache()" class="inline-flex items-center justify-center rounded-md text-sm font-medium bg-destructive text-destructive-foreground hover:bg-destructive/90 h-9 px-3 transition-colors" title="清除所有缓存(图片+视频)">
|
||||
<svg class="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 6h18"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
@@ -407,6 +420,27 @@
|
||||
<option value="true">开启</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
数据块间隔超时 (秒)
|
||||
<span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="流式响应中,两次数据块之间的最大等待时间。超过此时间将结束会话">?</span>
|
||||
</label>
|
||||
<input id="cfgStreamChunkTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="120">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
首次响应超时 (秒)
|
||||
<span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="从请求开始到收到第一个数据块的最大等待时间。超过此时间将结束会话">?</span>
|
||||
</label>
|
||||
<input id="cfgStreamFirstResponseTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="30">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
总超时限制 (秒)
|
||||
<span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="流式响应的总时长限制。设置为0表示不限制总时长">?</span>
|
||||
</label>
|
||||
<input id="cfgStreamTotalTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="600">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button onclick="saveGrokSettings()" class="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-6 transition-colors">保存配置</button>
|
||||
@@ -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)});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
+5
-2
@@ -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"
|
||||
|
||||
@@ -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}` | 获取生成的图片文件 | ❌ |
|
||||
|
||||
|
||||
<details>
|
||||
<summary>其余管理接口</summary>
|
||||
|
||||
| 方法 | 端点 | 描述 | 认证 |
|
||||
|------|------|------|------|
|
||||
| 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 | 获取统计信息 | ✅ |
|
||||
|
||||
</details>
|
||||
|
||||
<br>
|
||||
|
||||
### 使用说明
|
||||
## 使用说明
|
||||
|
||||
#### 调用次数说明
|
||||
### 调用次数与配额
|
||||
|
||||
- 普通账号(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 的图片分析调用格式一致)
|
||||
- 返回格式为 `<video src="{full_video_url}" controls="controls"></video>`
|
||||
- **注意: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`,故弃用,采用固定值以兼容所有请求
|
||||
|
||||
<br>
|
||||
|
||||
## 如何部署
|
||||
|
||||
### docker-compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
grok2api:
|
||||
image: ghcr.io/chenyme/grok2api:latest
|
||||
@@ -73,45 +78,83 @@ volumes:
|
||||
|
||||
<br>
|
||||
|
||||
### 可用模型
|
||||
> 所有模型均可进行图像生成,每次计次为 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}` | 获取生成图片文件 | ❌ |
|
||||
|
||||
<br>
|
||||
|
||||
### 参数说明
|
||||
> 所有参数请在启动服务后,进入 `/login` 路由后登陆管理进行配置
|
||||
<details>
|
||||
<summary>管理与统计接口(展开查看更多)</summary>
|
||||
|
||||
| 参数名 | 位置 | 必填 | 说明 | 默认值 |
|
||||
|---------|---------|------|------|--------|
|
||||
| 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 | 获取统计信息 | ✅ |
|
||||
|
||||
</details>
|
||||
|
||||
<br>
|
||||
|
||||
### 注意事项
|
||||
⚠️ 本项目仅供学习和研究目的,请遵守相关使用条款。
|
||||
## 可用模型一览
|
||||
|
||||
| 模型名称 | 计次 | 账户类型 | 图像生成/编辑 | 深度思考 | 联网搜索 | 视频生成 |
|
||||
|------------------------|--------|--------------|--------------|----------|----------|----------|
|
||||
| `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 | ❌ | ❌ | ❌ | ✅ |
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
## 配置参数说明
|
||||
|
||||
> 服务启动后,登录 `/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 |
|
||||
|
||||
<br>
|
||||
|
||||
此项目基于其他 Grok2API 项目进行学习重构,感谢 [LINUX DO](https://linux.do)、[VeroFess/grok2api](https://github.com/VeroFess/grok2api)、[xLmiler/grok2api_python](https://github.com/xLmiler/grok2api_python) 的项目大佬
|
||||
## ⚠️ 注意事项
|
||||
|
||||
本项目仅供学习与研究,请遵守相关使用条款!
|
||||
|
||||
<br>
|
||||
|
||||
> 本项目基于以下项目学习重构,特别感谢:[LINUX DO](https://linux.do)、[VeroFess/grok2api](https://github.com/VeroFess/grok2api)、[xLmiler/grok2api_python](https://github.com/xLmiler/grok2api_python)
|
||||
Reference in new issue
Block a user