Files
NotesAgentic/docs/development/Export开发说明.md
T
yxx 5c2441464d feat(export): 交付 Markdown → HTML 导出服务
实现 Export Service 完整生命周期:mistune AST → Document AST → HtmlExporter 渲染完整 HTML5,异步任务注册表 + 取消 + 24h 产物过期。新增 5 个 /api/exports 端点与 15 项测试;pdf/docx 与函数图像静态渲染留待后续 PR。
2026-09-04 09:02:33 +08:00

86 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 HtmlExporterDocument 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 占位)。