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:
renjianbo
2026-04-13 20:17:18 +08:00
parent 0608161c82
commit df4fab1e6e
31 changed files with 3784 additions and 251 deletions

View File

@@ -13,6 +13,7 @@ from app.api.auth import get_current_user
from app.models.user import User
from app.core.exceptions import NotFoundError, ValidationError, ConflictError
from app.services.permission_service import check_agent_permission
from app.services.agent_workspace_chat_log import fetch_agent_preview_chat_turns
from app.services.workflow_validator import validate_workflow
import uuid
@@ -68,6 +69,18 @@ class AgentFromSceneTemplateCreate(BaseModel):
budget_config: Optional[Dict[str, Any]] = None
class PreviewChatTurnResponse(BaseModel):
"""设计器预览侧单轮对话(来自已完成执行)"""
execution_id: str
created_at: datetime
user_text: str
agent_text: str
class Config:
from_attributes = True
class AgentResponse(BaseModel):
"""Agent响应模型"""
id: str
@@ -208,6 +221,40 @@ async def create_agent(
return agent
@router.get(
"/{agent_id}/preview-chat-history",
response_model=List[PreviewChatTurnResponse],
)
async def get_agent_preview_chat_history(
agent_id: str,
preview_user_id: Optional[str] = Query(
None,
description="预览会话 user_id与创建执行时 input_data.user_id 一致),只返回本会话记录",
),
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
获取设计器预览区的对话历史(按已完成执行还原)。
与前端 localStorage 中的 preview user_id 对齐,避免多人混在同一 Agent 下串会话。
须注册在 GET /{agent_id} 之前,避免路径被误匹配。
"""
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise NotFoundError(f"Agent不存在: {agent_id}")
if not check_agent_permission(db, current_user, agent, "read"):
raise HTTPException(status_code=403, detail="无权访问此Agent")
rows = fetch_agent_preview_chat_turns(
db,
agent_id,
preview_user_id=preview_user_id,
limit=limit,
)
return rows
@router.get("/{agent_id}", response_model=AgentResponse)
async def get_agent(
agent_id: str,

View File

@@ -14,6 +14,7 @@ from app.models.agent import Agent
from app.api.auth import get_current_user
from app.models.user import User
from app.tasks.workflow_tasks import execute_workflow_task, resume_workflow_task
from app.services.agent_workspace_chat_log import ensure_agent_dialogue_logged_from_db_execution
import uuid
import logging
@@ -478,4 +479,11 @@ async def get_execution(
if not _can_view_execution(db, current_user, execution):
raise HTTPException(status_code=403, detail="无权访问")
# 补救:若 Celery Worker 未带对话落盘逻辑,首次拉取已完成执行时由 API 进程写入 dialogue.md与任务内写入幂等
if execution.status == "completed":
try:
ensure_agent_dialogue_logged_from_db_execution(db, execution)
except Exception:
logger.debug("补写智能体对话 MD 跳过", exc_info=True)
return _execution_to_response(execution)

112
backend/app/api/uploads.py Normal file
View 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,
)