feat: Agent 批量测试、作业助手与上传预览;Windows 启动脚本与文档- 新增 run_agent_test_cases 与示例 JSON、(红头)agent测试用例文档
- 扩展 test_agent_execution(--homework、UTF-8 控制台) - 后端:uploads 预览、file_read、工作流与对话落盘等 - 前端:AgentChatPreview 与设计器相关调整 - 忽略 redis二进制、agent_workspaces、uploads、tessdata 等本机产物 Made-with: Cursor
This commit is contained in:
112
backend/app/api/uploads.py
Normal file
112
backend/app/api/uploads.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
预览 / 聊天附件上传:写入 LOCAL_FILE_TOOLS_ROOT 下,供 file_read 等工具使用。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import uuid
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.api.auth import get_current_user
|
||||
from app.core.config import settings
|
||||
from app.models.user import User
|
||||
from app.services.builtin_tools import _local_file_workspace_root
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/uploads",
|
||||
tags=["uploads"],
|
||||
)
|
||||
|
||||
|
||||
class PreviewUploadResponse(BaseModel):
|
||||
"""相对工作区根的路径,与 file_read 约定一致(可用相对路径)。"""
|
||||
|
||||
relative_path: str
|
||||
filename: str
|
||||
size: int
|
||||
content_type: str | None = None
|
||||
|
||||
|
||||
def _safe_filename(name: str) -> str:
|
||||
base = Path(name).name.strip()
|
||||
if not base:
|
||||
return "attachment.bin"
|
||||
base = re.sub(r"[^\w.\-\u4e00-\u9fff]+", "_", base, flags=re.UNICODE)
|
||||
return base[:180] if len(base) > 180 else base
|
||||
|
||||
|
||||
@router.post(
|
||||
"/preview",
|
||||
response_model=PreviewUploadResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def upload_preview_file(
|
||||
file: UploadFile = File(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
上传单个文件到工作区 `uploads/preview/<user_id>/`,返回相对路径。
|
||||
大小上限与 LOCAL_FILE_WRITE_MAX_BYTES 一致。
|
||||
"""
|
||||
max_bytes = max(1024, int(getattr(settings, "LOCAL_FILE_WRITE_MAX_BYTES", 2_097_152) or 2_097_152))
|
||||
root = _local_file_workspace_root()
|
||||
uid = str(current_user.id)
|
||||
dest_dir = root / "uploads" / "preview" / uid
|
||||
try:
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as e:
|
||||
logger.warning("创建上传目录失败: %s", e)
|
||||
raise HTTPException(status_code=500, detail="无法创建上传目录") from e
|
||||
|
||||
raw_name = file.filename or "attachment"
|
||||
safe = _safe_filename(raw_name)
|
||||
short = uuid.uuid4().hex[:12]
|
||||
dest = dest_dir / f"{short}_{safe}"
|
||||
|
||||
total = 0
|
||||
chunk_size = 1024 * 256
|
||||
try:
|
||||
with dest.open("wb") as out:
|
||||
while True:
|
||||
buf = await file.read(chunk_size)
|
||||
if not buf:
|
||||
break
|
||||
total += len(buf)
|
||||
if total > max_bytes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"文件超过允许大小({max_bytes} 字节)",
|
||||
)
|
||||
out.write(buf)
|
||||
except HTTPException:
|
||||
try:
|
||||
dest.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
except OSError as e:
|
||||
try:
|
||||
dest.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
logger.warning("写入上传文件失败: %s", e)
|
||||
raise HTTPException(status_code=500, detail="保存文件失败") from e
|
||||
|
||||
try:
|
||||
rel = dest.relative_to(root).as_posix()
|
||||
except ValueError:
|
||||
rel = str(dest).replace("\\", "/")
|
||||
|
||||
logger.info("预览上传 user=%s path=%s size=%s", uid, rel, total)
|
||||
return PreviewUploadResponse(
|
||||
relative_path=rel,
|
||||
filename=raw_name,
|
||||
size=total,
|
||||
content_type=file.content_type,
|
||||
)
|
||||
Reference in New Issue
Block a user