feat: add timeout mechanism
This commit is contained in:
1 parent
90a11df85d
commit
fe0103ab64
5 files changed
+242
-15
No files matched your search
+90
-3
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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'<video src="{full_video_url}" controls="controls"></video>'
|
||||
|
||||
|
||||
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"\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"
|
||||
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"
|
||||
|
||||
Reference in new issue
Block a user