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:
@@ -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,
|
||||
|
||||
@@ -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
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