Files
NotesAgentic/docs/development/Export开发说明.md
T
yxxandClaude Code f1ac414866 feat(export): PDF 内嵌函数图像矢量图
- 抽取 render.py 共享几何:新增 PlotGeometry + compute_geometry,render_svg
  改为薄序列化层,SVG 输出与重构前逐字节一致(8 组用例回归验证)
- 新增 app/plot/render_reportlab.py:消费共享几何产出 reportlab 矢量 Drawing
  (网格/坐标轴 Line、曲线 PolyLine、刻度/标签 String、ylabel Group 旋转),
  复用 STSong-Light 渲染中文,按页面内容宽 renderScale 缩放
- pdf.py _block_function_plot 改为内嵌矢量图(解析/渲染失败或超预算回退占位,
  单图失败不阻断整篇);mermaid 仍占位
- 抽取 FunctionPlotBudget + format_plot_diagnostic 到 _common.py,html/pdf 共用
- 文档同步:PDF 已内嵌函数图像,DOCX 仍占位(栅格化范围外)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-06 21:26:54 +08:00

10 KiB
Raw Blame History

Export 开发说明

所属模块:Export Service(后端,负责人 yxx)。交付「多格式文档导出」:Markdown → HTML / PDF / DOCX 的完整生命周期与 function-plot 静态 SVG 渲染。契约对应 第二阶段接口契约 §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
│   ├── _common.py     共享工具(URL 协议校验 + 函数图像预算 + 占位 warning 文案 + 元数据格式化)
│   ├── html.py        HtmlExporterDocument AST → 完整 HTML5
│   ├── pdf.py         PdfExporterDocument AST → PDFreportlab
│   └── docx.py        DocxExporterDocument AST → DOCXpython-docx
└── service.py         ExportService(注册表 + 后台渲染 + 取消 + 产物生命周期)

backend/app/plot/
├── parser.py           函数图像表达式解析(白名单 AST)
├── render.py           FunctionPlot → 共享几何(compute_geometry+ 静态 SVG
├── render_reportlab.py FunctionPlot → reportlab 矢量 DrawingPDF 内嵌)
└── renderer.py         StaticRenderer 内部契约(§10.4

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 三种,经 service._EXPORTERS 注册表按格式分发到对应导出器。

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.escapefunction_plotFunctionPlotStaticRenderer 解析为静态 SVG 内嵌(解析/渲染失败或超限时回退 <pre class="function-plot"> 占位并记 warning),mermaid 无法静态表达,渲染为占位 <pre class="mermaid"> 并记 warning,均不静默丢失;code_theme 仅作为代码容器 class,不引入 JS 高亮库。无法表示的节点统一 warnings.append(...) 跳过。

StaticRenderer 内部契约(§10.4

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

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

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

PdfExporterreportlab platypus)与 DocxExporterpython-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_plotrender_reportlab 消费 compute_geometry 的共享几何,产出矢量 Drawing(网格/坐标轴 Line、曲线 PolyLine、刻度/标签 Stringylabel 用 Group 旋转),再按页面内容宽缩放追加到 story,与 HTML 的 SVG 视觉一致;解析/渲染失败或超预算时回退源码占位并记 warning,单图失败不阻断整篇。
  • DOCX 通过 Normal 样式挂载 w:eastAsia=宋体 保证中文显示,bold/italic 由 Word 原生渲染;链接写入可点击的 w:hyperlink run。
  • 扩展名/MIMEhtml→.html/text/htmlpdf→.pdf/application/pdfdocx→.docx/application/vnd.openxmlformats-officedocument.wordprocessingml.document;路由 FileResponsemime_type + file_name 通用化,无需改路由。

运行生命周期

queued → running → completed | failed | cancelled

  • 创建时校验:note 源不存在 → 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}{ext}ext 由格式决定),下载 Content-Disposition_safe_download_name 清洗标题得到。ExportFile 记录 sha256sizeexpires_atcompleted_at + 24h),过期返回 EXPORT_FILE_EXPIRED410)。

资源上限

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

  • 输入源(notemarkdown)统一限制 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_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/DOCX 魔法字节与 CJK 字体、引用块正文与嵌套列表顺序等结构内容回归、排队任务取消、未知 note、取消、list/get、过期 410)与 ExportSource 契约校验。tests/test_plot.py 覆盖表达式解析/求值、SVG 渲染、共享几何 compute_geometryrender_reportlab 矢量 DrawingLine/PolyLine/String/Group、CJK 字体、y 翻转、缩放)与 StaticRenderer 契约(函数图像渲染、Mermaid 占位)。

范围外(后续 PR

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