在 Axonix 平台中,添加类似豆包的富文本编辑器功能,服务于以下场景:
| 层次 | 技术 |
|---|---|
| 前端框架 | React 18 + TypeScript + Vite + Ant Design |
| 编辑器核心 | Slate.js |
| 状态管理 | Zustand |
| 后端框架 | FastAPI(Python 3.10+) |
| ORM | SQLAlchemy 2.0 |
| 数据库 | PostgreSQL |
阶段 0 打通基础链路(当前)
mod-chat 中 AI 生成文档 → 编辑器打开 → 编辑 → 保存回 Chat
约 1.5 周
阶段 1 Word 模板 + 工作流文档编辑(近期目标)
上传 Word 模板 → 工作流生成文档 → 编辑器打开编辑 → 下载保持模板格式
约 2 周
阶段 2 oil-agent 跨平台集成
编辑器微前端化 → oil-agent 免登录接入 → 跨平台数据同步
约 2 周
阶段 3 编辑器增强 + 协同编辑
富文本增强、版本管理、实时多人协同
约 4 周
跑通 mod-chat 中"AI 生成文档 → 编辑器打开 → 编辑 → 保存回 Chat"的完整闭环。
从 Chat 打开编辑器
基础编辑器
保存并下载
.doc)文档 API
用户在 Chat 中查看 AI 生成的文档
↓
点击"编辑"按钮
↓
POST /api/v1/documents 创建文档记录,获取 documentId
↓
打开编辑器 Modal,加载文档内容
↓
用户编辑 → 计算变动块(标题 level + index)→ 写入本地缓存
↓
判断文档大小
├── 小文件(< 200KB)→ 防抖 3s → PUT /api/v1/documents/{id}(局部块更新)
└── 大文件(≥ 200KB)→ 仅写 IndexedDB,不自动推后端
↓
点击"保存"按钮 → 弹出保存对话框
↓
用户填写文件名,选择格式(.doc)→ 确认
↓
POST /api/v1/export/doc 提交 userId、文件名、格式、文档内容
↓
后端:加载默认样式文件 → DocxRenderer 渲染 .doc → 写入 tmp/{userId}/{date}/ → 写入 export_records 表
↓
返回 { recordId, downloadUrl, fileName, styleId, warning }
↓
前端根据 downloadUrl 自动触发浏览器下载
┌─────────────────────────────────┐
│ 保存文档 │
├─────────────────────────────────┤
│ 文件名 [季度报告_2026Q2 ] │
│ 格式 [.doc ▼ ] │
│ (目前仅支持 .doc) │
├─────────────────────────────────┤
│ [取消] [确认下载] │
└─────────────────────────────────┘
mod-chat/src/
components/
editor/
DocumentEditor.tsx // 编辑器 Modal 主组件
EditorToolbar.tsx // 工具栏(含保存按钮)
EditorPreview.tsx // Markdown 分屏预览
SaveDialog.tsx // 保存对话框(文件名 + 格式选择)
store/
editorStore.ts // 编辑器 Zustand store
api/
documentApi.ts // 文档 API 封装
exportApi.ts // 导出下载 API 封装
backend/
app/
main.py // FastAPI 应用入口,注册路由
config.py // 环境变量配置(DB URL、TEMP_DIR、BASE_URL 等)
core/
database.py // AsyncSession 连接池
dependencies.py // get_db 依赖注入
exceptions.py // ExportError 等自定义异常 + 全局 handler
models/
document.py // documents 表 ORM 模型
export_record.py // export_records 表 ORM 模型
schemas/
document.py // DocumentCreate / Update / Response
export.py // ExportDocRequest / ExportDocResponse / ExportRecord(camelCase 别名)
services/
document_service.py // 文档 CRUD 业务逻辑
export_service.py // DocxRenderer、load_style_file()、markdown_to_docx_bytes()、文件写入
export_record_service.py // 下载记录写入、查询、硬删除(含磁盘文件同步删除)
storage_monitor.py // 磁盘配额检查、后台定时任务(每 30 分钟)、警告触发
api/
v1/
documents.py // 文档 CRUD 路由(5 个端点)
export.py // 导出路由(POST /export/doc)
export_records.py // 下载记录路由(列表、重新下载、删除)
migrations/
versions/
001_init_documents.py // 建 documents 表(Alembic)
002_add_export_records.py // 建 export_records 表
tmp/
default.json // 默认样式文件,阶段 0 固定使用
requirements.txt
.env.example
| 任务 | 预计工时 |
|---|---|
| 编辑器组件搭建(Slate.js 集成) | 3 天 |
| 文档 CRUD API(FastAPI + PostgreSQL) | 2 天 |
| 保存对话框 + 导出 .doc API | 2 天 |
| Chat 集成(打开编辑器) | 1 天 |
| 联调测试 | 1 天 |
| 合计 | 约 1.5 周 |
支持工作流上传 Word 模板,工作流根据模板生成文档后,用户可在编辑器中打开、修改,并下载,下载后的 DOCX 文件格式、字体、标题样式与原模板完全一致。
样式管理
styleId 指定使用的样式;不传则使用默认样式Word 模板管理
{{名称}} 占位符的 Word 模板(.docx)工作流生成文档
编辑器打开工作流文档
下载(保持模板格式)
{{占位符}} 替换内容,生成 DOCX 文件流核心技术:
python-docx:提取样式完整 XML(full_xml_definition)、注入样式到导出文档、解析模板占位符、生成最终 DOCXmistune 3.x:将 Markdown 解析为 AST token 流,由 DocxRenderer 渲染为 python-docx 元素docxtpl(可选):基于 Jinja2 模板语法填充变量,用于模板导出场景样式上传阶段:
上传任意 .docx/.doc → 遍历所有样式 → 提取完整 XML(full_xml_definition)→ 序列化为 JSON 存储
模板上传阶段:
上传带占位符的 .docx → 解析 {{名称}} 列表 → 存储原始模板文件 + 元数据
生成文档阶段:
工作流触发 → 基于模板填充内容 → 生成文档记录(内容存 DB,关联 templateId)
编辑器打开阶段:
加载文档内容 → 查询关联模板元数据 → 编辑器 UI 应用对应标题样式
下载阶段(样式导出):
读取编辑器最新内容 + 加载样式 JSON → inject_styles_from_json() 注入样式 → DocxRenderer 渲染 → 返回 .doc 字节流
下载阶段(模板导出):
读取编辑器最新内容 → 按标题拆分内容块 → 遍历占位符匹配标题名 → docxtpl 替换后返回 DOCX 文件流
// 样式文件缓存表(从上传的 Word 文档提取)
interface StyleFile {
id: string;
name: string; // 样式文件名(源文档文件名去扩展名)
sourceFile: string; // 原始上传文件名(含扩展名)
filePath: string; // 样式 JSON 存储路径
summary: {
defaultFont: string;
defaultSizePt: number;
headingFonts: string[];
totalStyles: number;
styleTypes: Record<string, number>;
};
isDefault: boolean;
createdAt: number;
}
// 模板表(带占位符的 Word 文档)
interface DocumentTemplate {
id: string;
name: string;
fileKey: string; // 模板文件存储路径
placeholders: string[]; // 占位符名称列表,如 ["一季度回顾", "风险提示"]
hasHeader: boolean;
hasFooter: boolean;
createdAt: number;
}
// documents 表新增字段
interface Document {
// ...原有字段
templateId?: string; // 关联模板 ID(工作流文档使用)
source: 'chat' | 'workflow';
}
【样式管理流程】
用户上传任意 Word 文档
↓
POST /api/v1/styles 后端提取全量样式 XML,生成 JSON,写入样式缓存列表
↓
返回 { styleId, name, summary, ... }
【模板管理流程】
工作流中上传带占位符的 Word 模板
↓
POST /api/v1/templates 后端解析 {{名称}} 列表,存储模板文件和元数据
↓
返回 { templateId, placeholders, ... }
【工作流文档编辑 + 下载】
工作流根据业务数据 + 模板生成文档内容,POST /api/v1/documents(携带 templateId)
↓
用户点击"在编辑器中打开"
↓
编辑器加载文档内容,应用模板标题层级样式
↓
用户修改内容 → 自动保存
↓
点击"下载" → POST /api/v1/export/docx
↓
后端:读取最新文档内容 → 按标题拆分内容块 → 遍历占位符匹配 → 返回 DOCX 文件流
↓
浏览器下载,文件格式与模板一致
【自定义样式导出流程】
用户在保存对话框选择已上传的样式(styleId)
↓
POST /api/v1/export/doc(携带 styleId)
↓
后端:加载对应样式 JSON → inject_styles_from_json() → DocxRenderer 渲染 → 返回 .doc
mod-chat/src/
components/
editor/
TemplateStyleProvider.tsx // 加载模板样式并注入 CSS 变量(新增)
EditorToolbar.tsx // 扩展:工具栏样式选项与模板标题级别对应
api/
templateApi.ts // 模板信息查询 API 封装(新增)
backend/app/
models/
document_template.py // document_templates 表 ORM 模型(新增)
style_file.py // style_files 表 ORM 模型(新增)
schemas/
template.py // TemplateCreate / TemplateResponse(新增)
style.py // StyleFileResponse / StyleFileSummary(新增)
services/
template_service.py // 模板上传、占位符解析、文件存储(新增)
style_service.py // 样式上传、全量 XML 提取、JSON 序列化(新增)
export_service.py // 扩展:新增 export_docx_with_template();导出 .doc 时支持 styleId 加载
api/
v1/
templates.py // 模板管理路由(新增:POST、GET、DELETE)
styles.py // 样式管理路由(新增:POST、GET、GET/{id}、DELETE/{id})
export.py // 扩展:新增 /export/docx 端点
| 任务 | 预计工时 |
|---|---|
| 模板上传 + 样式解析 API | 2 天 |
| 工作流文档生成 API | 1 天 |
| 编辑器应用模板样式(前端) | 2 天 |
| DOCX 下载导出(python-docx 合并) | 2 天 |
| 联调测试 | 1 天 |
| 合计 | 约 2 周 |
将编辑器组件微前端化,供 oil-agent 平台通过免登录 Token 接入,两个平台共用同一份文档数据。
编辑器微前端化
免登录 Token 机制
跨平台数据同步
oil-agent 请求编辑某文档
↓
调用后端 POST /api/v1/edit-sessions,生成一次性 Token
↓
返回编辑器 URL(含 Token)
↓
oil-agent 打开新窗口,编辑器凭 Token 加载文档
↓
用户编辑保存 → 后端 Webhook 通知 oil-agent
↓
oil-agent 拉取最新文档内容
mod-editor/src/ // 新建独立微前端包
components/
editor/
DocumentEditor.tsx // 从 mod-chat 迁移,支持 platform prop
EditorToolbar.tsx
EditorPreview.tsx
SaveDialog.tsx
transport/
EditorTransport.ts // SSE / WebSocket 通信适配层(新增)
store/
editorStore.ts
api/
documentApi.ts
exportApi.ts
editSessionApi.ts // Token 会话 API 封装(新增)
vite.config.ts // Module Federation 打包配置(新增)
backend/app/
core/
cache.py // Redis 连接池(新增)
models/
edit_session.py // edit_sessions 表 ORM 模型(新增)
schemas/
edit_session.py // CreateEditSessionRequest / Response(新增)
services/
token_service.py // JWT 生成/验证/撤销 + Redis 双重校验(新增)
webhook_service.py // 触发 Webhook、签名、重试(新增)
api/
v1/
edit_sessions.py // Token 会话路由(新增,3 个端点)
webhooks.py // Webhook 配置路由(新增)
| 任务 | 预计工时 |
|---|---|
| Module Federation 打包配置 | 1 天 |
| Token 服务(JWT + Redis) | 2 天 |
| 编辑会话 API | 1 天 |
| Webhook 通知 | 1 天 |
| oil-agent 接入联调 | 2 天 |
| 合计 | 约 2 周 |
提升编辑器的编辑体验,并在有需要时支持多人实时协同。
富文本增强
PDF 导出
版本管理
实时协同(可选,按需启用)
阶段 3 完整架构图见 技术架构文档 - 阶段 3,新增依赖:Yjs、y-websocket、S3/MinIO(大文档迁移)、API Gateway。
mod-editor/src/
components/
editor/
CollabCursors.tsx // 协同光标渲染(在线用户实时位置)(新增)
OnlineUserList.tsx // 在线用户列表展示(新增)
toolbar/
FormatToolbar.tsx // 完整富文本工具栏(H1-H6、代码块、表格等)(新增)
FindReplacePanel.tsx // 查找替换面板(新增)
version/
VersionHistoryPanel.tsx // 版本历史列表(新增)
VersionDiffView.tsx // 两版本对比 Diff 视图(新增)
plugins/
withCollaboration.ts // Slate + Yjs 协同插件(新增)
withHistory.ts // 撤销/重做扩展
withImages.ts // 图片上传插入插件(新增)
api/
versionApi.ts // 版本历史 API 封装(新增)
exportApi.ts // 扩展:新增 PDF 导出
backend/app/
models/
document_version.py // document_versions 表 ORM 模型(新增)
schemas/
version.py // VersionResponse / DiffResponse(新增)
services/
version_service.py // 版本快照、回滚、Diff 逻辑(新增)
collab_service.py // y-websocket 会话管理(新增)
export_service.py // 扩展:新增 export_pdf()
api/
v1/
versions.py // 版本管理路由(新增,3 个端点)
export.py // 扩展:新增 /export/pdf 端点
collab.py // WebSocket 协同端点(新增)
| 任务 | 预计工时 |
|---|---|
| 富文本工具栏增强 | 3 天 |
| PDF 导出 | 2 天 |
| 版本管理(DB 设计 + API + 前端) | 3 天 |
| 实时协同(Yjs + WebSocket) | 5 天 |
| 合计 | 约 3-4 周 |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 文档唯一 ID |
| title | string | 标题 |
| content | text | 文档内容(Markdown / JSON),上限 200KB |
| format | string | markdown(阶段 0/1);阶段 3 支持 json |
| template_id | string | 关联模板 ID,工作流文档必填(阶段 1 引入) |
| source | string | chat / workflow |
| session_id | string | 关联聊天会话 ID(可选) |
| created_by | string | 创建者 |
| created_at | timestamp | |
| updated_at | timestamp |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 记录唯一 ID |
| user_id | string | 所属用户 ID |
| file_name | string | 含扩展名的完整文件名 |
| file_path | string | 磁盘存储绝对路径 |
| file_size | integer | 文件大小(字节) |
| download_url | string | 永久下载链接 |
| document_id | string | 关联文档 ID(可空) |
| style_id | string | 使用的样式 ID,默认 "default" |
| created_at | timestamp |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 模板唯一 ID |
| name | string | 模板名称 |
| file_key | string | 模板文件存储路径 |
| placeholders | jsonb | 占位符名称列表,如 ["一季度回顾", "风险提示"] |
| has_header | boolean | 是否包含页眉 |
| has_footer | boolean | 是否包含页脚 |
| created_at | timestamp |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 样式文件唯一 ID |
| name | string | 样式文件名(源文档文件名去扩展名) |
| source_file | string | 原始上传文件名(含扩展名) |
| file_path | string | 样式 JSON 存储路径 |
| summary | jsonb | 摘要(默认字体、标题字体、样式总数等) |
| is_default | boolean | 是否为系统默认样式 |
| created_at | timestamp |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 会话 ID |
| document_id | string | 文档 ID |
| token_hash | string | Token 哈希(不存明文) |
| platform | string | mod-chat / oil-agent |
| status | string | active / closed |
| expires_at | timestamp |
| 接口 | 方法 | 引入阶段 | 说明 |
|---|---|---|---|
| /api/v1/documents | POST | 阶段 0 | 创建文档 |
| /api/v1/documents | GET | 阶段 0 | 获取文档列表 |
| /api/v1/documents/{id} | GET | 阶段 0 | 获取文档详情 |
| /api/v1/documents/{id} | PUT | 阶段 0 | 更新文档(全量或局部块) |
| /api/v1/documents/{id} | DELETE | 阶段 0 | 删除文档 |
| /api/v1/export/doc | POST | 阶段 0 | 导出 .doc,写入下载记录,返回永久下载链接 |
| /api/v1/export/records | GET | 阶段 0 | 获取下载记录列表 |
| /api/v1/export/records/{recordId}/download | GET | 阶段 0 | 重新下载文件 |
| /api/v1/export/records/{recordId} | DELETE | 阶段 0 | 删除下载记录(同步删除磁盘文件) |
| /api/v1/admin/storage | GET | 阶段 0 | 管理端:查看磁盘配额与各用户占用 |
| /api/v1/styles | POST | 阶段 1 | 上传 Word 文档,提取全量样式 XML,加入样式缓存列表 |
| /api/v1/styles | GET | 阶段 1 | 获取样式缓存列表(含系统默认样式) |
| /api/v1/styles/{styleId} | GET | 阶段 1 | 获取样式详情(含完整 XML 定义) |
| /api/v1/styles/{styleId} | DELETE | 阶段 1 | 删除自定义样式 |
| /api/v1/templates | POST | 阶段 1 | 上传带占位符的 Word 模板 |
| /api/v1/templates | GET | 阶段 1 | 获取模板列表 |
| /api/v1/templates/{id} | GET | 阶段 1 | 获取模板详情 |
| /api/v1/templates/{id} | DELETE | 阶段 1 | 删除模板 |
| /api/v1/export/docx | POST | 阶段 1 | 按模板导出 DOCX(占位符填充,保持模板格式) |
| /api/v1/edit-sessions | POST | 阶段 2 | 创建编辑会话,返回 Token 和编辑器 URL |
| /api/v1/edit-sessions/{id}/document | GET | 阶段 2 | 凭 Token 获取文档内容和权限信息 |
| /api/v1/edit-sessions/{id} | DELETE | 阶段 2 | 关闭会话,立即撤销 Token |
| /api/v1/webhooks | POST | 阶段 2 | 配置 Webhook(订阅 document.updated / session.closed) |
| /api/v1/export/pdf | POST | 阶段 3 | 导出 PDF |
| /api/v1/documents/{id}/versions | GET | 阶段 3 | 版本历史列表 |
| /api/v1/documents/{id}/versions/{vId}/restore | POST | 阶段 3 | 回滚版本 |
| /api/v1/documents/{id}/versions/diff | GET | 阶段 3 | 版本对比(Diff) |
以 200KB 为分界线:
| 文档大小 | 自动保存行为 |
|---|---|
| 小文件(< 200KB) | 防抖 3 秒后直接 PUT 到后端,本地不做额外缓存 |
| 大文件(≥ 200KB) | 写入前端 IndexedDB 缓存,不立即推后端;用户手动点"保存"时才同步到后端 |
| 存储类型 | 用途 | 说明 |
|---|---|---|
| Cookies | 小型临时状态 | 存储编辑器 UI 状态(当前 documentId、预览模式开关等),TTL 与会话一致 |
| localStorage | 小文件草稿块缓存 | 按标题块存储变更内容,key 格式见 9.3,浏览器关闭后仍保留 |
| IndexedDB | 大文件内容缓存 | 存储 ≥ 200KB 的文档内容,异步读写,不阻塞主线程 |
不管文档大小,每次编辑都按标题等级定位到具体块做局部更新,而不是全量覆盖整个文档。
块的定义:以标题节点(H1/H2/H3...)为分割点,每个标题及其下属正文构成一个块,用"第 N 个 K 级标题"来唯一定位。
文档结构示例:
# 第一章 ← H1[0]
## 1.1 背景 ← H2[0]
## 1.2 目标 ← H2[1]
# 第二章 ← H1[1]
## 2.1 方案 ← H2[2]
用户只修改了"1.2 目标"下的正文
↓
定位到块:{ level: 2, index: 1 } 即"第 2 个 H2 标题"
↓
只更新该块,其余块不变
本地存储 key 格式:
localStorage key: doc:{documentId}:h{level}:{index}
示例: doc:doc-abc123:h2:1
后端局部更新请求(见 API 文档 2.3):
{
"blocks": [
{
"level": 2,
"index": 1,
"content": "更新后的内容..."
}
]
}
用户编辑内容
↓
计算变动块(level + index)
↓
写入 localStorage(小文件)或 IndexedDB(大文件)
↓
判断文档大小
├── 小文件(< 200KB)
│ 防抖 3 秒 → PUT /api/v1/documents/{id}(携带变动块)
│ 后端更新成功 → 清除对应本地缓存块
│ 后端更新失败 → 保留缓存,下次打开时提示"有未同步的草稿"
│
└── 大文件(≥ 200KB)
只写 IndexedDB,不自动推后端
用户手动点"保存" → 全量或按块同步到后端
打开编辑器,传入 documentId
↓
检查 localStorage / IndexedDB 是否有本地缓存块
├── 有缓存 → 对比本地 updatedAt 与服务端 updatedAt
│ ├── 本地更新 → 提示"发现未同步的草稿,是否恢复?"
│ │ 用户确认 → 合并本地块覆盖服务端内容
│ │ 用户拒绝 → 丢弃本地缓存,加载服务端内容
│ └── 服务端更新 → 直接加载服务端内容,清除本地缓存
└── 无缓存 → 直接从服务端加载文档内容
| 风险 | 影响 | 应对措施 |
|---|---|---|
| Word 模板样式复杂,解析不完整 | 下载格式与模板不一致 | 阶段 1 先支持标题/正文/字体基础样式,复杂样式(表格边框、页眉图片等)逐步覆盖 |
| 编辑器富文本节点与 DOCX 样式映射困难 | 导出排版错乱 | 建立明确的节点类型 ↔ DOCX 样式映射表,作为开发规范 |
| 大文档(> 200KB)阶段 0 无法存储 | 工作流生成超长文档时失败 | 阶段 0 强制限制,阶段 1 引入文件存储后解除 |
| oil-agent Token 安全 | 数据泄露 | JWT + Redis 双重验证,Token 与文档 ID 绑定,默认 1 小时过期 |
| 协同编辑并发冲突 | 数据不一致 | 引入成熟 CRDT 库(Yjs),阶段 3 单独评估 |
文档版本: v5.0 最后更新: 2026-06-18 维护者: Axonix 前端团队