# Export 开发说明 > 所属模块:Export Service(后端,负责人 yxx)。交付「多格式文档导出」:Markdown → HTML / PDF / DOCX 的完整生命周期与 function-plot 静态 SVG 渲染。契约对应 [第二阶段接口契约 §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 │ ├── _common.py 共享工具(URL 协议校验 + 函数图像预算 + 占位 warning 文案 + 元数据格式化) │ ├── html.py HtmlExporter(Document AST → 完整 HTML5) │ ├── pdf.py PdfExporter(Document AST → PDF,reportlab) │ └── docx.py DocxExporter(Document AST → DOCX,python-docx) └── service.py ExportService(注册表 + 后台渲染 + 取消 + 产物生命周期) backend/app/plot/ ├── parser.py 函数图像表达式解析(白名单 AST) ├── render.py FunctionPlot → 共享几何(compute_geometry)+ 静态 SVG ├── render_reportlab.py FunctionPlot → reportlab 矢量 Drawing(PDF 内嵌) └── renderer.py StaticRenderer 内部契约(§10.4) ``` 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` 三种,经 `service._EXPORTERS` 注册表按格式分发到对应导出器。 ## 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 文档(`` + `` 内嵌基础 CSS + ``),标题/正文/元信息文本一律 `html.escape`。`function_plot` 经 `FunctionPlotStaticRenderer` 解析为静态 SVG 内嵌(解析/渲染失败或超限时回退 `
` 占位并记 warning),`mermaid` 无法静态表达,渲染为占位 `
` 并记 warning,均不静默丢失;`code_theme` 仅作为代码容器 class,不引入 JS 高亮库。无法表示的节点统一 `warnings.append(...)` 跳过。

## StaticRenderer 内部契约(§10.4)

函数图像与 Mermaid 的静态渲染统一收敛到 `app/plot/renderer.py`:

- `StaticRenderRequest`(`kind` / `source` / `source_hash` / `theme` / `width` / `height`)是统一的渲染请求载体,`source_hash` 供缓存/去重,`theme` 供主题化渲染。
- `StaticRenderer` Protocol 定义 `render(request) -> StaticRenderResult`,导出器只面向协议,不直接调用 `render_svg`。
- `FunctionPlotStaticRenderer` 委托 `parse_source` 解析 + `render_svg` 输出内嵌 SVG;`parse` 与 `render_plot` 拆开,供导出器在渲染前先拿 `node_count` 做文档级累计复杂度预算。
- `MermaidStaticRenderer` 后端无 Mermaid 渲染能力,返回空占位结果并记 warning,交由前端渲染。

## PDF / DOCX 导出器(v1 文本优先)

`PdfExporter`(reportlab platypus)与 `DocxExporter`(python-docx)实现与 HtmlExporter 一致的同步 `render(document, options) -> ExportResult` + 异步 `export`。v1 为文本优先,覆盖标题/段落/行内强调与链接/列表/引用/表格/代码块/数学文本;`mermaid` 保留源码占位并记 warning。`function_plot` 在 PDF 中已内嵌为矢量图,在 DOCX 中仍保留源码占位并记 warning(DOCX 内嵌需栅格化,本轮范围外)。

- PDF 中文字体用 reportlab 内置 `STSong-Light` CID 字体,无外部字体依赖;CID 字体无独立 bold/italic 字重,行内强调退化为普通文本(内容不丢、样式简化),标题靠字号区分层级。
- PDF 的 `function_plot` 经 `render_reportlab` 消费 `compute_geometry` 的共享几何,产出矢量 `Drawing`(网格/坐标轴 `Line`、曲线 `PolyLine`、刻度/标签 `String`,ylabel 用 `Group` 旋转),再按页面内容宽缩放追加到 story,与 HTML 的 SVG 视觉一致;解析/渲染失败或超预算时回退源码占位并记 warning,单图失败不阻断整篇。
- DOCX 通过 Normal 样式挂载 `w:eastAsia=宋体` 保证中文显示,bold/italic 由 Word 原生渲染;链接写入可点击的 `w:hyperlink` run。
- 扩展名/MIME:html→`.html`/`text/html`,pdf→`.pdf`/`application/pdf`,docx→`.docx`/`application/vnd.openxmlformats-officedocument.wordprocessingml.document`;路由 `FileResponse` 按 `mime_type` + `file_name` 通用化,无需改路由。

## 运行生命周期

`queued → running → completed | failed | cancelled`。

- 创建时校验:`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}{ext}`(`ext` 由格式决定),下载 `Content-Disposition` 用 `_safe_download_name` 清洗标题得到。`ExportFile` 记录 `sha256`、`size` 与 `expires_at`(`completed_at + 24h`),过期返回 `EXPORT_FILE_EXPIRED`(410)。

## 资源上限

为防止超大输入或海量函数图像耗尽内存/线程,导出链路内置以下上限:

- 输入源(`note` 与 `markdown`)统一限制 `MAX_MARKDOWN_CHARS = 200_000` 字符,超限返回 `EXPORT_OPTIONS_INVALID`。
- 单个 `function-plot` 图块最多 16 条表达式,超限整块回退占位并记结构化诊断 `FUNCTION_PLOT_TOO_MANY_EXPRESSIONS`。
- 单篇文档最多 16 个函数图像,超出部分回退占位并记 warning。
- 单篇文档累计函数图像 AST 节点预算 `_MAX_TOTAL_PLOT_NODES = 8000`,超出部分回退占位并记 warning,防止多图块 × 多表达式 × 深表达式组合在采样求值时长时间占满 CPU。
- 并发渲染上限 `MAX_CONCURRENT_RENDERS = 2`,解析/渲染是 CPU 密集工作,超出限额的任务在内存中排队等待渲染槽位,避免大量任务同时占满工作线程与内存。
- 最终产物大小上限 `MAX_EXPORT_BYTES = 20 MB`,超限任务标记 failed 并返回 `EXPORT_OUTPUT_TOO_LARGE`。

## 错误码

错误分两类:**同步错误**在创建/查询请求的 HTTP 响应里直接返回对应状态码;**异步任务错误**在创建时已返回 `202`,后续轮询 `GET /api/exports/{job_id}` 仍返回 `200`,错误通过任务状态与 `error_code` 字段暴露,**不映射 HTTP 状态码**。

同步错误:

```text
EXPORT_SOURCE_NOT_FOUND      404
EXPORT_OPTIONS_INVALID       400
EXPORT_UNSUPPORTED_CONTENT   422(预留)
EXPORT_JOB_NOT_FOUND         404
EXPORT_FILE_EXPIRED          410
EXPORT_CAPACITY_EXCEEDED     429
```

异步任务错误(轮询返回 `200`,字段形如 `{"status": "failed", "error_code": "..."}`):

```text
EXPORT_RENDER_FAILED
EXPORT_OUTPUT_TOO_LARGE
```

## 测试

```powershell
cd backend
uv run pytest -q
```

`tests/test_export.py` 覆盖 Markdown 解析(标题/行内/列表/代码分流/表格/数学)、HTML 渲染(标签 + 转义 + warning)、Service 端到端(note 源与 markdown 源、PDF/DOCX 魔法字节与 CJK 字体、引用块正文与嵌套列表顺序等结构内容回归、排队任务取消、未知 note、取消、list/get、过期 410)与 `ExportSource` 契约校验。`tests/test_plot.py` 覆盖表达式解析/求值、SVG 渲染、共享几何 `compute_geometry`、`render_reportlab` 矢量 Drawing(Line/PolyLine/String/Group、CJK 字体、y 翻转、缩放)与 `StaticRenderer` 契约(函数图像渲染、Mermaid 占位)。

## 范围外(后续 PR)

- Mermaid 静态渲染(后端无渲染能力,HTML/PDF/DOCX 均保留源码占位)。
- DOCX 内嵌函数图像(需栅格化为 PNG,本轮范围外,仅 PDF 内嵌矢量图)。
- 函数图像交互预览与缩放(前端 JS Renderer 负责,后端仅提供静态 SVG)。
- 代码语法高亮(当前仅 CSS class 占位)。