feat: add timeout mechanism

This commit is contained in:
Chenyme committed 2025-10-14 10:44:41 +08:00
1 parent 90a11df85d
commit fe0103ab64
5 files changed
+242 -15

No files matched your search

+90 -3
View File
@@ -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]:
"""
+5
View File
@@ -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]:
+103 -7
View File
@@ -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"![Generated Image](https://assets.grok.com/{img})\n"
yield make_chunk(content.strip(), "stop")
timeout_manager.mark_chunk_received()
return
elif token:
yield make_chunk(token)
timeout_manager.mark_chunk_received()
chunk_index += 1
# 提取对话数据
@@ -323,6 +407,7 @@ class GrokResponseProcessor:
thinking_finished = True
yield make_chunk(content)
timeout_manager.mark_chunk_received()
chunk_index += 1
is_thinking = current_is_thinking
@@ -335,12 +420,23 @@ class GrokResponseProcessor:
# 发送结束块
yield make_chunk("", "stop")
# 发送流结束标记
yield "data: [DONE]\n\n"
# 记录流式响应统计
logger.info(f"[Processor] 流式响应完成,总耗时: {timeout_manager.get_total_duration():.2f}秒")
except Exception as e:
logger.error(f"[Processor] 流式处理严重错误: {e}")
yield make_chunk(f"处理错误: {e}", "error")
# 发送流结束标记
yield "data: [DONE]\n\n"
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"