feat(export): 交付 Markdown → HTML 导出服务
实现 Export Service 完整生命周期:mistune AST → Document AST → HtmlExporter 渲染完整 HTML5,异步任务注册表 + 取消 + 24h 产物过期。新增 5 个 /api/exports 端点与 15 项测试;pdf/docx 与函数图像静态渲染留待后续 PR。
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
- [AI Core 与 Agent Core 开发说明](development/AI-Core与Agent-Core开发说明.md)
|
||||
- [Knowledge 与 Retrieval Core 开发说明](development/Knowledge与Retrieval-Core开发说明.md)
|
||||
- [Benchmark 开发说明](development/Benchmark开发说明.md)
|
||||
- [Export 开发说明](development/Export开发说明.md)
|
||||
- [模型提供商与模型发现开发说明](development/模型提供商与模型发现开发说明.md)
|
||||
- [MCP Bridge 与 Plugin Host 开发说明](development/MCP-Bridge与Plugin-Host开发说明.md)
|
||||
- [独立 MCP Server 配置中心开发说明](development/独立MCP-Server配置中心开发说明.md)
|
||||
|
||||
@@ -2354,7 +2354,7 @@ Quality
|
||||
└── Retrieval 参数调优
|
||||
|
||||
Content Output
|
||||
├── Markdown → HTML / PDF / DOCX
|
||||
├── Markdown → HTML(已实现)/ PDF / DOCX(暂缓)
|
||||
├── Mermaid 编辑、预览与静态导出
|
||||
└── Function Plot 解析、预览与静态导出
|
||||
|
||||
@@ -2365,7 +2365,7 @@ Frontend Extension
|
||||
└── Plugin Settings UI
|
||||
```
|
||||
|
||||
上述列表描述第二阶段技术范围,其中 stdio MCP Bridge、Plugin Command Contribution 和 Plugin Settings Contribution 后端 Contract 已实现,其余能力以各自开发说明的状态为准。每项功能必须继续经过现有 Service、Contract、Permission 和 Adapter 边界,不因 Demo 需要在 Vue 组件、Router 或 Agent Runtime 中直接绑定第三方协议。
|
||||
上述列表描述第二阶段技术范围,其中 stdio MCP Bridge、Plugin Command Contribution、Plugin Settings Contribution 后端 Contract 与 Markdown → HTML 导出已实现,其余能力以各自开发说明的状态为准。每项功能必须继续经过现有 Service、Contract、Permission 和 Adapter 边界,不因 Demo 需要在 Vue 组件、Router 或 Agent Runtime 中直接绑定第三方协议。
|
||||
|
||||
第三阶段处理:
|
||||
|
||||
|
||||
@@ -66,11 +66,11 @@
|
||||
| Benchmark | POST | `/api/benchmarks/agent/runs` | 暂缓 | 创建 Agent Benchmark(依赖 Agent Runtime 完成后交付) |
|
||||
| Benchmark | GET | `/api/benchmarks/runs` | 已实现 | 分页获取 Benchmark Run |
|
||||
| Benchmark | GET/POST | `/api/benchmarks/runs/{run_id}/*` | 计划新增 | 查询、订阅、取消和读取报告 |
|
||||
| Export | POST | `/api/exports` | 计划新增 | 创建 HTML/PDF/DOCX 导出任务 |
|
||||
| Export | GET | `/api/exports` | 计划新增 | 分页获取导出任务 |
|
||||
| Export | GET | `/api/exports/{job_id}` | 计划新增 | 查询导出任务 |
|
||||
| Export | GET | `/api/exports/{job_id}/file` | 计划新增 | 下载已完成产物 |
|
||||
| Export | POST | `/api/exports/{job_id}/cancel` | 计划新增 | 取消导出任务 |
|
||||
| Export | POST | `/api/exports` | 已实现(HTML) | 创建导出任务;`pdf`/`docx` 暂缓,返回 `EXPORT_FORMAT_UNSUPPORTED` |
|
||||
| Export | GET | `/api/exports` | 已实现(HTML) | 分页获取导出任务 |
|
||||
| Export | GET | `/api/exports/{job_id}` | 已实现(HTML) | 查询导出任务 |
|
||||
| Export | GET | `/api/exports/{job_id}/file` | 已实现(HTML) | 下载已完成产物 |
|
||||
| Export | POST | `/api/exports/{job_id}/cancel` | 已实现(HTML) | 取消导出任务 |
|
||||
| Theme | Host Contract | `ThemePackageService` | 计划新增 | 导入、预览、启停和卸载主题包 |
|
||||
| Renderer | 内部 Contract | `StaticRenderer` | 计划新增 | Mermaid/Function Plot 预览和导出复用 |
|
||||
|
||||
@@ -1078,6 +1078,8 @@ VECTOR_INDEX_REBUILD_REQUIRED
|
||||
|
||||
## 10. Export Service
|
||||
|
||||
> 实现状态:HTML 导出已实现(`backend/app/export/`),`pdf`/`docx` 暂缓——请求这两个格式返回 `EXPORT_FORMAT_UNSUPPORTED`。函数图像与 Mermaid 在 HTML 中以占位代码块保留并记 warning,静态渲染由 §10.4 的 Render Contract 在后续 PR 补齐。
|
||||
|
||||
### 10.1 创建导出任务
|
||||
|
||||
`POST /api/exports`,返回 `202 ExportJob`。
|
||||
@@ -1088,7 +1090,7 @@ VECTOR_INDEX_REBUILD_REQUIRED
|
||||
"type": "note",
|
||||
"note_id": "note_123"
|
||||
},
|
||||
"format": "pdf",
|
||||
"format": "html",
|
||||
"options": {
|
||||
"theme_id": "light",
|
||||
"include_title": true,
|
||||
@@ -1099,7 +1101,7 @@ VECTOR_INDEX_REBUILD_REQUIRED
|
||||
}
|
||||
```
|
||||
|
||||
`source.type` 首批支持 `note` 和 `markdown`。`markdown` 来源用于尚未保存的预览,字段大小受限且不持久化到 Trace。`format` 固定为 `html`、`pdf`、`docx`。
|
||||
`source.type` 首批支持 `note` 和 `markdown`。`note` 来源通过 `source.note_id` 引用已建索引笔记;`markdown` 来源用于尚未保存的预览,内容放在 `source.markdown` 字段,大小限制为 200 000 字符、不持久化到 Trace。`format` 可取 `html`、`pdf`、`docx`,但当前仅 `html` 已实现,`pdf`/`docx` 返回 `EXPORT_FORMAT_UNSUPPORTED`。
|
||||
|
||||
响应:
|
||||
|
||||
@@ -1107,11 +1109,12 @@ VECTOR_INDEX_REBUILD_REQUIRED
|
||||
{
|
||||
"job_id": "export_123",
|
||||
"status": "queued",
|
||||
"format": "pdf",
|
||||
"format": "html",
|
||||
"progress": null,
|
||||
"file": null,
|
||||
"warnings": [],
|
||||
"error": null,
|
||||
"error_code": null,
|
||||
"created_at": "2026-08-31T10:30:00Z",
|
||||
"started_at": null,
|
||||
"completed_at": null
|
||||
@@ -1127,14 +1130,14 @@ VECTOR_INDEX_REBUILD_REQUIRED
|
||||
| POST | `/api/exports/{job_id}/cancel` | `OperationResponse` |
|
||||
| GET | `/api/exports/{job_id}/file` | 文件流 |
|
||||
|
||||
下载响应设置正确 `Content-Type`、经过清理的 `Content-Disposition` 文件名和 `Content-Length`。未完成、失败或过期 Job 不返回空文件。
|
||||
下载响应设置正确 `Content-Type`、经过清理的 `Content-Disposition` 文件名和 `Content-Length`。未完成、失败或过期的 Job 不返回空文件:未完成/失败返回 `EXPORT_JOB_NOT_FOUND`(404),产物过期(超过 `expires_at`)返回 `EXPORT_FILE_EXPIRED`(410)。
|
||||
|
||||
完成 Job 的 file:
|
||||
|
||||
```json
|
||||
{
|
||||
"file_name": "操作系统复习.pdf",
|
||||
"mime_type": "application/pdf",
|
||||
"file_name": "操作系统复习.html",
|
||||
"mime_type": "text/html",
|
||||
"size": 1048576,
|
||||
"sha256": "...",
|
||||
"expires_at": "2026-09-01T10:30:00Z"
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Export 开发说明
|
||||
|
||||
> 所属模块:Export Service(后端,负责人 yxx)。本次交付「多格式文档导出」第一步:Markdown → HTML 的完整生命周期;PDF/DOCX 与函数图像静态渲染在后续 PR 补齐。契约对应 [第二阶段接口契约 §10](../contracts/第二阶段接口契约-开发版.md)。
|
||||
|
||||
## 定位
|
||||
|
||||
Export Service 把笔记或未保存的 Markdown 文本渲染为可下载的 HTML 文件。采用与 Benchmark 一致的「创建即返回 queued、后台 asyncio.Task 执行」的内存模型,产物带 24h 过期时间,过期后不可下载。导出是轮询式(无 SSE 事件流),客户端通过 `GET /api/exports/{job_id}` 轮询状态,完成后走 `GET /api/exports/{job_id}/file` 下载。
|
||||
|
||||
## 模块布局
|
||||
|
||||
```text
|
||||
backend/app/export/
|
||||
├── __init__.py 包说明
|
||||
├── document.py Document AST 内部协议 + DocumentExporter Protocol + ExportResult
|
||||
├── markdown.py mistune 'ast' renderer → Document AST
|
||||
├── exporters/
|
||||
│ ├── __init__.py
|
||||
│ └── html.py HtmlExporter(Document AST → 完整 HTML5)
|
||||
└── service.py ExportService(注册表 + 后台渲染 + 取消 + 产物生命周期)
|
||||
```
|
||||
|
||||
HTTP DTO(`ExportStatus` / `ExportFormat` / `ExportSource` / `ExportOptions` / `ExportJob` 等)放在 [app/contracts.py](../../backend/app/contracts.py),与 Benchmark DTO 同层;`DocumentNode` / `ExportResult` 属导出器内部协议,放在 `export/document.py`,不进入 HTTP 契约。
|
||||
|
||||
## 接口
|
||||
|
||||
| 方法 | 路径 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| POST | `/api/exports` | 创建导出任务(202) |
|
||||
| GET | `/api/exports?status=&format=&limit=&offset=` | 分页获取任务 |
|
||||
| GET | `/api/exports/{job_id}` | 查询任务状态 |
|
||||
| GET | `/api/exports/{job_id}/file` | 下载已完成产物 |
|
||||
| POST | `/api/exports/{job_id}/cancel` | 取消任务 |
|
||||
|
||||
`source.type` 支持 `note`(引用已建索引笔记)与 `markdown`(未保存预览,字段为 `source.markdown`,上限 200 000 字符)。当前仅 `format=html` 实现,`pdf`/`docx` 返回 `EXPORT_FORMAT_UNSUPPORTED`。
|
||||
|
||||
## Markdown → Document AST
|
||||
|
||||
解析用 [mistune](https://github.com/lepture/mistune) 的内置 `renderer="ast"`(非自写 `BaseRenderer`),因为 mistune 的行内渲染按字符串拼接、无法承载结构化子节点;ast renderer 直接给出带 `children`/`attrs`/`raw` 的 token 树,`_AstMapper` 只做 token → `DocumentNode` 的搬运,不掺入任何 HTML。插件启用 `table`、`math`、`url`、`task_lists`。
|
||||
|
||||
fenced code 按语言分流:`mermaid` → `mermaid` 节点、`function_plot`/`functionplot` → `function_plot` 节点,其余 → `code_block`(`attributes.language`)。`node_id` 按遍历顺序 `node_{seq:03d}` 生成,仅渲染内部使用,无需跨请求稳定。
|
||||
|
||||
## HtmlExporter
|
||||
|
||||
递归渲染 Document AST 为完整 HTML5 文档(`<!doctype html>` + `<head>` 内嵌基础 CSS + `<body>`),标题/正文/元信息文本一律 `html.escape`。`mermaid` 与 `function_plot` 无法静态表达,渲染为占位 `<pre class="mermaid">`/`<pre class="function-plot">` 并记 warning,不静默丢失;`code_theme` 仅作为代码容器 class,不引入 JS 高亮库。无法表示的节点统一 `warnings.append(...)` 跳过。
|
||||
|
||||
## 运行生命周期
|
||||
|
||||
`queued → running → completed | failed | cancelled`。
|
||||
|
||||
- 创建时校验:`format` 非 html → `EXPORT_FORMAT_UNSUPPORTED`;`note` 源不存在 → `EXPORT_SOURCE_NOT_FOUND`(404);`markdown` 源为空或超上限 → `EXPORT_OPTIONS_INVALID`。
|
||||
- 内存注册表上限 `MAX_JOBS=100`,超限只淘汰终态任务;满容量且全为活动任务时返回 `EXPORT_CAPACITY_EXCEEDED`(429)。
|
||||
- 后台渲染在解析前后各让出一次执行权,使「创建后立即取消」的 queued 任务能及时进入 cancelled。
|
||||
- 失败只向公开响应暴露项目错误码与安全消息,详细异常进入日志。
|
||||
|
||||
## 产物生命周期
|
||||
|
||||
产物写入 `settings.exports_path`(默认 `backend/data/exports/`,可通过 `APP_EXPORTS_PATH` 覆盖,已加入 `.gitignore`),文件名为 `{job_id}.html`,下载 `Content-Disposition` 用 `_safe_download_name` 清洗标题得到。`ExportFile` 记录 `sha256`、`size` 与 `expires_at`(`completed_at + 24h`),过期返回 `EXPORT_FILE_EXPIRED`(410)。
|
||||
|
||||
## 错误码
|
||||
|
||||
```text
|
||||
EXPORT_SOURCE_NOT_FOUND 404
|
||||
EXPORT_FORMAT_UNSUPPORTED 400
|
||||
EXPORT_OPTIONS_INVALID 400
|
||||
EXPORT_RENDER_FAILED 500
|
||||
EXPORT_UNSUPPORTED_CONTENT 422
|
||||
EXPORT_JOB_NOT_FOUND 404
|
||||
EXPORT_FILE_EXPIRED 410
|
||||
EXPORT_CAPACITY_EXCEEDED 429
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
```powershell
|
||||
cd backend
|
||||
uv run pytest -q
|
||||
```
|
||||
|
||||
`tests/test_export.py` 覆盖 Markdown 解析(标题/行内/列表/代码分流/表格/数学)、HTML 渲染(标签 + 转义 + warning)、Service 端到端(note 源与 markdown 源、pdf 拒绝、未知 note、取消、list/get、过期 410)与 `ExportSource` 契约校验。
|
||||
|
||||
## 范围外(后续 PR)
|
||||
|
||||
- PDF / DOCX 导出(`python-docx` 等底层库在 PoC 后冻结,封装在 Exporter Adapter 内)。
|
||||
- 函数图像绘制(FunctionPlot 结构化模型 + 白名单表达式解析器 + SVG 静态渲染,契约 §10.4/§12)。
|
||||
- 代码语法高亮(当前仅 CSS class 占位)。
|
||||
Reference in New Issue
Block a user