Merge pull request #7 from chenyme/proxy

fix: proxy exception repair
This commit is contained in:
Chenyme authored and GitHub committed 2025-10-14 11:44:39 +08:00
commit 48610be945
11 files changed
+453 -191

No files matched your search

+100 -11
View File
@@ -252,7 +252,7 @@ async def admin_login(request: LoginRequest) -> LoginResponse:
@router.post("/api/logout")
async def admin_logout(authenticated: bool = Depends(verify_admin_session),
async def admin_logout(_: bool = Depends(verify_admin_session),
authorization: Optional[str] = Header(None)) -> Dict[str, Any]:
"""
管理员登出
@@ -279,7 +279,7 @@ async def admin_logout(authenticated: bool = Depends(verify_admin_session),
@router.get("/api/tokens", response_model=TokenListResponse)
async def list_tokens(authenticated: bool = Depends(verify_admin_session)) -> TokenListResponse:
async def list_tokens(_: bool = Depends(verify_admin_session)) -> TokenListResponse:
"""
获取所有Token列表
@@ -337,7 +337,7 @@ async def list_tokens(authenticated: bool = Depends(verify_admin_session)) -> To
@router.post("/api/tokens/add")
async def add_tokens(request: AddTokensRequest,
authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""
批量添加Token
@@ -372,7 +372,7 @@ async def add_tokens(request: AddTokensRequest,
@router.post("/api/tokens/delete")
async def delete_tokens(request: DeleteTokensRequest,
authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""
批量删除Token
@@ -406,7 +406,7 @@ async def delete_tokens(request: DeleteTokensRequest,
@router.get("/api/settings")
async def get_settings(authenticated: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
async def get_settings(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""获取全局配置"""
try:
logger.debug("[Admin] 获取全局配置")
@@ -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
View File
@@ -22,21 +22,21 @@ router = APIRouter(prefix="/chat", tags=["聊天"])
@router.post("/completions", response_model=None)
async def chat_completions(
request: OpenAIChatRequest,
authenticated: Optional[str] = Depends(auth_manager.verify)
_: Optional[str] = Depends(auth_manager.verify)
):
"""
创建聊天补全
兼容OpenAI聊天API的端点,支持流式和非流式响应。
Args:
request: OpenAI格式的聊天请求
authenticated: 认证状态(由依赖注入)
_: 认证依赖(自动验证)
Returns:
OpenAIChatCompletionResponse: 非流式响应
StreamingResponse: 流式响应
Raises:
HTTPException: 当请求处理失败时
"""
@@ -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={
+9 -9
View File
@@ -19,15 +19,15 @@ router = APIRouter(tags=["模型"])
@router.get("/models")
async def list_models(authenticated: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
async def list_models(_: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
"""
获取可用模型列表
返回 OpenAI 兼容的模型列表格式,包含系统支持的所有 Grok 模型的详细信息。
Args:
authenticated: 认证状态(由依赖注入)
_: 认证依赖(自动验证)
Returns:
Dict[str, Any]: 包含模型列表的响应数据
"""
@@ -85,14 +85,14 @@ async def list_models(authenticated: Optional[str] = Depends(auth_manager.verify
@router.get("/models/{model_id}")
async def get_model(model_id: str, authenticated: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
async def get_model(model_id: str, _: Optional[str] = Depends(auth_manager.verify)) -> Dict[str, Any]:
"""
获取特定模型信息
Args:
model_id (str): 模型ID
authenticated: 认证状态(由依赖注入)
_: 认证依赖(自动验证)
Returns:
Dict[str, Any]: 模型详细信息
"""
+1 -1
View File
@@ -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,
+8 -8
View File
@@ -15,8 +15,8 @@ class OpenAIChatRequest(BaseModel):
max_tokens: Optional[int] = Field(None, ge=1, le=100000, description="最大Token数")
top_p: Optional[float] = Field(1.0, ge=0, le=1, description="采样参数")
@field_validator('messages')
@classmethod
@field_validator('messages')
def validate_messages(cls, v):
"""验证消息格式"""
if not v:
@@ -49,8 +49,8 @@ class OpenAIChatRequest(BaseModel):
return v
@field_validator('model')
@classmethod
@field_validator('model')
def validate_model(cls, v):
"""验证模型名称"""
allowed_models = [
@@ -68,16 +68,16 @@ class OpenAIChatCompletionMessage(BaseModel):
"""聊天完成消息"""
role: str = Field(..., description="角色")
content: str = Field(..., description="消息内容")
reference_id: Optional[str] = Field(None, description="参考ID")
annotations: Optional[List[str]] = Field(None, description="注释")
reference_id: Optional[str] = Field(default=None, description="参考ID")
annotations: Optional[List[str]] = Field(default=None, description="注释")
class OpenAIChatCompletionChoice(BaseModel):
"""聊天完成选项"""
index: int = Field(..., description="选项索引")
message: OpenAIChatCompletionMessage = Field(..., description="响应消息")
logprobs: Optional[float] = Field(None, description="对数概率")
finish_reason: str = Field("stop", description="完成原因")
logprobs: Optional[float] = Field(default=None, description="对数概率")
finish_reason: str = Field(default="stop", description="完成原因")
class OpenAIChatCompletionResponse(BaseModel):
@@ -108,8 +108,8 @@ class OpenAIChatCompletionChunkChoice(BaseModel):
class OpenAIChatCompletionChunkResponse(BaseModel):
"""流式聊天完成响应"""
id: str = Field(..., description="响应ID")
object: str = Field("chat.completion.chunk", description="对象类型")
object: str = Field(default="chat.completion.chunk", description="对象类型")
created: int = Field(..., description="创建时间戳")
model: str = Field(..., description="使用的模型")
system_fingerprint: Optional[str] = Field(None, description="系统指纹")
system_fingerprint: Optional[str] = Field(default=None, description="系统指纹")
choices: List[OpenAIChatCompletionChunkChoice] = Field(..., description="响应选项")
+7 -13
View File
@@ -14,12 +14,7 @@ class CacheService:
"""缓存服务基类"""
def __init__(self, cache_type: str):
"""
初始化缓存服务
Args:
cache_type: 缓存类型,'image' 或 'video'
"""
"""初始化缓存服务"""
self.cache_type = cache_type
self.cache_dir = Path(f"data/temp/{cache_type}")
self.cache_dir.mkdir(parents=True, exist_ok=True)
@@ -27,7 +22,6 @@ class CacheService:
@staticmethod
def _get_cache_filename(file_path: str) -> str:
"""将文件路径转换为缓存文件名"""
# 移除开头的斜杠并替换所有斜杠为短横线
filename = file_path.lstrip('/').replace('/', '-')
return filename
@@ -75,7 +69,7 @@ class CacheService:
# 代理配置
proxy_url = setting.grok_config.get("proxy_url")
proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else {}
async with AsyncSession() as session:
logger.debug(f"[{self.cache_type.upper()}Cache] 开始下载: {file_url}")
@@ -115,12 +109,12 @@ class CacheService:
async def cleanup_cache(self):
"""清理缓存目录,确保不超过配置的大小限制"""
try:
# 获取配置的最大缓存大小(MB)
# 获取配置的最大缓存大小
config_key = f"{self.cache_type}_cache_max_size_mb"
max_size_mb = setting.global_config.get(config_key, 500)
max_size_bytes = max_size_mb * 1024 * 1024
# 获取所有缓存文件及其大小和修改时间
# 获取缓存大小和修改时间
files = []
total_size = 0
@@ -138,10 +132,10 @@ class CacheService:
logger.info(f"[{self.cache_type.upper()}Cache] 缓存大小 {total_size / 1024 / 1024:.2f}MB 超过限制 {max_size_mb}MB,开始清理")
# 按修改时间排序(最旧的在前)
# 按修改时间排序
files.sort(key=lambda x: x[2])
# 删除最旧的文件直到总大小低于限制
# 删除文件直到总大小低于限制
for file_path, size, _ in files:
if total_size <= max_size_bytes:
break
@@ -170,7 +164,7 @@ class ImageCacheService(CacheService):
auth_token: 认证令牌
Returns:
缓存文件路径,下载失败返回 None
缓存文件路径,下载失败返回 None`
"""
return await self.download_file(image_path, auth_token, timeout=30.0)
+19 -9
View File
@@ -148,18 +148,23 @@ class GrokClient:
try:
# 构建请求头和代理
headers = GrokClient._build_headers(auth_token)
proxies = GrokClient._get_proxy()
proxy_config = GrokClient._get_proxy()
# 构建请求参数
request_kwargs = {
"headers": headers,
"data": json.dumps(payload),
"impersonate": IMPERSONATE_BROWSER,
"timeout": REQUEST_TIMEOUT,
"stream": True,
"proxies": proxy_config if proxy_config else None
}
# 在线程池中执行同步HTTP请求,避免阻塞事件循环
response = await asyncio.to_thread(
curl_requests.post,
GROK_API_ENDPOINT,
headers=headers,
data=json.dumps(payload),
impersonate=IMPERSONATE_BROWSER,
timeout=REQUEST_TIMEOUT,
stream=True,
**proxies
**request_kwargs
)
logger.debug(f"[Client] API响应状态码: {response.status_code}")
@@ -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
View File
@@ -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![Generated Image](https://assets.grok.com/{img})"
# 返回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"![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
# 对话模式
# 提取对话数据
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
View File
@@ -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
View File
@@ -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"
+118 -75
View File
@@ -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)