针对 PR 审阅 P1「组合复杂度仍可长时间占满导出线程」与 P3「EXPORT_OUTPUT_TOO_LARGE 误标 HTTP 413」: - plot: FunctionPlot 记录整块 AST 节点数(node_count),parser 累计 - html: 单篇文档累计节点预算 _MAX_TOTAL_PLOT_NODES=8000,超限回退占位 - service: 并发渲染信号量 MAX_CONCURRENT_RENDERS=2,超限额任务排队等待 - docs: 错误码区分同步 HTTP 错误与异步任务错误,EXPORT_OUTPUT_TOO_LARGE 由 error_code 返回而非 HTTP 413 - 补充节点预算与并发限制两条回归测试(全量 627 通过) Co-Authored-By: Claude Code <noreply@anthropic.com>
6.7 KiB
Export 开发说明
所属模块:Export Service(后端,负责人 yxx)。交付「多格式文档导出」:Markdown → HTML 的完整生命周期与 function-plot 静态 SVG 渲染;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 HtmlExporter(Document AST → 完整 HTML5)
└── service.py ExportService(注册表 + 后台渲染 + 取消 + 产物生命周期)
HTTP DTO(ExportStatus / 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。插件启用 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。
- 单篇文档累计函数图像 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 状态码。
同步错误:
EXPORT_SOURCE_NOT_FOUND 404
EXPORT_FORMAT_UNSUPPORTED 400
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": "..."}):
EXPORT_RENDER_FAILED
EXPORT_OUTPUT_TOO_LARGE
测试
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 占位)。