Files
deerflow-code/offline-backend-20260512/backend/app/gateway/routers/report_structures.py
T

169 lines
6.6 KiB
Python

"""CRUD API for deep-research report structures (报告结构 / 工作流配置).
The deep-research workbench's 高级配置 → 工作流配置 dialog renders a card per
row; picking a card applies ``content`` to the research config's
``custom_outline``. Global / shared — every account sees the same cards.
Routes (prefix ``/api/report-structures``):
GET "" list (optional ``enabled`` / ``title`` filters)
POST "" create (title / name / content; enabled forced True)
GET "/{id}" get one
PUT "/{id}" update title / name / content
PUT "/{id}/enabled" toggle 启用/停用
DELETE "/{id}" delete
The first list call seeds the two built-in outlines (通用分析结构 /
递进式论证结构) when the table has never held a row — see
``deerflow.persistence.report_structures.defaults``.
"""
from __future__ import annotations
import logging
from uuid import uuid4
from fastapi import APIRouter, HTTPException, Query, Request
from pydantic import BaseModel, Field
from app.gateway.deps import get_optional_user_from_request
from deerflow.persistence.report_structures import ReportStructureStore
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/report-structures", tags=["report-structures"])
# ── schemas ──────────────────────────────────────────────────────────────
class ReportStructureResponse(BaseModel):
id: str
title: str
name: str
content: str
enabled: bool = True
created_by: str | None = None
created_at: str | None = None
updated_at: str | None = None
class ReportStructureListResponse(BaseModel):
structures: list[ReportStructureResponse]
class ReportStructureCreateRequest(BaseModel):
title: str = Field(..., min_length=1, max_length=256)
name: str = Field(default="", max_length=256)
content: str = Field(..., min_length=1, max_length=20_000)
class ReportStructureUpdateRequest(BaseModel):
title: str | None = Field(default=None, min_length=1, max_length=256)
name: str | None = Field(default=None, max_length=256)
content: str | None = Field(default=None, min_length=1, max_length=20_000)
class ReportStructureEnabledRequest(BaseModel):
enabled: bool
# ── helpers ──────────────────────────────────────────────────────────────
def _get_store(request: Request) -> ReportStructureStore:
store = getattr(request.app.state, "report_structure_store", None)
if store is None:
raise HTTPException(status_code=503, detail="Report structure store not available")
return store
async def _current_username(request: Request) -> str | None:
"""创建用户名:邮箱(本部署的账号即邮箱),无认证时为 None。"""
user = await get_optional_user_from_request(request)
if user is None:
return None
return str(user.email) or str(user.id)
# ── routes ───────────────────────────────────────────────────────────────
@router.get("", response_model=ReportStructureListResponse)
async def list_report_structures(
request: Request,
enabled: bool | None = Query(default=None, description="启用/停用筛选;缺省=全部"),
title: str | None = Query(default=None, max_length=256, description="标题模糊筛选(大小写不敏感)"),
) -> ReportStructureListResponse:
store = _get_store(request)
await store.ensure_defaults()
rows = await store.list_structures()
if enabled is not None:
rows = [r for r in rows if bool(r.get("enabled", True)) is enabled]
if title:
needle = title.strip().lower()
if needle:
rows = [r for r in rows if needle in str(r.get("title", "")).lower()]
return ReportStructureListResponse(structures=[ReportStructureResponse(**r) for r in rows])
@router.post("", response_model=ReportStructureResponse, status_code=201)
async def create_report_structure(request: Request, body: ReportStructureCreateRequest) -> ReportStructureResponse:
store = _get_store(request)
row = await store.create_structure(
{
"id": str(uuid4()),
"title": body.title.strip(),
"name": body.name.strip(),
"content": body.content.strip(),
"created_by": await _current_username(request),
}
)
return ReportStructureResponse(**row)
@router.get("/{structure_id}", response_model=ReportStructureResponse)
async def get_report_structure(request: Request, structure_id: str) -> ReportStructureResponse:
store = _get_store(request)
row = await store.get_structure(structure_id)
if row is None:
raise HTTPException(status_code=404, detail="报告结构不存在")
return ReportStructureResponse(**row)
@router.put("/{structure_id}", response_model=ReportStructureResponse)
async def update_report_structure(
request: Request, structure_id: str, body: ReportStructureUpdateRequest
) -> ReportStructureResponse:
store = _get_store(request)
data: dict = {}
if body.title is not None:
data["title"] = body.title.strip()
if body.name is not None:
data["name"] = body.name.strip()
if body.content is not None:
data["content"] = body.content.strip()
if not data:
raise HTTPException(status_code=400, detail="没有需要更新的字段")
row = await store.update_structure(structure_id, data)
if row is None:
raise HTTPException(status_code=404, detail="报告结构不存在")
return ReportStructureResponse(**row)
@router.put("/{structure_id}/enabled", response_model=ReportStructureResponse)
async def set_report_structure_enabled(
request: Request, structure_id: str, body: ReportStructureEnabledRequest
) -> ReportStructureResponse:
store = _get_store(request)
row = await store.set_enabled(structure_id, body.enabled)
if row is None:
raise HTTPException(status_code=404, detail="报告结构不存在")
return ReportStructureResponse(**row)
@router.delete("/{structure_id}", status_code=204)
async def delete_report_structure(request: Request, structure_id: str) -> None:
store = _get_store(request)
deleted = await store.delete_structure(structure_id)
if not deleted:
raise HTTPException(status_code=404, detail="报告结构不存在")