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

5.2 KiB
Raw Blame History

Export 开发说明

所属模块:Export Service(后端,负责人 yxx)。本次交付「多格式文档导出」第一步:Markdown → HTML 的完整生命周期;PDF/DOCX 与函数图像静态渲染在后续 PR 补齐。契约对应 第二阶段接口契约 §10

定位

Export Service 把笔记或未保存的 Markdown 文本渲染为可下载的 HTML 文件。采用与 Benchmark 一致的「创建即返回 queued、后台 asyncio.Task 执行」的内存模型,产物带 24h 过期时间,过期后不可下载。导出是轮询式(无 SSE 事件流),客户端通过 GET /api/exports/{job_id} 轮询状态,完成后走 GET /api/exports/{job_id}/file 下载。

模块布局

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 DTOExportStatus / ExportFormat / ExportSource / ExportOptions / ExportJob 等)放在 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 的内置 renderer="ast"(非自写 BaseRenderer),因为 mistune 的行内渲染按字符串拼接、无法承载结构化子节点;ast renderer 直接给出带 children/attrs/raw 的 token 树,_AstMapper 只做 token → DocumentNode 的搬运,不掺入任何 HTML。插件启用 tablemathurltask_lists

fenced code 按语言分流:mermaidmermaid 节点、function_plot/functionplotfunction_plot 节点,其余 → code_blockattributes.language)。node_id 按遍历顺序 node_{seq:03d} 生成,仅渲染内部使用,无需跨请求稳定。

HtmlExporter

递归渲染 Document AST 为完整 HTML5 文档(<!doctype html> + <head> 内嵌基础 CSS + <body>),标题/正文/元信息文本一律 html.escapemermaidfunction_plot 无法静态表达,渲染为占位 <pre class="mermaid">/<pre class="function-plot"> 并记 warning,不静默丢失;code_theme 仅作为代码容器 class,不引入 JS 高亮库。无法表示的节点统一 warnings.append(...) 跳过。

运行生命周期

queued → running → completed | failed | cancelled

  • 创建时校验:format 非 html → EXPORT_FORMAT_UNSUPPORTEDnote 源不存在 → EXPORT_SOURCE_NOT_FOUND404);markdown 源为空或超上限 → EXPORT_OPTIONS_INVALID
  • 内存注册表上限 MAX_JOBS=100,超限只淘汰终态任务;满容量且全为活动任务时返回 EXPORT_CAPACITY_EXCEEDED429)。
  • 后台渲染在解析前后各让出一次执行权,使「创建后立即取消」的 queued 任务能及时进入 cancelled。
  • 失败只向公开响应暴露项目错误码与安全消息,详细异常进入日志。

产物生命周期

产物写入 settings.exports_path(默认 backend/data/exports/,可通过 APP_EXPORTS_PATH 覆盖,已加入 .gitignore),文件名为 {job_id}.html,下载 Content-Disposition_safe_download_name 清洗标题得到。ExportFile 记录 sha256sizeexpires_atcompleted_at + 24h),过期返回 EXPORT_FILE_EXPIRED410)。

错误码

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

测试

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 占位)。