fix(export): 修复引用块正文丢失、嵌套列表顺序与排队取消(复审) #37

Closed
yxx wants to merge 0 commits from feat/export-service into main
Collaborator

复审修复(响应 PR #33 审阅结论)

修复 3 个 P2 与 1 个 P3,补充结构内容回归测试,并同步最新 main(e29cc42)。

P2 修复

  • 引用块正文丢失:PDF/DOCX 的 _block_blockquote 此前把块级子节点(paragraph/list)交给行内渲染器,导致 quoted **content** 的正文被当作「无法表示的行内节点」丢弃。现改为逐个渲染块级子节点,并继承引用缩进/颜色。
  • PDF 嵌套列表顺序颠倒_block_list_item 此前在循环里遇到嵌套列表就立即递归输出,父级正文后置。现改为先输出父级正文、再输出子列表。
  • 排队任务无法及时取消_execute 此前在获得渲染槽位前就置为 running,取消标志要等拿到槽位后才检查。现改为等待槽位期间保持 queued 并同时监听取消,取消即时生效。

P3 修复

  • 接口契约:同步 html/pdf/docx 三格式均已实现,移除 EXPORT_FORMAT_UNSUPPORTED

附带修复

  • DOCX 列表项此前未处理直接 text 子节点,正文被块级渲染器丢弃(结构内容测试暴露),已一并修复。

测试

  • 新增结构内容回归测试:引用块正文(PDF 提取文本 / DOCX 解包 XML)、嵌套列表父级在前、排队任务取消。
  • 全量 708 用例通过。
## 复审修复(响应 PR #33 审阅结论) 修复 3 个 P2 与 1 个 P3,补充结构内容回归测试,并同步最新 main(`e29cc42`)。 ### P2 修复 - **引用块正文丢失**:PDF/DOCX 的 `_block_blockquote` 此前把块级子节点(paragraph/list)交给行内渲染器,导致 `quoted **content**` 的正文被当作「无法表示的行内节点」丢弃。现改为逐个渲染块级子节点,并继承引用缩进/颜色。 - **PDF 嵌套列表顺序颠倒**:`_block_list_item` 此前在循环里遇到嵌套列表就立即递归输出,父级正文后置。现改为先输出父级正文、再输出子列表。 - **排队任务无法及时取消**:`_execute` 此前在获得渲染槽位前就置为 `running`,取消标志要等拿到槽位后才检查。现改为等待槽位期间保持 `queued` 并同时监听取消,取消即时生效。 ### P3 修复 - **接口契约**:同步 `html`/`pdf`/`docx` 三格式均已实现,移除 `EXPORT_FORMAT_UNSUPPORTED`。 ### 附带修复 - DOCX 列表项此前未处理直接 `text` 子节点,正文被块级渲染器丢弃(结构内容测试暴露),已一并修复。 ### 测试 - 新增结构内容回归测试:引用块正文(PDF 提取文本 / DOCX 解包 XML)、嵌套列表父级在前、排队任务取消。 - 全量 708 用例通过。
yxx added 14 commits 2026-09-06 17:52:27 +08:00
实现 Export Service 完整生命周期:mistune AST → Document AST → HtmlExporter 渲染完整 HTML5,异步任务注册表 + 取消 + 24h 产物过期。新增 5 个 /api/exports 端点与 15 项测试;pdf/docx 与函数图像静态渲染留待后续 PR。
- P1 链接/图片 URL 协议白名单校验,危险协议降级为纯文本 + warning
- P2 图片 AST 字段映射(src=attrs.url,alt 取 children 文本)
- P2 原始 HTML 块转义保留,正文不丢失 + warning
- P2 过期/淘汰/重启清理导出产物文件
- P2 解析与渲染移入 asyncio.to_thread,运行中取消生效
- P2 function-plot 围栏别名补全
- 回归测试覆盖全部修复

Co-Authored-By: Claude Code <noreply@anthropic.com>
- 新增 app/plot 包:白名单表达式解析(ast 无 eval)+ FunctionPlot 模型 + 静态 SVG 渲染
- HtmlExporter 的 function_plot 节点解析并内嵌 SVG,解析失败回退占位并转诊断
- 新增 test_plot.py(13 个测试)覆盖表达式安全、指令解析、SVG 输出与导出链路集成

Co-Authored-By: Claude Code <noreply@anthropic.com>
- P1 浮点刻度死循环:_ticks 改为有上限的整数索引推进并校验步长推进
- P2 求值异常:白名单函数校验参数数量;负数底非整数指数按断点处理;采样容错复数
- P2 无效纵轴范围:退化/非有限 range 丢弃并自动采样重算;渲染异常回退占位不阻断导出
- 补 6 个回归测试

Co-Authored-By: Claude Code <noreply@anthropic.com>
同步 main(054f704),解决 contracts.py / main.py / README.md / 技术栈说明 的合并冲突。
- contracts.py:保留 pydantic 多行导入并新增 RequestOverride
- main.py:合并 lifespan(导出孤儿清理 + 转写/本地模型生命周期)
- README.md / 技术栈说明:文档取 main 最新版本

Co-Authored-By: Claude Code <noreply@anthropic.com>
- P2 复杂表达式绕过异常回退:解析与渲染共同纳入局部异常回退;
  AST 深度/节点数上限拦截 RecursionError
- P2 极端有限范围生成 nan SVG:校验坐标跨度有限且 >0,回退安全范围;
  _polyline 拒绝非有限像素坐标
- P3 更新接口契约文档:function-plot 静态 SVG 已实现

Co-Authored-By: Claude Code <noreply@anthropic.com>
针对 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>
针对 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>
# Conflicts:
#	backend/app/routes.py
- 新增 PdfExporter(reportlab)与 DocxExporter(python-docx),实现与
  HtmlExporter 一致的同步 render + 异步 export,v1 文本优先(标题/段落/
  行内强调与链接/列表/引用/表格/代码块/数学文本),function_plot 与 mermaid
  保留源码占位并记 warning。
- service 层加 _EXPORTERS 注册表按格式分发,删除 format!=html 硬限制,
  扩展名/MIME/产物清理泛化到 html/pdf/docx 三种格式。
- 新增 app/plot/renderer.py:StaticRenderRequest + StaticRenderer Protocol +
  FunctionPlotStaticRenderer + MermaidStaticRenderer;HtmlExporter 改经
  FunctionPlotStaticRenderer 消费,去除对 render_svg 的直接依赖。
- 补齐 PDF/DOCX 魔法字节、CJK 字体、占位 warning 与 StaticRenderer 契约测试。
- 更新 Export开发说明.md。

Co-Authored-By: Claude Code <noreply@anthropic.com>
# Conflicts:
#	.gitignore
#	backend/app/main.py
- 引用块直接子节点为块级节点,PDF/DOCX 改为逐个渲染并继承缩进/颜色,
  不再交给行内渲染器导致正文丢失
- PDF 嵌套列表先输出父级正文再输出子列表,修复顺序颠倒
- 等待渲染槽位期间保持 queued 并监听取消,取消即时生效
- DOCX 列表项补处理直接 text 子节点,避免正文被块级渲染器丢弃
- 补充引用块/嵌套列表/排队取消的结构内容回归测试
- 接口契约同步 html/pdf/docx 三格式均已实现,移除 EXPORT_FORMAT_UNSUPPORTED

Co-Authored-By: Claude Code <noreply@anthropic.com>
Owner

审阅结论:请求修改,暂不建议合并

审阅提交:6c14047

上一轮提出的问题已经完成基础修复:

  • PDF/DOCX 引用块正文可以正常保留;
  • 简单父子嵌套列表顺序正确;
  • 等待渲染槽位的任务保持 queued,可以即时取消;
  • PDF/DOCX 接口状态已经同步。

本轮进一步审阅发现 2 个 P2、1 个 P3

[P2] PDF 仍会重排混合列表项内容

位置:backend/app/export/exporters/pdf.py:223–238

当前实现会先收集列表项中的全部正文,随后统一输出所有嵌套列表。这只能保证简单的“父项在前、子列表在后”,无法保持混合列表项的原始 AST 顺序。

例如:

- parent

  - child

  after

原始顺序是:

parent
child
after

实际 PDF 流程顺序为:

parent
after
child

建议按照 AST 顺序分段输出:

  1. 遇到普通内容时暂存当前正文;
  2. 遇到嵌套列表前先输出已暂存正文;
  3. 在当前位置递归输出嵌套列表;
  4. 然后继续处理后续正文。

请增加“父段落—子列表—后续段落”的顺序回归测试。

[P2] DOCX/PDF 列表项会丢失链接和强调语义

位置:

  • backend/app/export/exporters/docx.py:169–184
  • backend/app/export/exporters/pdf.py:227–235

列表项的直接子节点可能是 strongemphasislink。当前遇到带 children 的节点时,会直接渲染其子节点,从而绕过该节点自身的格式处理。

实测:

- **bold** [link](https://example.com)

DOCX 结果中:

  • bold 被写成 w:b=0,没有加粗;
  • 不存在 w:hyperlink
  • 不存在指向 https://example.com 的外部关系。

PDF 中的链接也会被降级为普通文字,并且不会产生 warning。

建议:

  • 对直接行内节点调用 _render_inline_node
  • 仅对 paragraph 等块级容器展开 children
  • 分别增加列表项内加粗、斜体和链接的产物结构测试。

[P3] StaticRenderer 仍被标记为计划新增

位置:docs/contracts/第二阶段接口契约-开发版.md:77

本 PR 已新增并接入 StaticRenderer 内部契约,但接口总表仍将 Renderer 标记为“计划新增”。

请更新为“已实现”,并明确:

  • Function Plot 已支持后端静态 SVG 渲染;
  • Mermaid 当前返回占位结果;
  • PDF/DOCX 中两者目前保留源码占位。

验证结果

  • 后端全量测试:708 passed
  • git diff --check:通过
  • 与最新 main:无冲突
  • 远程状态:可合并

Git 层面已经可以合并,但仍建议修复以上问题并补充测试后再提交复审。

## 审阅结论:请求修改,暂不建议合并 审阅提交:`6c14047` 上一轮提出的问题已经完成基础修复: - PDF/DOCX 引用块正文可以正常保留; - 简单父子嵌套列表顺序正确; - 等待渲染槽位的任务保持 `queued`,可以即时取消; - PDF/DOCX 接口状态已经同步。 本轮进一步审阅发现 **2 个 P2、1 个 P3**。 ### [P2] PDF 仍会重排混合列表项内容 位置:`backend/app/export/exporters/pdf.py:223–238` 当前实现会先收集列表项中的全部正文,随后统一输出所有嵌套列表。这只能保证简单的“父项在前、子列表在后”,无法保持混合列表项的原始 AST 顺序。 例如: ```markdown - parent - child after ``` 原始顺序是: ```text parent child after ``` 实际 PDF 流程顺序为: ```text parent after child ``` 建议按照 AST 顺序分段输出: 1. 遇到普通内容时暂存当前正文; 2. 遇到嵌套列表前先输出已暂存正文; 3. 在当前位置递归输出嵌套列表; 4. 然后继续处理后续正文。 请增加“父段落—子列表—后续段落”的顺序回归测试。 ### [P2] DOCX/PDF 列表项会丢失链接和强调语义 位置: - `backend/app/export/exporters/docx.py:169–184` - `backend/app/export/exporters/pdf.py:227–235` 列表项的直接子节点可能是 `strong`、`emphasis` 或 `link`。当前遇到带 `children` 的节点时,会直接渲染其子节点,从而绕过该节点自身的格式处理。 实测: ```markdown - **bold** [link](https://example.com) ``` DOCX 结果中: - `bold` 被写成 `w:b=0`,没有加粗; - 不存在 `w:hyperlink`; - 不存在指向 `https://example.com` 的外部关系。 PDF 中的链接也会被降级为普通文字,并且不会产生 warning。 建议: - 对直接行内节点调用 `_render_inline_node`; - 仅对 `paragraph` 等块级容器展开 `children`; - 分别增加列表项内加粗、斜体和链接的产物结构测试。 ### [P3] `StaticRenderer` 仍被标记为计划新增 位置:`docs/contracts/第二阶段接口契约-开发版.md:77` 本 PR 已新增并接入 `StaticRenderer` 内部契约,但接口总表仍将 Renderer 标记为“计划新增”。 请更新为“已实现”,并明确: - Function Plot 已支持后端静态 SVG 渲染; - Mermaid 当前返回占位结果; - PDF/DOCX 中两者目前保留源码占位。 ### 验证结果 - 后端全量测试:**708 passed** - `git diff --check`:通过 - 与最新 `main`:无冲突 - 远程状态:可合并 Git 层面已经可以合并,但仍建议修复以上问题并补充测试后再提交复审。
Kronecker closed this pull request 2026-09-06 19:00:53 +08:00
Kronecker reopened this pull request 2026-09-06 19:37:19 +08:00
Kronecker closed this pull request 2026-09-06 19:37:33 +08:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Kronecker/NotesAgentic#37