| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- """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
|