blocks.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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. updates = body.model_dump(exclude_none=True)
  54. with ContentDB(doc.content_db_path) as content_db:
  55. content_db.update_block(blockId, updates)
  56. # 更新文档时间戳
  57. await svc.update_document_timestamp(documentId)
  58. return ok({"blockId": blockId, "message": "Block updated successfully"})
  59. @router.delete("/{blockId}", summary="删除 block")
  60. async def delete_block(
  61. documentId: str,
  62. blockId: str,
  63. db: AsyncSession = Depends(get_db),
  64. ) -> dict:
  65. """删除指定 block"""
  66. svc = DocumentService(db)
  67. doc = await svc.get_document(documentId)
  68. with ContentDB(doc.content_db_path) as content_db:
  69. content_db.delete_block(blockId)
  70. # 更新文档时间戳
  71. await svc.update_document_timestamp(documentId)
  72. return ok({"message": "Block deleted successfully"})
  73. @router.get("/search", summary="搜索 blocks")
  74. async def search_blocks(
  75. documentId: str,
  76. q: str = Query(..., description="搜索关键词"),
  77. type: str = Query(None, description="Block 类型筛选"),
  78. db: AsyncSession = Depends(get_db),
  79. ) -> dict:
  80. """在文档中搜索包含关键词的 blocks"""
  81. svc = DocumentService(db)
  82. doc = await svc.get_document(documentId)
  83. with ContentDB(doc.content_db_path) as content_db:
  84. blocks = content_db.search_blocks(q, type)
  85. return ok({
  86. "blocks": blocks,
  87. "total": len(blocks),
  88. "query": q
  89. })
  90. @router.get("/toc", summary="获取目录树")
  91. async def get_toc(
  92. documentId: str,
  93. db: AsyncSession = Depends(get_db),
  94. ) -> dict:
  95. """获取文档的标题目录树"""
  96. svc = DocumentService(db)
  97. doc = await svc.get_document(documentId)
  98. with ContentDB(doc.content_db_path) as content_db:
  99. headings = content_db.get_headings()
  100. # 构建树形结构
  101. toc = _build_toc_tree(headings)
  102. return ok({"toc": toc})
  103. @router.get("/stats", summary="获取统计信息")
  104. async def get_stats(
  105. documentId: str,
  106. db: AsyncSession = Depends(get_db),
  107. ) -> dict:
  108. """获取文档的 block 统计信息"""
  109. svc = DocumentService(db)
  110. doc = await svc.get_document(documentId)
  111. with ContentDB(doc.content_db_path) as content_db:
  112. stats = content_db.get_stats()
  113. return ok(stats)
  114. def _build_toc_tree(headings: list[dict]) -> list[dict]:
  115. """构建目录树
  116. Args:
  117. headings: 标题 block 列表
  118. Returns:
  119. 树形结构的目录
  120. """
  121. root = []
  122. stack = []
  123. for h in headings:
  124. # 提取内容(可能是字符串或富文本数组)
  125. content = h["content"]
  126. if isinstance(content, list):
  127. # 富文本:拼接所有片段
  128. content = "".join(seg.get("text", "") for seg in content)
  129. node = {
  130. "id": h["id"],
  131. "level": h["level"],
  132. "content": content,
  133. "children": []
  134. }
  135. # 找到父节点
  136. while stack and stack[-1]["level"] >= h["level"]:
  137. stack.pop()
  138. if stack:
  139. stack[-1]["children"].append(node)
  140. else:
  141. root.append(node)
  142. stack.append(node)
  143. return root