| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- """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.post("", summary="插入新 block")
- async def create_block(
- documentId: str,
- body: BlockCreate,
- db: AsyncSession = Depends(get_db),
- ) -> dict:
- """在指定位置插入新 block(type, content, level, style, after_block_id,返回 blockId)"""
- # 1. 参数校验
- if body.type == 'heading':
- if body.level not in range(1, 7):
- return {"code": 400, "message": "heading 的 level 必须是 1-6"}
- else:
- # 其他类型强制设置为 0
- body.level = 0
-
- # 2. 获取文档
- svc = DocumentService(db)
- doc = await svc.get_document(documentId)
-
- try:
- with ContentDB(doc.content_db_path) as content_db:
- # 3. 计算 index(同类型同级别的稀疏序号)
- new_index = content_db.calculate_index_for_insert(
- body.after_block_id,
- body.type,
- body.level
- )
-
- # 4. 计算 block_order(文档全局位置)
- new_block_order = content_db.calculate_block_order_for_insert(
- body.after_block_id
- )
-
- # 5. 生成 Block ID
- new_id = ContentDB.generate_block_id(body.type, body.level, new_index)
-
- # 6. 构建新 block
- new_block = {
- 'id': new_id,
- 'block_order': new_block_order,
- 'type': body.type,
- 'level': body.level,
- 'index': new_index,
- 'content': body.content,
- 'word_style': body.word_style,
- 'style': body.style,
- 'metadata': body.metadata
- }
-
- # 7. 插入数据库
- content_db.insert_blocks([new_block])
- except ValueError as e:
- return {"code": 404, "message": str(e)}
- except Exception as e:
- return {"code": 500, "message": f"插入失败: {str(e)}"}
-
- # 8. 更新文档时间戳
- await svc.update_document_timestamp(documentId)
-
- return ok({"blockId": new_id, "message": "Block created successfully"})
- @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]:
- """构建目录树(headings 列表 -> 树形结构)"""
- 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
|