"""blocks.py — Blocks 操作 API""" from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from app.api.v1 import ok from app.core.dependencies import get_db from app.schemas.block import ( BlockBase, BlockCreate, BlockSearchResult, BlockStats, BlockUpdate, TOCItem, ) from app.services.content_db import ContentDB from app.services.document_service import DocumentService router = APIRouter(prefix="/documents/{documentId}/blocks", tags=["Blocks"]) @router.get("", summary="获取文档的所有 blocks") async def get_blocks( documentId: str, db: AsyncSession = Depends(get_db), ) -> dict: """获取文档的所有 blocks,按 block_order 排序""" svc = DocumentService(db) doc = await svc.get_document(documentId) with ContentDB(doc.content_db_path) as content_db: blocks = content_db.get_blocks() return ok({"blocks": blocks}) @router.get("/{blockId}", summary="获取单个 block") async def get_block( documentId: str, blockId: str, db: AsyncSession = Depends(get_db), ) -> dict: """获取指定 block 的详细信息""" svc = DocumentService(db) doc = await svc.get_document(documentId) with ContentDB(doc.content_db_path) as content_db: block = content_db.get_block_by_id(blockId) if not block: return {"code": 404, "message": f"Block not found: {blockId}"} return ok({"block": block}) @router.put("/{blockId}", summary="更新 block") async def update_block( documentId: str, blockId: str, body: BlockUpdate, db: AsyncSession = Depends(get_db), ) -> dict: """更新指定 block 的内容或样式""" svc = DocumentService(db) doc = await svc.get_document(documentId) # ★ 获取原始 block with ContentDB(doc.content_db_path) as content_db: original_block = content_db.get_block_by_id(blockId) if not original_block: return {"code": 404, "message": f"Block not found: {blockId}"} # ★ TOC block 验证 if original_block.get("type") == "toc": # TOC block 只允许更新 metadata(如删除标记) if body.content is not None: return { "code": 403, "message": "TOC block content is readonly and auto-generated" } # 只允许更新特定 metadata 字段 if body.metadata: allowed_keys = {"deletable"} if not set(body.metadata.keys()).issubset(allowed_keys): return { "code": 403, "message": f"TOC block only allows updating: {allowed_keys}" } # 更新 block updates = body.model_dump(exclude_none=True) with ContentDB(doc.content_db_path) as content_db: content_db.update_block(blockId, updates) # 更新文档时间戳 await svc.update_document_timestamp(documentId) return ok({"blockId": blockId, "message": "Block updated successfully"}) @router.delete("/{blockId}", summary="删除 block") async def delete_block( documentId: str, blockId: str, db: AsyncSession = Depends(get_db), ) -> dict: """删除指定 block""" svc = DocumentService(db) doc = await svc.get_document(documentId) with ContentDB(doc.content_db_path) as content_db: content_db.delete_block(blockId) # 更新文档时间戳 await svc.update_document_timestamp(documentId) return ok({"message": "Block deleted successfully"}) @router.get("/search", summary="搜索 blocks") async def search_blocks( documentId: str, q: str = Query(..., description="搜索关键词"), type: str = Query(None, description="Block 类型筛选"), db: AsyncSession = Depends(get_db), ) -> dict: """在文档中搜索包含关键词的 blocks""" svc = DocumentService(db) doc = await svc.get_document(documentId) with ContentDB(doc.content_db_path) as content_db: blocks = content_db.search_blocks(q, type) return ok({ "blocks": blocks, "total": len(blocks), "query": q }) @router.get("/toc", summary="获取目录树") async def get_toc( documentId: str, db: AsyncSession = Depends(get_db), ) -> dict: """获取文档的标题目录树""" svc = DocumentService(db) doc = await svc.get_document(documentId) with ContentDB(doc.content_db_path) as content_db: headings = content_db.get_headings() # 构建树形结构 toc = _build_toc_tree(headings) return ok({"toc": toc}) @router.get("/stats", summary="获取统计信息") async def get_stats( documentId: str, db: AsyncSession = Depends(get_db), ) -> dict: """获取文档的 block 统计信息""" svc = DocumentService(db) doc = await svc.get_document(documentId) with ContentDB(doc.content_db_path) as content_db: stats = content_db.get_stats() return ok(stats) def _build_toc_tree(headings: list[dict]) -> list[dict]: """构建目录树 Args: headings: 标题 block 列表 Returns: 树形结构的目录 """ root = [] stack = [] for h in headings: # 提取内容(可能是字符串或富文本数组) content = h["content"] if isinstance(content, list): # 富文本:拼接所有片段 content = "".join(seg.get("text", "") for seg in content) node = { "id": h["id"], "level": h["level"], "content": content, "children": [] } # 找到父节点 while stack and stack[-1]["level"] >= h["level"]: stack.pop() if stack: stack[-1]["children"].append(node) else: root.append(node) stack.append(node) return root