blocks.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. """blocks.py — Blocks 操作 API"""
  2. from fastapi import APIRouter, Depends, Query
  3. from sqlalchemy.ext.asyncio import AsyncSession
  4. from app.api.v1 import ok
  5. from app.core.dependencies import get_db
  6. from app.schemas.block import (
  7. BlockBase,
  8. BlockCreate,
  9. BlockSearchResult,
  10. BlockStats,
  11. BlockUpdate,
  12. TOCItem,
  13. )
  14. from app.services.content_db import ContentDB
  15. from app.services.document_service import DocumentService
  16. router = APIRouter(prefix="/documents/{documentId}/blocks", tags=["Blocks"])
  17. @router.get("", summary="获取文档的所有 blocks")
  18. async def get_blocks(
  19. documentId: str,
  20. db: AsyncSession = Depends(get_db),
  21. ) -> dict:
  22. """获取文档的所有 blocks,按 block_order 排序"""
  23. svc = DocumentService(db)
  24. doc = await svc.get_document(documentId)
  25. with ContentDB(doc.content_db_path) as content_db:
  26. blocks = content_db.get_blocks()
  27. return ok({"blocks": blocks})
  28. @router.get("/{blockId}", summary="获取单个 block")
  29. async def get_block(
  30. documentId: str,
  31. blockId: str,
  32. db: AsyncSession = Depends(get_db),
  33. ) -> dict:
  34. """获取指定 block 的详细信息"""
  35. svc = DocumentService(db)
  36. doc = await svc.get_document(documentId)
  37. with ContentDB(doc.content_db_path) as content_db:
  38. block = content_db.get_block_by_id(blockId)
  39. if not block:
  40. return {"code": 404, "message": f"Block not found: {blockId}"}
  41. return ok({"block": block})
  42. @router.put("/{blockId}", summary="更新 block")
  43. async def update_block(
  44. documentId: str,
  45. blockId: str,
  46. body: BlockUpdate,
  47. db: AsyncSession = Depends(get_db),
  48. ) -> dict:
  49. """更新指定 block 的内容或样式"""
  50. svc = DocumentService(db)
  51. doc = await svc.get_document(documentId)
  52. # ★ 获取原始 block
  53. with ContentDB(doc.content_db_path) as content_db:
  54. original_block = content_db.get_block_by_id(blockId)
  55. if not original_block:
  56. return {"code": 404, "message": f"Block not found: {blockId}"}
  57. # ★ TOC block 验证
  58. if original_block.get("type") == "toc":
  59. # TOC block 只允许更新 metadata(如删除标记)
  60. if body.content is not None:
  61. return {
  62. "code": 403,
  63. "message": "TOC block content is readonly and auto-generated"
  64. }
  65. # 只允许更新特定 metadata 字段
  66. if body.metadata:
  67. allowed_keys = {"deletable"}
  68. if not set(body.metadata.keys()).issubset(allowed_keys):
  69. return {
  70. "code": 403,
  71. "message": f"TOC block only allows updating: {allowed_keys}"
  72. }
  73. # 更新 block
  74. updates = body.model_dump(exclude_none=True)
  75. with ContentDB(doc.content_db_path) as content_db:
  76. content_db.update_block(blockId, updates)
  77. # 更新文档时间戳
  78. await svc.update_document_timestamp(documentId)
  79. return ok({"blockId": blockId, "message": "Block updated successfully"})
  80. @router.delete("/{blockId}", summary="删除 block")
  81. async def delete_block(
  82. documentId: str,
  83. blockId: str,
  84. db: AsyncSession = Depends(get_db),
  85. ) -> dict:
  86. """删除指定 block"""
  87. svc = DocumentService(db)
  88. doc = await svc.get_document(documentId)
  89. with ContentDB(doc.content_db_path) as content_db:
  90. content_db.delete_block(blockId)
  91. # 更新文档时间戳
  92. await svc.update_document_timestamp(documentId)
  93. return ok({"message": "Block deleted successfully"})
  94. @router.get("/search", summary="搜索 blocks")
  95. async def search_blocks(
  96. documentId: str,
  97. q: str = Query(..., description="搜索关键词"),
  98. type: str = Query(None, description="Block 类型筛选"),
  99. db: AsyncSession = Depends(get_db),
  100. ) -> dict:
  101. """在文档中搜索包含关键词的 blocks"""
  102. svc = DocumentService(db)
  103. doc = await svc.get_document(documentId)
  104. with ContentDB(doc.content_db_path) as content_db:
  105. blocks = content_db.search_blocks(q, type)
  106. return ok({
  107. "blocks": blocks,
  108. "total": len(blocks),
  109. "query": q
  110. })
  111. @router.get("/toc", summary="获取目录树")
  112. async def get_toc(
  113. documentId: str,
  114. db: AsyncSession = Depends(get_db),
  115. ) -> dict:
  116. """获取文档的标题目录树"""
  117. svc = DocumentService(db)
  118. doc = await svc.get_document(documentId)
  119. with ContentDB(doc.content_db_path) as content_db:
  120. headings = content_db.get_headings()
  121. # 构建树形结构
  122. toc = _build_toc_tree(headings)
  123. return ok({"toc": toc})
  124. @router.get("/stats", summary="获取统计信息")
  125. async def get_stats(
  126. documentId: str,
  127. db: AsyncSession = Depends(get_db),
  128. ) -> dict:
  129. """获取文档的 block 统计信息"""
  130. svc = DocumentService(db)
  131. doc = await svc.get_document(documentId)
  132. with ContentDB(doc.content_db_path) as content_db:
  133. stats = content_db.get_stats()
  134. return ok(stats)
  135. def _build_toc_tree(headings: list[dict]) -> list[dict]:
  136. """构建目录树
  137. Args:
  138. headings: 标题 block 列表
  139. Returns:
  140. 树形结构的目录
  141. """
  142. root = []
  143. stack = []
  144. for h in headings:
  145. # 提取内容(可能是字符串或富文本数组)
  146. content = h["content"]
  147. if isinstance(content, list):
  148. # 富文本:拼接所有片段
  149. content = "".join(seg.get("text", "") for seg in content)
  150. node = {
  151. "id": h["id"],
  152. "level": h["level"],
  153. "content": content,
  154. "children": []
  155. }
  156. # 找到父节点
  157. while stack and stack[-1]["level"] >= h["level"]:
  158. stack.pop()
  159. if stack:
  160. stack[-1]["children"].append(node)
  161. else:
  162. root.append(node)
  163. stack.append(node)
  164. return root