针对 PR 审阅「函数数量没有限制,可能生成数百 MB 的 SVG」: - parser: 单块 function-plot 表达式上限 _MAX_EXPRESSIONS=16,超限整块回退 - html: 单篇文档函数图像上限 _MAX_FUNCTION_PLOTS=16,超出回退源码占位 - service: 输入源 MAX_MARKDOWN_CHARS、产物 MAX_EXPORT_BYTES,超限分别 拒绝创建或标记 failed(EXPORT_OUTPUT_TOO_LARGE) - 补充 4 条回归测试与文档说明 Co-Authored-By: Claude Code <noreply@anthropic.com>
96 lines
5.9 KiB
Markdown
96 lines
5.9 KiB
Markdown
# Export 开发说明
|
||
|
||
> 所属模块:Export Service(后端,负责人 yxx)。交付「多格式文档导出」:Markdown → HTML 的完整生命周期与 function-plot 静态 SVG 渲染;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`。`function_plot` 解析为静态 SVG 内嵌(解析/渲染失败或超限时回退 `<pre class="function-plot">` 占位并记 warning),`mermaid` 无法静态表达,渲染为占位 `<pre class="mermaid">` 并记 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)。
|
||
|
||
## 资源上限
|
||
|
||
为防止超大输入或海量函数图像耗尽内存/线程,导出链路内置以下上限:
|
||
|
||
- 输入源(`note` 与 `markdown`)统一限制 `MAX_MARKDOWN_CHARS = 200_000` 字符,超限返回 `EXPORT_OPTIONS_INVALID`。
|
||
- 单个 `function-plot` 图块最多 16 条表达式,超限整块回退占位并记结构化诊断 `FUNCTION_PLOT_TOO_MANY_EXPRESSIONS`。
|
||
- 单篇文档最多 16 个函数图像,超出部分回退占位并记 warning。
|
||
- 最终产物大小上限 `MAX_EXPORT_BYTES = 20 MB`,超限任务标记 failed 并返回 `EXPORT_OUTPUT_TOO_LARGE`。
|
||
|
||
## 错误码
|
||
|
||
```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
|
||
EXPORT_OUTPUT_TOO_LARGE 413
|
||
```
|
||
|
||
## 测试
|
||
|
||
```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 内)。
|
||
- 函数图像交互预览与缩放(前端 JS Renderer 负责,后端仅提供静态 SVG)。
|
||
- 代码语法高亮(当前仅 CSS class 占位)。
|