"""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 Args: documentId: 文档 ID body: Block 创建请求 - type: Block 类型(heading/paragraph/table/image/toc) - content: Block 内容 - level: 标题级别(heading 必填:1-6,其他类型固定为 0) - word_style: Word 样式名(可选) - style: 自定义样式(可选) - metadata: 元数据(可选) - after_block_id: 插入位置(null = 末尾) Returns: 包含新 block ID 的响应 """ # 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]: """构建目录树 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