Skip to content

Commit 864b041

Browse files
committed
feat: 缓存批量下载 + 媒体文件在线预览 + 视频超分 + 多项增强
- 缓存管理:新增 ZIP 批量下载端点 + 单文件下载按钮 + 底部工具栏下载操作 - 媒体预览:FileResponse 设置 Content-Disposition: inline,浏览器直接播放视频/显示图片 - 视频超分:新增 upscale 工具模块 - 聊天处理器:重构流式响应逻辑,增强多模态处理 - NSFW 服务:扩展过滤和检测能力 - Token 管理:优化路由和并发控制 - 安全:.gitignore 排除 data/config.toml 防止敏感信息泄露
1 parent 6d42839 commit 864b041

File tree

21 files changed

+827
-130
lines changed

21 files changed

+827
-130
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ logs/
4545
*.log
4646

4747
# Data
48+
data/config.toml
4849
data/*.json
4950
data/tmp/*
5051
data/.locks/*

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@
7070

7171
- 新增 **「刷新全部」** 按钮:一键刷新所有 Token 状态,无需手动全选
7272

73+
---
74+
75+
### 缓存管理增强
76+
77+
- 新增 **批量下载**:勾选多个本地图片/视频文件后,点击底部工具栏「下载」按钮,服务端自动打包为 ZIP(`ZIP_STORED` 不压缩)一次性下载;仅选 1 个文件时直接下载,不打包
78+
- 新增 **单文件下载**:每行文件操作列新增下载图标,一键下载单个文件
79+
- 新增 **视频/图片在线预览**:浏览器打开文件链接可直接播放视频或显示图片,不再触发下载
80+
7381
<br>
7482

7583
## 部署方式
@@ -140,7 +148,7 @@ docker compose up -d
140148
- **状态筛选**:按状态(正常/限流/失效)或 NSFW 状态筛选
141149
- **批量操作**:批量刷新、导出、删除、开启 NSFW
142150
- **配置管理**:在线修改系统配置
143-
- **缓存管理**查看和清理媒体缓存
151+
- **缓存管理**查看、清理和下载媒体缓存(支持批量下载图片/视频)
144152
- **Imagine 图片生成/编辑**:WebSocket/SSE 实时图片生成 + 图片编辑模式(二开增强)
145153
- **Video 视频生成**:可视化视频生成,支持图生视频(二开新增)
146154
- **Voice Live 陪聊**:LiveKit 语音会话

app/api/v1/admin.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,72 @@ async def delete_local_cache_item_api(data: dict):
16891689
raise HTTPException(status_code=500, detail=str(e))
16901690

16911691

1692+
@router.post("/api/v1/admin/cache/download", dependencies=[Depends(verify_api_key)])
1693+
async def download_cache_files_api(data: dict):
1694+
"""Batch download local cache files as a ZIP archive (ZIP_STORED, no compression)."""
1695+
import zipfile
1696+
import tempfile
1697+
from datetime import datetime
1698+
from fastapi.responses import FileResponse
1699+
from starlette.background import BackgroundTask
1700+
from app.services.grok.services.assets import DownloadService
1701+
1702+
cache_type = data.get("type", "image")
1703+
names = data.get("names", [])
1704+
1705+
if not names or not isinstance(names, list):
1706+
raise HTTPException(status_code=400, detail="No files specified")
1707+
1708+
dl_service = DownloadService()
1709+
base_dir = dl_service.image_dir if cache_type == "image" else dl_service.video_dir
1710+
1711+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
1712+
try:
1713+
packed = 0
1714+
with zipfile.ZipFile(tmp, "w", zipfile.ZIP_STORED) as zf:
1715+
for name in names:
1716+
if not name or ".." in name or "/" in name or "\\" in name or ":" in name:
1717+
continue
1718+
file_path = base_dir / name
1719+
try:
1720+
if file_path.resolve().parent != base_dir.resolve():
1721+
continue
1722+
except (ValueError, OSError):
1723+
continue
1724+
if file_path.exists() and file_path.is_file():
1725+
zf.write(file_path, name)
1726+
packed += 1
1727+
tmp.close()
1728+
1729+
if packed == 0:
1730+
os.unlink(tmp.name)
1731+
raise HTTPException(status_code=404, detail="No valid files found")
1732+
1733+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1734+
zip_name = f"{cache_type}_{timestamp}.zip"
1735+
1736+
async def cleanup():
1737+
try:
1738+
os.unlink(tmp.name)
1739+
except Exception:
1740+
pass
1741+
1742+
return FileResponse(
1743+
tmp.name,
1744+
media_type="application/zip",
1745+
filename=zip_name,
1746+
background=BackgroundTask(cleanup),
1747+
)
1748+
except HTTPException:
1749+
raise
1750+
except Exception:
1751+
try:
1752+
os.unlink(tmp.name)
1753+
except Exception:
1754+
pass
1755+
raise HTTPException(status_code=500, detail="Failed to create ZIP archive")
1756+
1757+
16921758
@router.post("/api/v1/admin/cache/online/clear", dependencies=[Depends(verify_api_key)])
16931759
async def clear_online_cache_api(data: dict):
16941760
"""清理在线缓存"""

app/api/v1/chat.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,11 @@ class ChatCompletionRequest(BaseModel):
110110
model: str = Field(..., description="模型名称")
111111
messages: List[MessageItem] = Field(..., description="消息数组")
112112
stream: Optional[bool] = Field(None, description="是否流式输出")
113-
thinking: Optional[str] = Field(None, description="思考模式: enabled/disabled/None")
113+
reasoning_effort: Optional[str] = Field(
114+
None, description="推理强度: none/minimal/low/medium/high/xhigh"
115+
)
116+
temperature: Optional[float] = Field(0.8, description="采样温度: 0-2")
117+
top_p: Optional[float] = Field(0.95, description="nucleus 采样: 0-1")
114118

115119
# 视频生成配置
116120
video_config: Optional[VideoConfig] = Field(None, description="视频生成参数")
@@ -272,7 +276,7 @@ async def chat_completions(request: ChatCompletionRequest):
272276
model=request.model,
273277
messages=[msg.model_dump() for msg in request.messages],
274278
stream=resolved_stream,
275-
thinking=request.thinking,
279+
thinking=request.reasoning_effort,
276280
aspect_ratio=v_conf.aspect_ratio,
277281
video_length=v_conf.video_length,
278282
resolution=v_conf.resolution_name,
@@ -283,7 +287,9 @@ async def chat_completions(request: ChatCompletionRequest):
283287
model=request.model,
284288
messages=[msg.model_dump() for msg in request.messages],
285289
stream=resolved_stream,
286-
thinking=request.thinking,
290+
reasoning_effort=request.reasoning_effort,
291+
temperature=request.temperature,
292+
top_p=request.top_p,
287293
)
288294

289295
if isinstance(result, dict):

app/api/v1/files.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
IMAGE_DIR = BASE_DIR / "image"
1818
VIDEO_DIR = BASE_DIR / "video"
1919

20+
# 浏览器直接预览,不触发下载
21+
_INLINE_HEADERS = {
22+
"Content-Disposition": "inline",
23+
"Cache-Control": "public, max-age=31536000, immutable",
24+
}
25+
2026

2127
def _resolve_media_path(base_dir: Path, filename: str) -> Path:
2228
"""Resolve a safe media path under the target directory."""
@@ -55,11 +61,10 @@ async def get_image(filename: str):
5561
elif file_path.suffix.lower() == ".webp":
5662
content_type = "image/webp"
5763

58-
# 增加缓存头,支持高并发场景下的浏览器/CDN缓存
5964
return FileResponse(
6065
file_path,
6166
media_type=content_type,
62-
headers={"Cache-Control": "public, max-age=31536000, immutable"},
67+
headers=_INLINE_HEADERS,
6368
)
6469

6570
logger.warning(f"Image not found: {filename}")
@@ -78,7 +83,7 @@ async def get_video(filename: str):
7883
return FileResponse(
7984
file_path,
8085
media_type="video/mp4",
81-
headers={"Cache-Control": "public, max-age=31536000, immutable"},
86+
headers=_INLINE_HEADERS,
8287
)
8388

8489
logger.warning(f"Video not found: {filename}")

app/services/grok/models/model.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ class ModelService:
119119
cost=Cost.HIGH,
120120
display_name="GROK-4.1-THINKING",
121121
),
122+
ModelInfo(
123+
model_id="grok-4.20-beta",
124+
grok_model="grok-420",
125+
model_mode="MODEL_MODE_GROK_420",
126+
cost=Cost.LOW,
127+
display_name="GROK-4.20-BETA",
128+
),
122129
ModelInfo(
123130
model_id="grok-imagine-1.0",
124131
grok_model="grok-3",

0 commit comments

Comments
 (0)