INSERT_BLOCK_COMPLETE_GUIDE.md 22 KB

插入 Block 功能完整指南

状态:✅ 已完成并验收通过
实现日期:2026-07-10
版本:v1.0


📋 目录


1. 快速开始

🚀 最简单的使用方式

API 端点

POST /api/v1/documents/{documentId}/blocks

插入段落(最简单)

// 在某个 block 后插入段落
await fetch(`/api/v1/documents/${docId}/blocks`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    type: 'paragraph',
    content: '这是新段落内容',
    after_block_id: 'block-p-100'
  })
});

插入标题(必须指定 level)

// 插入 H2 标题
await fetch(`/api/v1/documents/${docId}/blocks`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    type: 'heading',
    level: 2,  // ⚠️ 必填:1-6
    content: '第二章 项目背景',
    word_style: 'Heading 2',
    after_block_id: 'block-h1-0'
  })
});

⚠️ 重要规则

规则 说明
必填字段 typecontent 必须提供
level 参数 heading 必填(1-6),其他类型可省略(默认 0)
不要提供 idindexblock_order 由后端自动生成
📍 插入位置 after_block_id 指定插入位置,null = 末尾

2. API 参考

📌 API 端点

POST /api/v1/documents/{documentId}/blocks

路径参数

参数 类型 必填 说明 示例
documentId string 文档 ID "doc-abc123456789"

📝 请求参数

前端需要提供的参数

参数 类型 必填 默认值 说明
type string - Block 类型:heading, paragraph, table, image, toc
content string/dict/list - Block 内容(格式因类型而异)
level int 0 heading 必填(1-6),其他类型固定为 0
word_style string "" Word 样式名(如 "Heading 1", "Normal")
style dict {} 自定义样式 JSON
metadata dict {} 元数据(如 parent_heading_id
after_block_id string/null null 在哪个 block 后插入(null = 末尾)

⚠️ 重要idindexblock_order 由后端自动生成,前端不需要不应该提供!


🎯 level 参数详解

必填条件

Block 类型 level 取值 是否必填 说明
heading 1 - 6 ✅ 必填 对应 H1 - H6 标题
paragraph 0 ❌ 可省略 固定值(默认 0)
table 0 ❌ 可省略 固定值(默认 0)
image 0 ❌ 可省略 固定值(默认 0)
toc 0 ❌ 可省略 固定值(默认 0)

示例

// ✅ 正确:heading 类型必须指定 level
{
  "type": "heading",
  "level": 2,
  "content": "第一节"
}

// ✅ 正确:paragraph 类型可以省略 level
{
  "type": "paragraph",
  "content": "段落内容"
}

// ❌ 错误:heading 类型未指定 level
{
  "type": "heading",
  "content": "标题"
}

📊 响应格式

✅ 成功响应

{
  "code": 0,
  "message": "success",
  "data": {
    "blockId": "block-p-250",
    "message": "Block created successfully"
  }
}

❌ 错误响应

heading 未指定 level

{
  "code": 400,
  "message": "heading 的 level 必须是 1-6"
}

after_block_id 不存在

{
  "code": 404,
  "message": "Block not found: block-p-999"
}

📦 content 格式详解

1. 标题和段落 (heading / paragraph)

格式 A:纯文本字符串

{
  "type": "paragraph",
  "content": "这是一段普通文本"
}

格式 B:富文本数组

{
  "type": "heading",
  "content": [
    {"text": "第一章 ", "style": {}},
    {"text": "重要通知", "style": {"bold": true, "color": "FF0000"}}
  ]
}

2. 表格 (table)

{
  "type": "table",
  "content": {
    "rows": [
      {
        "cells": [
          {"text": "姓名", "rowspan": 1, "colspan": 1, "col_index": 0, "style": {"bold": true}},
          {"text": "年龄", "rowspan": 1, "colspan": 1, "col_index": 1, "style": {}}
        ]
      }
    ]
  }
}

3. 图片 (image)

{
  "type": "image",
  "content": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
}

🎨 样式属性参考

段落/标题级样式(style 字段)

属性 类型 说明 示例
font_name string 字体名称 "微软雅黑"
font_size number 字号(磅) 14.0
bold boolean 是否加粗 true
italic boolean 是否斜体 true
underline boolean 是否下划线 true
strike boolean 是否删除线 true
color string 文字颜色(十六进制) "FF0000"
align string 对齐方式 "left", "center", "right", "justify"

图片样式

属性 类型 说明 示例
width number 宽度值 10.0
height number 高度值 7.0
unit string 单位 "cm", "inch"
align string 对齐方式 "left", "center", "right"

🔧 常用 Word 样式名

// 标题
word_style: 'Heading 1'     // H1
word_style: 'Heading 2'     // H2
word_style: 'Heading 3'     // H3

// 段落
word_style: 'Normal'        // 正文
word_style: 'Body Text'     // 正文

// 表格
word_style: 'Table Grid'    // 表格网格

📚 完整示例

示例 1:插入普通段落

POST /api/v1/documents/doc-abc123/blocks

{
  "type": "paragraph",
  "content": "这是新段落",
  "after_block_id": "block-p-200"
}

示例 2:插入 H2 标题

POST /api/v1/documents/doc-abc123/blocks

{
  "type": "heading",
  "level": 2,
  "content": "1.2 项目背景",
  "word_style": "Heading 2",
  "metadata": {
    "parent_heading_id": "block-h1-0"
  },
  "after_block_id": "block-h2-0"
}

示例 3:插入富文本段落

POST /api/v1/documents/doc-abc123/blocks

{
  "type": "paragraph",
  "content": [
    {"text": "项目位于 ", "style": {}},
    {"text": "陕西省延安市", "style": {"bold": true, "color": "FF0000"}},
    {"text": ",总投资 ", "style": {}},
    {"text": "5000 万元", "style": {"bold": true, "color": "0000FF"}}
  ],
  "word_style": "Normal",
  "after_block_id": "block-p-100"
}

示例 4:插入到文档末尾

POST /api/v1/documents/doc-abc123/blocks

{
  "type": "paragraph",
  "content": "文档结尾的段落",
  "after_block_id": null
}

示例 5:插入表格

POST /api/v1/documents/doc-abc123/blocks

{
  "type": "table",
  "content": {
    "rows": [
      {
        "cells": [
          {"text": "姓名", "rowspan": 1, "colspan": 1, "col_index": 0, "style": {"bold": true}},
          {"text": "年龄", "rowspan": 1, "colspan": 1, "col_index": 1, "style": {"bold": true}}
        ]
      },
      {
        "cells": [
          {"text": "张三", "rowspan": 1, "colspan": 1, "col_index": 0, "style": {}},
          {"text": "30", "rowspan": 1, "colspan": 1, "col_index": 1, "style": {}}
        ]
      }
    ]
  },
  "word_style": "Table Grid",
  "metadata": {
    "cols": 2,
    "rows": 2
  },
  "after_block_id": "block-p-300"
}

3. 实现详解

🔍 核心概念

后端自动生成的字段

1. id - Block ID

生成规则:根据类型、级别和 index 自动生成

类型 ID 格式 示例
标题 block-h{level}-{index} block-h2-100
段落 block-p-{index} block-p-200
表格 block-table-{index} block-table-0
图片 block-img-{index} block-img-50
目录 block-toc-{index} block-toc-0

2. index - 同类型同级别序号

说明:用于区分同类型同级别的 blocks,采用稀疏排序

计算方式:

  1. 查找前后两个同类型同级别的 block
  2. 计算它们的 index 中间值
  3. 如果没有后续 block,则追加(最大值 + 100)

不同类型独立计数:

H1 标题:index = 0, 100, 200...(独立)
H2 标题:index = 0, 50, 100, 200...(独立)
段落:   index = 0, 100, 200...(独立)

3. block_order - 文档全局位置

说明:决定 blocks 在文档中的显示顺序(所有类型共享同一序列)

计算方式:

  1. 根据 after_block_id 查询前一个 block 的 block_order
  2. 查询下一个 block 的 block_order
  3. 计算中间值
  4. 如果间隙不足(≤1),触发局部重排

🔑 关键区别:index vs block_order

字段 作用域 用途 稀疏排序方式
index 局部(同类型同级别) 生成 Block ID,区分同类 blocks 在前后同类 block 的 index 之间插入
block_order 全局(所有 blocks) 决定文档显示顺序 在前后任意 block 的 block_order 之间插入

📐 稀疏排序详解

基本原理

传统排序的问题:

初始:1, 2, 3, 4, 5
在 2 后插入:需要更新 3, 4, 5 → 4, 5, 6  ❌ O(n) 操作

稀疏排序的优势:

初始:100, 200, 300, 400, 500
在 200 后插入:250                      ✅ O(1) 操作
结果:100, 200, 250(新), 300, 400, 500

完整插入示例

初始文档结构

┌─────────────────────┬──────────┬────────────┐
│ Block               │ index    │ block_order│
├─────────────────────┼──────────┼────────────┤
│ H1: 第一章          │ 0        │ 100        │
│ H2: 1.1 概述        │ 0        │ 200        │
│ 段落1               │ 0        │ 300        │
│ H2: 1.2 背景        │ 100      │ 400        │
│ 段落2               │ 100      │ 500        │
│ H1: 第二章          │ 100      │ 600        │
└─────────────────────┴──────────┴────────────┘

场景:在段落1后插入 H2 标题

前端请求:

{
  "type": "heading",
  "level": 2,
  "content": "1.1.1 新增内容",
  "word_style": "Heading 2",
  "after_block_id": "block-p-0"
}

后端计算过程:

步骤 1:计算 index

# 查找前后两个 H2 标题
prev_h2_index = 0    # H2: 1.1 概述
next_h2_index = 100  # H2: 1.2 背景

# 计算中间值
new_index = (0 + 100) // 2  # = 50 ✅

步骤 2:计算 block_order

# 查找前后两个 block
after_order = 300   # 段落1
next_order = 400    # H2: 1.2 背景

# 计算中间值
new_block_order = (300 + 400) // 2  # = 350 ✅

步骤 3:生成 ID

new_id = f"block-h{level}-{index}"  # = "block-h2-50"

插入后的文档结构:

┌─────────────────────┬──────────┬────────────┐
│ Block               │ index    │ block_order│
├─────────────────────┼──────────┼────────────┤
│ H1: 第一章          │ 0        │ 100        │
│ H2: 1.1 概述        │ 0        │ 200        │
│ 段落1               │ 0        │ 300        │
│ H2: 新增内容 🆕     │ 50       │ 350        │  ← 新插入
│ H2: 1.2 背景        │ 100      │ 400        │
│ 段落2               │ 100      │ 500        │
│ H1: 第二章          │ 100      │ 600        │
└─────────────────────┴──────────┴────────────┘

关键点:

  • index = 50:在两个 H2 的 index(0, 100)之间
  • block_order = 350:在两个 block 的 order(300, 400)之间
  • id = "block-h2-50":根据规则自动生成
  • 无需更新其他 blocks:稀疏排序的核心优势!

🔧 实现代码

后端计算 index

def calculate_index_for_insert(
    db: ContentDB,
    after_block_id: str,
    new_type: str,
    new_level: int = 0
) -> int:
    """计算插入 block 的 index(稀疏排序)"""
    
    # 1. 获取 after_block 的 block_order
    after_block = db.get_block_by_id(after_block_id)
    after_order = after_block['block_order']
    
    # 2. 查找下一个同类型同级别的 block
    if new_type == 'heading':
        cursor = db.conn.execute("""
            SELECT index FROM document_blocks
            WHERE type = ? AND level = ? AND block_order > ?
            ORDER BY block_order LIMIT 1
        """, (new_type, new_level, after_order))
    else:
        cursor = db.conn.execute("""
            SELECT index FROM document_blocks
            WHERE type = ? AND block_order > ?
            ORDER BY block_order LIMIT 1
        """, (new_type, after_order))
    
    next_row = cursor.fetchone()
    
    if next_row:
        next_index = next_row['index']
        # 查找前一个同类型同级别的 block
        # ... 省略详细代码
        new_index = (prev_index + next_index) // 2
    else:
        # 没有下一个同类 block,追加到最后
        max_index = db.get_max_index(new_type, new_level)
        new_index = max_index + 100
    
    return new_index

后端计算 block_order

def calculate_block_order_for_insert(
    db: ContentDB,
    after_block_id: str = None
) -> int:
    """计算插入 block 的 block_order(稀疏排序)"""
    
    if after_block_id:
        after_block = db.get_block_by_id(after_block_id)
        after_order = after_block['block_order']
        
        # 查询下一个 block
        cursor = db.conn.execute("""
            SELECT block_order FROM document_blocks
            WHERE block_order > ?
            ORDER BY block_order LIMIT 1
        """, (after_order,))
        
        next_row = cursor.fetchone()
        
        if next_row:
            next_order = next_row['block_order']
            return (after_order + next_order) // 2
        else:
            return after_order + 100
    else:
        # 插入到文档末尾
        max_order = db.get_max_block_order()
        return max_order + 100

4. 前端集成

💡 React Hook 示例

import { useState } from 'react';

interface BlockCreateRequest {
  type: 'heading' | 'paragraph' | 'table' | 'image' | 'toc';
  level?: number;
  content: string | any[] | any;
  word_style?: string;
  style?: any;
  metadata?: any;
  after_block_id?: string | null;
}

export function useBlockInsert(documentId: string) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const insertBlock = async (block: BlockCreateRequest) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(`/api/v1/documents/${documentId}/blocks`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(block)
      });
      
      const data = await response.json();
      
      if (data.code !== 0) {
        throw new Error(data.message);
      }
      
      return data.data.blockId;
    } catch (err: any) {
      setError(err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  };

  return { insertBlock, loading, error };
}

// 使用示例
function MyEditor() {
  const { insertBlock, loading, error } = useBlockInsert('doc-abc123');
  
  const handleInsertParagraph = async () => {
    const blockId = await insertBlock({
      type: 'paragraph',
      content: '新段落',
      after_block_id: 'block-p-100'
    });
    console.log('插入成功:', blockId);
  };
  
  const handleInsertHeading = async () => {
    const blockId = await insertBlock({
      type: 'heading',
      level: 2,  // ⚠️ 必填
      content: '第二章',
      word_style: 'Heading 2',
      after_block_id: 'block-h1-0'
    });
    console.log('插入成功:', blockId);
  };
  
  return (
    <div>
      <button onClick={handleInsertParagraph} disabled={loading}>
        插入段落
      </button>
      <button onClick={handleInsertHeading} disabled={loading}>
        插入标题
      </button>
      {error && <div style={{color: 'red'}}>{error}</div>}
    </div>
  );
}

💡 Vue 3 Composable 示例

import { ref } from 'vue';

export function useBlockInsert(documentId: string) {
  const loading = ref(false);
  const error = ref<string | null>(null);

  const insertBlock = async (block: any) => {
    loading.value = true;
    error.value = null;
    
    try {
      const response = await fetch(`/api/v1/documents/${documentId}/blocks`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(block)
      });
      
      const data = await response.json();
      
      if (data.code !== 0) {
        throw new Error(data.message);
      }
      
      return data.data.blockId;
    } catch (err: any) {
      error.value = err.message;
      throw err;
    } finally {
      loading.value = false;
    }
  };

  return { insertBlock, loading, error };
}

🐛 调试技巧

1. 检查响应

const response = await fetch(...);
const data = await response.json();
console.log('API 响应:', data);

2. 验证请求体

const requestBody = {
  type: 'heading',
  level: 2,
  content: '标题'
};
console.log('请求体:', JSON.stringify(requestBody, null, 2));

3. 常见错误

错误信息 原因 解决方法
heading 的 level 必须是 1-6 heading 未指定 level 或超出范围 添加 level: 2(1-6)
Block not found: xxx after_block_id 不存在 检查 block ID 是否正确
插入失败: ... 其他错误 查看详细错误信息

5. 测试验证

🧪 测试场景

测试文件:test_insert_block.py

运行测试:

python test_insert_block.py

测试结果

✅ 场景 1:在段落1后插入 H2 标题

测试数据:

{
  "type": "heading",
  "level": 2,
  "content": "1.1.1 新增内容",
  "after_block_id": "block-p-0"
}

计算结果:

  • index: 50 ✅(在 0 和 100 之间)
  • block_order: 350 ✅(在 300 和 400 之间)
  • ID: block-h2-50

✅ 场景 2:在新 H2 后插入段落

测试数据:

{
  "type": "paragraph",
  "content": "新段落内容",
  "after_block_id": "block-h2-50"
}

计算结果:

  • index: 50 ✅(段落独立计数)
  • block_order: 375 ✅(在 350 和 400 之间)
  • ID: block-p-50

✅ 场景 3:插入到文档末尾

测试数据:

{
  "type": "paragraph",
  "content": "文档末尾段落",
  "after_block_id": null
}

计算结果:

  • index: 200 ✅(最大值 + 100)
  • block_order: 700 ✅(最大值 + 100)
  • ID: block-p-200

测试统计

✅ 所有测试通过!
📈 统计信息:
  - 总 blocks: 9
  - 按类型:
    - heading: 5
    - paragraph: 4

6. 实现总结

✅ 实现清单

1. 后端核心方法(app/services/content_db.py

  • calculate_index_for_insert() - 计算 index
  • calculate_block_order_for_insert() - 计算 block_order
  • _rebalance_indexes_between() - 局部重排 index
  • _rebalance_block_orders_between() - 局部重排 block_order
  • generate_block_id() - 生成 Block ID

2. API 端点(app/api/v1/blocks.py

  • POST /api/v1/documents/{documentId}/blocks
    • 参数校验
    • heading level 强制校验(1-6)
    • 其他类型 level 自动设为 0
    • 自动计算 index、block_order
    • 自动生成 ID
    • 错误处理

3. Schema 定义(app/schemas/block.py

  • BlockCreate schema
  • level 参数注释

4. 文档

  • 完整 API 文档
  • 前端快速参考
  • 实现总结
  • 测试验证

🎯 核心特性

1. 稀疏排序

  • ⚡ O(1) 插入性能
  • 📈 支持大文档(10000+ blocks)
  • 🔄 自动局部重排

2. 自动生成

  • 🆔 Block ID 自动生成
  • 📊 index 自动计算
  • 📍 block_order 自动计算

3. 前端简单

  • 📝 只需提供:type + content + after_block_id
  • 🚫 不需要:id + index + block_order
  • ✅ 透明化:无需关心稀疏排序

4. 参数校验

  • ✅ heading 必须指定 level(1-6)
  • ✅ 其他类型 level 自动为 0
  • ✅ 完善的错误处理

📊 设计优势

优势 说明
性能 O(1) 插入,无需更新其他 blocks
简单 前端只需关注内容和位置
可扩展 支持频繁插入和大文档
一致性 后端统一管理序号

📂 修改的文件

新增文件

  • INSERT_BLOCK_COMPLETE_GUIDE.md - 本文档
  • test_insert_block.py - 测试脚本

修改文件

  • app/services/content_db.py
  • app/api/v1/blocks.py
  • app/schemas/block.py

✨ 小贴士

  1. 💡 after_block_id = null 表示插入到文档末尾
  2. 💡 只有 heading 类型需要指定 level(1-6)
  3. 💡 不要提供 id、index、block_order
  4. 💡 content 可以是字符串或数组(富文本)
  5. 💡 始终检查 code !== 0 判断是否成功

🚀 立即可用

前端现在可以直接调用 API 进行 Block 插入操作!

最简单的调用:

await fetch(`/api/v1/documents/${docId}/blocks`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    type: 'paragraph',
    content: '新段落',
    after_block_id: 'block-p-100'
  })
});

实现者:AI Assistant (Kiro)
实现日期:2026-07-10
版本:v1.0
状态:✅ 已完成并验收通过