# 文本编辑器后端 API 详细文档 ## 1. 概述 ### 1.1 基本信息 - **Base URL**: `https://api.axonix.com` - **API 版本**: `v1` - **协议**: HTTPS - **认证方式**: 复用宿主应用 Auth(阶段 0/1);阶段 2 引入 Bearer Token (JWT) - **响应格式**: JSON ### 1.2 分阶段 API 范围 | 阶段 | 新增 API | |------|---------| | 阶段 0 | 文档 CRUD、导出 .doc(默认样式) | | 阶段 1 | 样式管理、模板管理、导出 DOCX | | 阶段 2 | 编辑会话(Token)、Webhook | | 阶段 3 | PDF 导出、版本管理 | ### 1.3 样式与模板的区别 这是两个独立功能,用户可以按需使用其中一个或两个都不用: | | 样式(Style) | 模板(Template) | |---|---|---| | 用途 | 控制输出文件的字体、颜色、标题层级等视觉格式 | 定义文档结构,内容填入占位符位置 | | 上传内容 | 任意 Word 文档,后端提取其中的样式文件 | 带占位符(`{{名称}}`)的 Word 文档 | | 导出方式 | 导出 .doc 时选择样式,不选用系统默认 | 导出 DOCX 时指定模板,内容按标题匹配占位符 | | 是否必须 | 否,有默认样式兜底 | 否,不使用模板则走样式导出流程 | ### 1.4 通用响应格式 ```json { "code": 0, "message": "Success", "data": { }, "timestamp": 1680000000000 } ``` --- ## 2. 文档管理 API(阶段 0) ### 数据结构 ```python from pydantic import BaseModel, Field from typing import Literal, Optional from datetime import datetime # 局部块更新 class BlockUpdate(BaseModel): level: int # 标题等级:1=H1,2=H2,以此类推 index: int # 该等级标题在文档中的顺序下标(从 0 开始) content: str # 该块的新内容(含标题行本身) # 创建文档请求 class CreateDocumentRequest(BaseModel): title: str # 文档标题,最大 255 字符 content: str # 文档内容(Markdown),最大 200KB format: Literal["markdown"] = "markdown" # 内容格式,默认 markdown session_id: Optional[str] = None # 关联的聊天会话 ID template_id: Optional[str] = None # 关联 Word 模板 ID(阶段 1 引入) # 更新文档请求(全量或局部块) class UpdateDocumentRequest(BaseModel): title: Optional[str] = None content: Optional[str] = None # 最大 200KB,与 blocks 二选一 blocks: Optional[list[BlockUpdate]] = None # 局部块更新,与 content 二选一 # 文档完整信息(响应) class DocumentResponse(BaseModel): id: str title: str content: str format: str session_id: Optional[str] = None template_id: Optional[str] = None created_by: str created_at: datetime updated_at: datetime # 文档列表项(不含 content,减少传输量) class DocumentListItem(BaseModel): id: str title: str template_id: Optional[str] = None created_at: datetime updated_at: datetime # 分页信息 class Pagination(BaseModel): page: int page_size: int total: int total_pages: int ``` ### 2.1 创建文档 **触发时机**:用户在 Chat 中点击"编辑"按钮时。 **目的**:在数据库中建立一条文档记录,获取 `documentId`,后续编辑过程中的防抖自动保存(PUT)都依赖这个 ID。 **注意**:此接口只做持久化,不生成任何文件,不返回下载链接。 **接口地址**: `POST /api/v1/documents` **请求体**: ```json { "title": "我的文档", "content": "# 标题\n\n这是文档内容...", "format": "markdown", "sessionId": "chat-session-123", "templateId": "tpl-001" } ``` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | title | string | 是 | 文档标题,最大 255 字符 | | content | string | 是 | 文档内容,最大 200KB | | format | string | 否 | 默认 `markdown` | | sessionId | string | 否 | 关联聊天会话 ID | | templateId | string | 否 | 关联 Word 模板 ID(阶段 1 引入) | **响应示例**: ```json { "code": 0, "data": { "documentId": "doc-abc123", "title": "我的文档", "format": "markdown", "createdAt": 1680000000000 } } ``` --- ### 2.2 获取文档详情 **触发时机**:编辑器 Modal 打开后,根据 `documentId` 加载文档内容。 **目的**:拉取数据库中存储的文档内容,回填到编辑器。 **接口地址**: `GET /api/v1/documents/{documentId}` **响应示例**: ```json { "code": 0, "data": { "id": "doc-abc123", "title": "我的文档", "content": "# 标题\n\n这是文档内容...", "format": "markdown", "sessionId": "chat-session-123", "templateId": "tpl-001", "createdBy": "user-xyz", "createdAt": 1680000000000, "updatedAt": 1680000100000 } } ``` --- ### 2.3 更新文档 **触发时机**:小文件(< 200KB)编辑后防抖 3 秒自动触发;大文件由用户手动保存时触发。 **目的**:将编辑器变动内容同步到数据库。支持全量更新和按标题块的局部更新,局部更新只传变动的块,减少传输量。 **接口地址**: `PUT /api/v1/documents/{documentId}` **请求体(全量更新)**: ```json { "title": "更新后的标题", "content": "更新后的完整内容..." } ``` **请求体(局部块更新)**: ```json { "blocks": [ { "level": 2, "index": 1, "content": "## 1.2 目标\n\n更新后的段落内容..." } ] } ``` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | title | string | 否 | 新标题(全量更新时使用) | | content | string | 否 | 新的完整内容,最大 200KB(全量更新时使用) | | blocks | array | 否 | 局部块更新列表,与 `content` 二选一 | | blocks[].level | integer | 是 | 标题等级,1 = H1,2 = H2,以此类推 | | blocks[].index | integer | 是 | 该等级标题在文档中的顺序下标(从 0 开始) | | blocks[].content | string | 是 | 该块的新内容(包含标题行本身) | **响应示例**: ```json { "code": 0, "data": { "documentId": "doc-abc123", "updatedAt": 1680000200000 } } ``` --- ### 2.4 删除文档 **触发时机**:用户主动删除某条文档记录时。 **目的**:从数据库中删除文档草稿记录。 **接口地址**: `DELETE /api/v1/documents/{documentId}` **响应示例**: ```json { "code": 0, "message": "Document deleted successfully" } ``` --- ### 2.5 获取文档列表 **触发时机**:需要展示历史文档列表时。 **目的**:分页查询当前用户的文档草稿记录。 **接口地址**: `GET /api/v1/documents` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | page | integer | 否 | 页码,默认 1 | | pageSize | integer | 否 | 每页数量,默认 20,最大 100 | | sessionId | string | 否 | 按聊天会话过滤 | | sortBy | string | 否 | `createdAt` / `updatedAt`,默认 `updatedAt` | | sortOrder | string | 否 | `asc` / `desc`,默认 `desc` | **响应示例**: ```json { "code": 0, "data": { "documents": [ { "id": "doc-abc123", "title": "我的文档", "templateId": "tpl-001", "createdAt": 1680000000000, "updatedAt": 1680000100000 } ], "pagination": { "page": 1, "pageSize": 20, "total": 100, "totalPages": 5 } } } ``` --- ## 3. 导出 API(阶段 0) **本节说明**:阶段 0 导出不依赖样式库和模板,使用系统内置默认样式将 Markdown 内容转换为 .doc 文件。阶段 1 引入样式管理后,本接口扩展支持用户选择自定义样式(通过 `styleId` 参数),接口结构不变。 导出文件按用户、按天分区保存在服务器本地,下载链接永久有效。每次导出生成一条下载记录,用户可通过下载记录接口随时重新下载或删除历史文件。后台运行磁盘监控模块,超出配额时自动向相关用户和管理员发送警告。 ### 数据结构 ```python from pydantic import BaseModel from typing import Literal, Optional from datetime import datetime # 导出 .doc 请求 class ExportDocRequest(BaseModel): file_name: str # 文件名(不含扩展名),最大 255 字符 format: Literal["doc"] # 阶段 0 固定为 doc content: str # 文档当前内容(Markdown) document_id: Optional[str] = None # 关联文档 ID,传入时后端同步更新草稿记录 style_id: Optional[str] = None # 样式 ID;不传时使用系统默认样式;阶段 1 起支持用户自定义样式 # 导出响应 class ExportDocResponse(BaseModel): record_id: str # 下载记录 ID,可用于重新下载或删除 download_url: str # 永久下载链接 file_name: str # 含扩展名的完整文件名,如 季度报告_2026Q2.doc style_id: str # 实际使用的样式 ID warning: Optional[str] = None # 存储空间超限时附带警告信息,正常时为 null # 下载记录 class ExportRecord(BaseModel): record_id: str user_id: str file_name: str # 含扩展名的完整文件名 file_size: int # 文件大小(字节) download_url: str # 永久下载链接 document_id: Optional[str] # 关联文档 ID(可空) style_id: str # 使用的样式 ID created_at: datetime # 下载记录列表响应 class ExportRecordListResponse(BaseModel): records: list[ExportRecord] pagination: Pagination # 分页信息,复用文档管理中的 Pagination 结构 ``` ### 文件存储结构 导出文件按用户、按天分区保存,路径格式如下: ``` tmp/ └── {user_id}/ └── {YYYY-MM-DD}/ └── {filename}_{token}.doc ``` ### 3.1 导出 .doc **触发时机**:用户点击编辑器"保存"按钮,在弹出对话框中填写文件名、选择格式并确认后触发。 **目的**:将 Markdown 内容按指定样式渲染生成 .doc 文件,持久化保存到服务器并写入下载记录,返回永久下载链接。输出字体、标题层级、段落间距等均由样式文件控制。 **注意**:此接口不操作数据库文档记录,仅负责文件生成与记录写入。如需同时更新草稿,可传入 `documentId`,后端会先更新一次记录再生成文件。 **接口地址**: `POST /api/v1/export/doc` **请求体**: ```json { "userId": "user-xyz", "fileName": "季度报告_2026Q2", "format": "doc", "content": "# 标题\n\n这是文档内容...", "documentId": "doc-abc123", "styleId": "style-001" } ``` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | userId | string | 是 | 当前用户 ID,用于文件分区存储和配额检查 | | fileName | string | 是 | 用户填写的文件名(不含扩展名),最大 255 字符 | | format | string | 是 | 文件格式,阶段 0 固定为 `doc` | | content | string | 是 | 文档当前内容(Markdown) | | documentId | string | 否 | 关联的文档 ID,用于同步更新自动保存记录 | | styleId | string | 否 | 样式 ID;不传时使用系统默认样式;阶段 1 起可传用户上传的自定义样式 ID | **响应示例(正常)**: ```json { "code": 0, "data": { "recordId": "rec-abc123", "downloadUrl": "https://api.axonix.com/api/v1/export/records/rec-abc123/download", "fileName": "季度报告_2026Q2.doc", "styleId": "default", "warning": null } } ``` **响应示例(存储超限)**: ```json { "code": 0, "data": { "recordId": "rec-abc123", "downloadUrl": "https://api.axonix.com/api/v1/export/records/rec-abc123/download", "fileName": "季度报告_2026Q2.doc", "styleId": "default", "warning": "您的存储空间已超出限额,请删除旧文件释放空间" } } ``` > 存储超限时导出仍然成功,`warning` 字段携带提示信息,文件可正常下载。 --- ### 3.2 获取下载记录列表 **触发时机**:用户打开下载历史面板时。 **目的**:分页查询当前用户的全部导出记录,已删除的记录默认不返回。 **接口地址**: `GET /api/v1/export/records` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | userId | string | 是 | 当前用户 ID | | page | integer | 否 | 页码,默认 1 | | pageSize | integer | 否 | 每页数量,默认 20,最大 100 | | sortOrder | string | 否 | `asc` / `desc`,默认 `desc` | **响应示例**: ```json { "code": 0, "data": { "records": [ { "recordId": "rec-abc123", "userId": "user-xyz", "fileName": "季度报告_2026Q2.doc", "fileSize": 204800, "downloadUrl": "https://api.axonix.com/api/v1/export/records/rec-abc123/download", "documentId": "doc-abc123", "styleId": "default", "createdAt": 1680000000000 } ], "pagination": { "page": 1, "pageSize": 20, "total": 5, "totalPages": 1 } } } ``` --- ### 3.3 重新下载文件 **触发时机**:用户在下载历史中点击某条记录的"下载"按钮时。 **目的**:根据记录 ID 查找本地文件,触发浏览器下载。 **接口地址**: `GET /api/v1/export/records/{recordId}/download` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | userId | string | 是 | 当前用户 ID,校验记录归属(Query 参数) | **响应**:直接返回文件流。 ``` Content-Type: application/msword Content-Disposition: attachment; filename="季度报告_2026Q2.doc" ``` **错误情况**: | HTTP 状态码 | 错误码 | 说明 | |------------|--------|------| | 404 | 4041 | 记录不存在、文件已被删除,或 recordId 与 userId 不匹配 | --- ### 3.4 删除下载记录 **触发时机**:用户在下载历史中主动删除某条记录时。 **目的**:同步删除磁盘文件和数据库记录。 **接口地址**: `DELETE /api/v1/export/records/{recordId}` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | userId | string | 是 | 当前用户 ID,校验记录归属(Query 参数) | **响应示例**: ```json { "code": 0, "message": "Record deleted successfully" } ``` **错误情况**: | HTTP 状态码 | 错误码 | 说明 | |------------|--------|------| | 404 | 4041 | 记录不存在,或 recordId 与 userId 不匹配 | --- ### 3.5 磁盘监控 后台在应用启动时自动运行磁盘监控模块,每 30 分钟执行一次检查,同时每次导出后也会触发一次实时检查。 **配额规则**: | 检查项 | 阈值 | 触发行为 | |--------|------|---------| | `tmp/` 总占用 | 超过磁盘总容量的 50% | 给所有实际有文件的用户发警告 + 给管理员发警告 | | 单用户目录占用 | 超过 `磁盘总容量 × 50% ÷ 实际有文件用户数` | 仅给该用户发警告 | > 实际有文件用户数 = `tmp/` 下存在非空目录的用户数,动态计算。 **警告通知方式**(当前阶段): - 用户侧:导出响应体 `warning` 字段 - 管理员侧:服务端日志 + `GET /api/v1/admin/storage` 接口返回超限标记 **管理端存储查询接口**: `GET /api/v1/admin/storage` ```json { "code": 0, "data": { "diskTotalBytes": 107374182400, "diskUsedBytes": 53687091200, "tmpTotalBytes": 10737418240, "quotaBytes": 53687091200, "quotaExceeded": false, "activeUsers": 5, "perUserQuotaBytes": 10737418240, "users": [ { "userId": "user-xyz", "usedBytes": 204800, "quotaExceeded": false } ] } } ``` --- ## 4. 样式管理 API(阶段 1) 用户上传任意 Word 文档,后端自动提取其中**所有样式的完整 XML 定义**,生成与原文档同名的样式文件并持久化存储,加入用户的样式缓存列表,导出 .doc 时可从列表中选择使用。 ### 功能说明 - 上传文档后,后端遍历文档中的所有样式(段落样式、字符样式、表格样式、列表样式等),完整保留每个样式的原始 XML 结构(含所有属性、子元素和命名空间),不做任何简化裁剪。 - 样式文件以上传文档的原始文件名命名(去掉扩展名),存入用户的样式库缓存。 - 每次导出 .doc 时,用户可从缓存列表中选择一个样式应用到输出文件;不选则使用系统默认样式。 ### 数据结构 ```python from pydantic import BaseModel from typing import Optional from datetime import datetime # 单个样式的完整定义 class StyleDefinition(BaseModel): name: str # 样式名称,如 "标题 1" style_id: str # 样式 ID,如 "1" type: str # 样式类型:paragraph / character / table / numbering builtin: bool # 是否为内置样式 hidden: bool # 是否隐藏 quick_style: bool # 是否显示在快速样式库 priority: Optional[int] # 显示优先级 base_style: Optional[str] # 基础样式名称 next_paragraph_style: Optional[str] # 下一段落样式名称 font_summary: Optional[dict] # 字体摘要(name, size_pt, bold, italic 等) paragraph_format_summary: Optional[dict] # 段落格式摘要(alignment, indent, spacing 等) full_xml_definition: dict # 完整原始 XML(转换为嵌套字典,保留所有属性和子元素) # 样式文件摘要(列表展示用) class StyleFileSummary(BaseModel): default_font: str # 正文默认字体,如 "宋体" default_size_pt: float # 正文默认字号(磅),如 12.0 heading_fonts: list[str] # 各级标题字体列表(H1 到 H6) total_styles: int # 提取到的样式总数 style_types: dict[str, int] # 各类型样式数量,如 {"paragraph": 120, "character": 40} # 样式文件完整信息(响应) class StyleFileResponse(BaseModel): style_id: str # 样式文件唯一 ID name: str # 样式文件名(源文档文件名去扩展名) source_file: str # 原始上传文件名(含扩展名) summary: StyleFileSummary # 摘要信息,用于列表展示 is_default: bool # 是否为系统默认样式 created_at: datetime # 样式文件列表项(不含完整样式定义,减少传输量) class StyleFileListItem(BaseModel): style_id: str name: str source_file: str summary: StyleFileSummary is_default: bool created_at: datetime ``` ### 后端提取流程 上传文档后,后端执行以下步骤: 1. 使用 `python-docx` 打开上传的 Word 文档(.doc / .docx) 2. 遍历文档 `doc.styles` 中的所有样式对象 3. 对每个样式提取: - 基本属性(`name`、`style_id`、`type`、`builtin`、`hidden` 等) - 字体摘要(通过 `style.font` API 读取 `name`、`size_pt`、`bold`、`italic`、`color_rgb` 等) - 段落格式摘要(通过 `style.paragraph_format` API 读取 `alignment`、`indent`、`spacing` 等) - **完整 XML 定义**(对 `style.element` 递归转换为嵌套字典,保留所有属性、子元素和文本节点) 4. 以上传文件名(去扩展名)命名,序列化为 JSON 保存到存储层 5. 写入数据库样式缓存记录,返回摘要信息 ### 4.1 上传样式(从 Word 文档提取全量样式 XML) **目的**:用户上传任意 Word 文档,后端提取其中所有样式的完整 XML 定义,生成同名样式文件存入缓存列表,供后续导出时选用。 **接口地址**: `POST /api/v1/styles` **请求格式**: `multipart/form-data` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | file | File | 是 | Word 文档(.doc / .docx),最大 20MB | | name | string | 否 | 自定义样式名称;不传时默认使用上传文件名(去扩展名) | **后端处理说明**: - 样式文件名默认取自上传文件名(去扩展名),例如上传 `季报模板_2026Q2.docx`,生成样式文件名为 `季报模板_2026Q2` - 提取结果以 JSON 格式持久化,字段 `full_xml_definition` 包含每个样式的完整原始 XML,转换为嵌套字典结构,保留所有命名空间属性(`w:`、`w14:` 等)和子元素 **响应示例**: ```json { "code": 0, "data": { "styleId": "style-001", "name": "季报模板_2026Q2", "sourceFile": "季报模板_2026Q2.docx", "summary": { "defaultFont": "宋体", "defaultSizePt": 12.0, "headingFonts": ["黑体", "黑体", "宋体", "宋体", "宋体", "宋体"], "totalStyles": 164, "styleTypes": { "paragraph": 120, "character": 40, "table": 3, "numbering": 1 } }, "isDefault": false, "createdAt": 1680000000000 } } ``` **错误情况**: | HTTP 状态码 | 错误码 | 说明 | |------------|--------|------| | 400 | 4001 | 文件格式不支持(非 .doc / .docx) | | 413 | 4002 | 文件超过 20MB 限制 | | 422 | 4003 | 文档无法解析(损坏或加密) | | 409 | 4004 | 同名样式文件已存在(可用 `?overwrite=true` 强制覆盖) | --- ### 4.2 获取样式列表(样式缓存) **目的**:返回当前用户的样式缓存列表,包含系统默认样式,供前端展示样式选择器。列表按创建时间倒序排列,每次导出时用户从此列表中选择应用哪个样式。 **接口地址**: `GET /api/v1/styles` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | page | integer | 否 | 页码,默认 1 | | pageSize | integer | 否 | 每页数量,默认 20,最大 100 | **响应示例**: ```json { "code": 0, "data": { "styles": [ { "styleId": "default", "name": "系统默认样式", "sourceFile": null, "summary": { "defaultFont": "宋体", "defaultSizePt": 12.0, "headingFonts": ["黑体", "黑体", "宋体", "宋体", "宋体", "宋体"], "totalStyles": 20, "styleTypes": { "paragraph": 15, "character": 5, "table": 0, "numbering": 0 } }, "isDefault": true, "createdAt": 1680000000000 }, { "styleId": "style-001", "name": "季报模板_2026Q2", "sourceFile": "季报模板_2026Q2.docx", "summary": { "defaultFont": "宋体", "defaultSizePt": 12.0, "headingFonts": ["黑体", "黑体", "宋体", "宋体", "宋体", "宋体"], "totalStyles": 164, "styleTypes": { "paragraph": 120, "character": 40, "table": 3, "numbering": 1 } }, "isDefault": false, "createdAt": 1680000000000 } ], "pagination": { "page": 1, "pageSize": 20, "total": 2, "totalPages": 1 } } } ``` --- ### 4.3 获取样式详情(含完整 XML 定义) **目的**:返回指定样式文件的完整信息,包含所有样式的完整 XML 定义,供调试或样式预览使用。 **接口地址**: `GET /api/v1/styles/{styleId}` **响应示例**(`styles` 数组内每条记录结构): ```json { "code": 0, "data": { "styleId": "style-001", "name": "季报模板_2026Q2", "sourceFile": "季报模板_2026Q2.docx", "summary": { "defaultFont": "宋体", "defaultSizePt": 12.0, "headingFonts": ["黑体", "黑体", "宋体", "宋体", "宋体", "宋体"], "totalStyles": 164, "styleTypes": { "paragraph": 120, "character": 40, "table": 3, "numbering": 1 } }, "isDefault": false, "createdAt": 1680000000000, "styles": [ { "name": "标题 1", "styleId": "1", "type": "paragraph", "builtin": true, "hidden": false, "quickStyle": true, "priority": 9, "baseStyle": "正文", "nextParagraphStyle": "正文", "fontSummary": { "name": "黑体", "sizePt": 16.0, "bold": true, "italic": false, "underline": false, "colorRgb": "000000", "strike": false, "allCaps": false, "smallCaps": false }, "paragraphFormatSummary": { "alignment": "LEFT", "leftIndentPt": null, "rightIndentPt": null, "firstLineIndentPt": null, "spaceBeforePt": 12.0, "spaceAfterPt": 6.0, "lineSpacing": 1.5, "keepTogether": false, "keepWithNext": true, "pageBreakBefore": false }, "fullXmlDefinition": { "@tag": "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}style", "@attrib": { "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}type": "paragraph", "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}styleId": "1" }, "@children": { "{...}name": { "@tag": "...", "@attrib": { "{...}val": "heading 1" } }, "{...}rPr": { "...": "完整字符属性 XML" }, "{...}pPr": { "...": "完整段落属性 XML" } } } } ] } } ``` > `fullXmlDefinition` 字段为嵌套字典格式,`@tag` 为完整 Clark 表示法标签名(含命名空间),`@attrib` 为属性字典,`@children` 为子元素字典(同名子元素自动合并为数组)。 --- ### 4.4 删除样式 **接口地址**: `DELETE /api/v1/styles/{styleId}` > 系统默认样式(`styleId: "default"`)不可删除,返回 403。 > 若该样式正在被某个文档引用,返回 409,需先解除引用再删除。 **响应示例**: ```json { "code": 0, "message": "Style deleted successfully" } ``` --- ## 5. 模板管理 API(阶段 1) 用户上传带占位符的 Word 文档作为模板,占位符格式为 `{{名称}}`。导出时后端根据文档内容按标题名称匹配占位符,替换后生成 DOCX。 ### 占位符匹配规则 - 占位符名称与 Markdown 文档中的**标题名称完全一致**时匹配,替换为该标题下的完整内容块(含子标题) - 匹配不上的占位符**清空**(替换为空字符串) - 示例:模板含 `{{一季度回顾}}`,文档中有 `## 一季度回顾`,则该标题及其下文内容替换占位符 ### 数据结构 ```python from pydantic import BaseModel from typing import Optional from datetime import datetime # 模板完整信息(响应) class TemplateResponse(BaseModel): template_id: str name: str placeholders: list[str] # 模板中所有占位符名称列表,如 ["一季度回顾", "二季度展望"] has_header: bool # 是否包含页眉 has_footer: bool # 是否包含页脚 created_at: datetime # 模板列表项 class TemplateListItem(BaseModel): template_id: str name: str placeholders: list[str] created_at: datetime ``` ### 5.1 上传模板 **目的**:用户上传带占位符的 Word 文档,后端解析出所有 `{{名称}}` 占位符列表并返回,供用户确认模板结构。 **接口地址**: `POST /api/v1/templates` **请求格式**: `multipart/form-data` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | file | File | 是 | Word 文档(.docx),最大 20MB,需包含 `{{名称}}` 格式占位符 | | name | string | 是 | 模板名称 | **响应示例**: ```json { "code": 0, "data": { "templateId": "tpl-001", "name": "季度报告模板", "placeholders": ["报告摘要", "一季度回顾", "二季度展望", "风险提示"], "hasHeader": true, "hasFooter": true, "createdAt": 1680000000000 } } ``` --- ### 5.2 获取模板信息 **接口地址**: `GET /api/v1/templates/{templateId}` **响应示例**: ```json { "code": 0, "data": { "templateId": "tpl-001", "name": "季度报告模板", "placeholders": ["报告摘要", "一季度回顾", "二季度展望", "风险提示"], "hasHeader": true, "hasFooter": true, "createdAt": 1680000000000 } } ``` --- ### 5.3 获取模板列表 **接口地址**: `GET /api/v1/templates` **响应示例**: ```json { "code": 0, "data": { "templates": [ { "templateId": "tpl-001", "name": "季度报告模板", "placeholders": ["报告摘要", "一季度回顾", "二季度展望", "风险提示"], "createdAt": 1680000000000 } ] } } ``` --- ### 5.4 删除模板 **接口地址**: `DELETE /api/v1/templates/{templateId}` **响应示例**: ```json { "code": 0, "message": "Template deleted successfully" } ``` --- ## 6. 导出 DOCX API(阶段 1) ### 数据结构 ```python from pydantic import BaseModel # 导出 DOCX(模板填充)请求 class ExportDocxRequest(BaseModel): document_id: str # 文档 ID,后端查文档内容并按标题匹配占位符 template_id: str # 模板 ID,必须指定 # 响应为文件流,HTTP Header 如下: # Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document # Content-Disposition: attachment; filename="{title}.docx" ``` ### 6.1 导出 DOCX(模板填充) **触发时机**:用户在编辑器中选择了模板并完成编辑后,点击"下载"按钮时触发。 **目的**:后端以指定模板为基础,解析文档 Markdown 内容按标题拆分为内容块,按标题名称匹配模板中的 `{{占位符}}`,替换后生成 DOCX。输出文件的字体、样式、页眉页脚完全继承模板。 **后端处理流程**: 1. 根据 `documentId` 查文档内容(Markdown) 2. 根据 `templateId` 加载模板文件和占位符列表 3. 将 Markdown 按标题拆分为内容块(标题名 → 内容) 4. 遍历占位符,标题名称匹配则替换为对应内容,匹配不上则清空 5. 生成 DOCX 文件流返回 **接口地址**: `POST /api/v1/export/docx` **请求体**: ```json { "documentId": "doc-abc123", "templateId": "tpl-001" } ``` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | documentId | string | 是 | 文档 ID,后端据此获取内容并拆分内容块 | | templateId | string | 是 | 模板 ID,决定输出文件的结构和样式 | **响应**:直接返回 DOCX 文件流,前端触发浏览器下载。 ``` Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document Content-Disposition: attachment; filename="{title}.docx" ``` **错误情况**: | 错误码 | 说明 | |--------|------| | 404 | documentId 或 templateId 不存在 | | 422 | 文档内容为空,无法生成 | --- ## 7. 编辑会话 API(阶段 2) ### 数据结构 ```python from pydantic import BaseModel from typing import Literal, Optional from datetime import datetime # 创建编辑会话请求 class CreateEditSessionRequest(BaseModel): document_id: str platform: Literal["mod-chat", "oil-agent"] expires_in: int = 3600 # Token 有效期(秒),默认 3600,最大 86400 user_id: Optional[str] = None # 用户 ID,审计用途 # 编辑会话信息(响应) class EditSessionResponse(BaseModel): session_id: str token: str # JWT,用于后续请求的 Bearer Token editor_url: str # 带 token 的编辑器完整 URL expires_at: int # Unix 毫秒时间戳 # 会话中的文档信息 class SessionDocument(BaseModel): id: str title: str content: str format: str template_id: Optional[str] = None # 会话信息 class SessionInfo(BaseModel): session_id: str platform: Literal["mod-chat", "oil-agent"] expires_at: int # 凭 Token 获取文档的响应 class SessionDocumentResponse(BaseModel): document: SessionDocument permissions: list[Literal["read", "write"]] session_info: SessionInfo ``` ### 7.1 创建编辑会话 **触发时机**:oil-agent 需要让用户编辑某份文档时,由 oil-agent 后端调用此接口。 **目的**:生成一个与指定文档绑定的临时 Token 和编辑器 URL,供 oil-agent 将 URL 传给用户打开,实现免登录访问编辑器。Token 与文档 ID 强绑定,无法访问其他文档。 **接口地址**: `POST /api/v1/edit-sessions` **请求体**: ```json { "documentId": "doc-abc123", "platform": "oil-agent", "expiresIn": 3600, "userId": "user-xyz" } ``` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | documentId | string | 是 | 要编辑的文档 ID | | platform | string | 是 | `mod-chat` / `oil-agent` | | expiresIn | integer | 否 | Token 有效期(秒),默认 3600,最大 86400 | | userId | string | 否 | 用户 ID(审计用途) | **响应示例**: ```json { "code": 0, "data": { "sessionId": "edit-session-xyz", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "editorUrl": "https://axonix.com/editor?session=edit-session-xyz&token=...", "expiresAt": 1680003600000 } } ``` --- ### 7.2 凭 Token 获取文档 **触发时机**:编辑器页面加载时,用 URL 中的 Token 换取文档内容。 **目的**:校验 Token 有效性,返回对应文档内容和权限信息,完成免登录鉴权。 **接口地址**: `GET /api/v1/edit-sessions/{sessionId}/document` **请求头**: `Authorization: Bearer {token}` **响应示例**: ```json { "code": 0, "data": { "document": { "id": "doc-abc123", "title": "我的文档", "content": "...", "format": "markdown", "templateId": "tpl-001" }, "permissions": ["read", "write"], "sessionInfo": { "sessionId": "edit-session-xyz", "platform": "oil-agent", "expiresAt": 1680003600000 } } } ``` --- ### 7.3 关闭编辑会话 **触发时机**:用户关闭编辑器或 oil-agent 主动撤回访问权限时。 **目的**:立即撤销 Token,使该链接失效,防止 Token 被复用。 **接口地址**: `DELETE /api/v1/edit-sessions/{sessionId}` **请求头**: `Authorization: Bearer {token}` **响应示例**: ```json { "code": 0, "message": "Session closed successfully" } ``` --- ## 8. Webhook 通知(阶段 2) ### 数据结构 ```python from pydantic import BaseModel from typing import Literal # 配置 Webhook 请求 class CreateWebhookRequest(BaseModel): url: str # 接收事件的回调地址 events: list[Literal["document.updated", "session.closed"]] # 订阅的事件类型 secret: str # 用于签名验证的密钥 # document.updated 事件数据 class DocumentUpdatedData(BaseModel): document_id: str session_id: str platform: Literal["mod-chat", "oil-agent"] # session.closed 事件数据 class SessionClosedData(BaseModel): session_id: str document_id: str platform: Literal["mod-chat", "oil-agent"] duration: int # 会话持续时长(秒) ``` ### 8.1 配置 Webhook **接口地址**: `POST /api/v1/webhooks` **请求体**: ```json { "url": "https://oil-agent.com/api/webhooks/editor", "events": ["document.updated", "session.closed"], "secret": "webhook-secret-key" } ``` ### 8.2 Webhook 事件格式 #### document.updated ```json { "event": "document.updated", "timestamp": 1680000100000, "data": { "documentId": "doc-abc123", "sessionId": "edit-session-xyz", "platform": "oil-agent" } } ``` #### session.closed ```json { "event": "session.closed", "timestamp": 1680003600000, "data": { "sessionId": "edit-session-xyz", "documentId": "doc-abc123", "platform": "oil-agent", "duration": 3600 } } ``` ### 8.3 签名验证 请求头: `X-Axonix-Signature: sha256=` ```python import hmac, hashlib def verify_webhook(payload: bytes, signature: str, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() return hmac.compare_digest(signature, expected) ``` --- ## 9. 导出扩展 API(阶段 3) ### 数据结构 ```python from pydantic import BaseModel from typing import Literal, Optional # 导出 PDF 选项 class ExportPdfOptions(BaseModel): page_size: Literal["A4", "A3", "Letter"] = "A4" orientation: Literal["portrait", "landscape"] = "portrait" include_page_numbers: bool = False # 导出 PDF 请求 class ExportPdfRequest(BaseModel): document_id: str options: Optional[ExportPdfOptions] = None # 响应为文件流,HTTP Header 如下: # Content-Type: application/pdf # Content-Disposition: attachment; filename="{title}.pdf" ``` ### 9.1 导出为 PDF **接口地址**: `POST /api/v1/export/pdf` **请求体**: ```json { "documentId": "doc-abc123", "options": { "pageSize": "A4", "includePageNumbers": true } } ``` **响应**: 直接返回 PDF 文件流。 ``` Content-Type: application/pdf Content-Disposition: attachment; filename="document.pdf" ``` --- ## 10. 版本管理 API(阶段 3) ### 数据结构 ```python from pydantic import BaseModel from typing import Literal, Optional from datetime import datetime # 版本记录 class DocumentVersion(BaseModel): version_id: str created_at: datetime created_by: str summary: Optional[str] = None # 版本说明,可选 # 单条 Diff 项 class DiffItem(BaseModel): type: Literal["equal", "insert", "delete"] text: str # 版本对比结果 class DiffResult(BaseModel): diff: list[DiffItem] # 回滚响应 class RestoreVersionResponse(BaseModel): document_id: str restored_from: str # 回滚来源的 version_id updated_at: datetime ``` ### 10.1 获取版本历史 **接口地址**: `GET /api/v1/documents/{documentId}/versions` **响应示例**: ```json { "code": 0, "data": { "versions": [ { "versionId": "ver-001", "createdAt": 1680000000000, "createdBy": "user-xyz", "summary": "初始版本" } ] } } ``` ### 10.2 回滚版本 **接口地址**: `POST /api/v1/documents/{documentId}/versions/{versionId}/restore` **响应示例**: ```json { "code": 0, "data": { "documentId": "doc-abc123", "restoredFrom": "ver-001", "updatedAt": 1680005000000 } } ``` ### 10.3 版本对比 **接口地址**: `GET /api/v1/documents/{documentId}/versions/diff?from={versionId}&to={versionId}` **响应示例**: ```json { "code": 0, "data": { "diff": [ { "type": "equal", "text": "第一段内容" }, { "type": "delete", "text": "被删除的内容" }, { "type": "insert", "text": "新增的内容" } ] } } ``` --- ## 11. 限流与配额 | 接口类型 | 限制 | 窗口期 | |---------|------|--------| | 文档 CRUD | 100 次/分钟 | 滑动窗口 | | 模板上传 | 10 次/小时 | 固定窗口 | | DOCX / PDF 导出 | 20 次/小时 | 固定窗口 | | 编辑会话管理 | 50 次/分钟 | 滑动窗口 | 超限时返回 HTTP 429,响应头包含 `X-RateLimit-Remaining` 和 `Retry-After`。 --- ## 12. 测试环境 - **Base URL**: `https://api-dev.axonix.com` - **测试文档 ID**: `test-doc-001` - **测试模板 ID**: `test-tpl-001` 本地 Mock: ```bash npm run mock-server # http://localhost:8080/api/v1/documents ``` --- **文档版本**: v4.0 **最后更新**: 2026-06-11 **维护者**: Axonix API 团队