fix(export): 文档级组合复杂度预算与并发渲染限制 #33

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

修复内容(PR #30 审阅 1 P1 + 1 P3)

[P1] 组合复杂度仍可长时间占满导出线程

上一轮已分别限制单表达式 AST 深度/节点数、单块表达式数、文档图像数、输入大小与产物大小,但缺少整篇文档累计预算,组合起来仍允许高开销输入。本轮:

  • app/plot/model.py + app/plot/parser.py:FunctionPlot 新增 node_count(整块所有表达式 AST 节点数之和),parse_source 逐表达式累计。
  • app/export/exporters/html.py:文档级累计节点预算 _MAX_TOTAL_PLOT_NODES = 8000,超出部分回退源码占位并记 warning,不再采样求值。
  • app/export/service.py:并发渲染信号量 MAX_CONCURRENT_RENDERS = 2,解析/渲染(CPU 密集)超限额的任务在内存中排队等待渲染槽位,避免大量任务同时占满工作线程与内存。

[P3] EXPORT_OUTPUT_TOO_LARGE 不会对应 HTTP 413

docs/development/Export开发说明.md 错误码章节拆分为「同步 HTTP 错误」与「异步任务错误」两类:EXPORT_OUTPUT_TOO_LARGE(及 EXPORT_RENDER_FAILED)在创建时已返回 202,后续轮询 GET /api/exports/{job_id} 仍返回 200,错误通过 {"status": "failed", "error_code": "..."} 暴露,不再误标 HTTP 413。

测试

新增 2 条回归测试(文档级节点预算、并发渲染上限),全量 627 passed。

🤖 Generated with Claude Code

## 修复内容(PR #30 审阅 1 P1 + 1 P3) ### [P1] 组合复杂度仍可长时间占满导出线程 上一轮已分别限制单表达式 AST 深度/节点数、单块表达式数、文档图像数、输入大小与产物大小,但缺少**整篇文档累计预算**,组合起来仍允许高开销输入。本轮: - `app/plot/model.py` + `app/plot/parser.py`:`FunctionPlot` 新增 `node_count`(整块所有表达式 AST 节点数之和),`parse_source` 逐表达式累计。 - `app/export/exporters/html.py`:文档级累计节点预算 `_MAX_TOTAL_PLOT_NODES = 8000`,超出部分回退源码占位并记 warning,不再采样求值。 - `app/export/service.py`:并发渲染信号量 `MAX_CONCURRENT_RENDERS = 2`,解析/渲染(CPU 密集)超限额的任务在内存中排队等待渲染槽位,避免大量任务同时占满工作线程与内存。 ### [P3] EXPORT_OUTPUT_TOO_LARGE 不会对应 HTTP 413 `docs/development/Export开发说明.md` 错误码章节拆分为「同步 HTTP 错误」与「异步任务错误」两类:`EXPORT_OUTPUT_TOO_LARGE`(及 `EXPORT_RENDER_FAILED`)在创建时已返回 `202`,后续轮询 `GET /api/exports/{job_id}` 仍返回 `200`,错误通过 `{"status": "failed", "error_code": "..."}` 暴露,不再误标 HTTP 413。 ### 测试 新增 2 条回归测试(文档级节点预算、并发渲染上限),全量 **627 passed**。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
yxx requested review from Kronecker 2026-09-06 14:02:58 +08:00
Owner

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

审阅提交:406dd42

上轮提出的文档级复杂度预算和并发渲染限制已完成。本次提交还新增了 PDF、DOCX 导出与 StaticRenderer 契约。复审发现 3 个 P2、1 个 P3。

[P2] PDF/DOCX 导出会丢失引用块正文

位置:

  • backend/app/export/exporters/pdf.py:173–174
  • backend/app/export/exporters/docx.py:129–134

blockquote 的直接子节点是 paragraph,但两个导出器都将这些块级节点传给了只支持行内节点的 _render_inline。

实测导出以下内容:

> quoted **content**

PDF 和 DOCX 均未包含 quoted content,只产生“无法表示的行内节点:paragraph”警告。

建议逐个处理引用块中的 paragraph、list 等块级子节点,并继承引用缩进和文字颜色。回归测试需要验证实际产物中的正文,而不只是文件魔法字节。

[P2] PDF 嵌套列表的输出顺序颠倒

位置:backend/app/export/exporters/pdf.py:201–212

父列表项的 Paragraph 在遍历结束后才加入 story,但遇到嵌套列表时会立即递归输出。

实测:

- parent
  - child

生成的 PDF 流程顺序为:

child
parent

建议先输出父列表项正文,再递归输出其子列表,或者按照 AST 子节点顺序分段生成内容。

[P2] 等待渲染槽位的任务无法及时取消

位置:backend/app/export/service.py:224–239

任务在获得 _render_slots 之前就被标记为 running,取消标志却要在获得槽位之后才检查。

实测第二个任务等待单个渲染槽位时,调用取消后仍保持 running,必须等前一个任务渲染完成,才能变为 cancelled。如果前面的任务耗时较长,取消会延迟数十秒甚至更久。

建议:

  • 等待信号量期间同时监听 cancel_event;
  • 或取消对应的 asyncio.Task;
  • 等待槽位期间保持 queued;
  • 获得槽位后再设置 running 和 started_at;
  • 补充排队任务取消测试。

[P3] 接口契约仍称 PDF/DOCX 尚未实现

位置:

  • docs/contracts/第二阶段接口契约-开发版.md:71
  • docs/contracts/第二阶段接口契约-开发版.md:1083
  • docs/contracts/第二阶段接口契约-开发版.md:1106
  • 错误码列表中的 EXPORT_FORMAT_UNSUPPORTED

代码现已支持 html、pdf、docx,但团队接口契约仍写着仅 HTML 已实现,PDF/DOCX 返回 EXPORT_FORMAT_UNSUPPORTED。

请同步更新接口总表、Export Service 实现状态、请求参数说明、示例和错误码列表,避免前端继续禁用 PDF/DOCX。

其他合并事项

当前分支与最新 main 存在冲突:

  • .gitignore
  • backend/app/main.py

另外,PR 标题和正文只描述复杂度与并发修复,没有说明最新加入的 PDF/DOCX 导出功能,建议一并更新。

验证结果

  • 后端全量测试:685 passed
  • git diff --check:通过
  • 远程状态:不可直接合并

请修复上述问题、补充结构内容测试并同步最新 main 后再提交复审。

## 审阅结论:请求修改,暂不建议合并 审阅提交:`406dd42` 上轮提出的文档级复杂度预算和并发渲染限制已完成。本次提交还新增了 PDF、DOCX 导出与 `StaticRenderer` 契约。复审发现 **3 个 P2、1 个 P3**。 ### [P2] PDF/DOCX 导出会丢失引用块正文 位置: - `backend/app/export/exporters/pdf.py:173–174` - `backend/app/export/exporters/docx.py:129–134` `blockquote` 的直接子节点是 `paragraph`,但两个导出器都将这些块级节点传给了只支持行内节点的 `_render_inline`。 实测导出以下内容: ```markdown > quoted **content** ``` PDF 和 DOCX 均未包含 `quoted content`,只产生“无法表示的行内节点:paragraph”警告。 建议逐个处理引用块中的 `paragraph`、`list` 等块级子节点,并继承引用缩进和文字颜色。回归测试需要验证实际产物中的正文,而不只是文件魔法字节。 ### [P2] PDF 嵌套列表的输出顺序颠倒 位置:`backend/app/export/exporters/pdf.py:201–212` 父列表项的 `Paragraph` 在遍历结束后才加入 `story`,但遇到嵌套列表时会立即递归输出。 实测: ```markdown - parent - child ``` 生成的 PDF 流程顺序为: ```text child parent ``` 建议先输出父列表项正文,再递归输出其子列表,或者按照 AST 子节点顺序分段生成内容。 ### [P2] 等待渲染槽位的任务无法及时取消 位置:`backend/app/export/service.py:224–239` 任务在获得 `_render_slots` 之前就被标记为 `running`,取消标志却要在获得槽位之后才检查。 实测第二个任务等待单个渲染槽位时,调用取消后仍保持 `running`,必须等前一个任务渲染完成,才能变为 `cancelled`。如果前面的任务耗时较长,取消会延迟数十秒甚至更久。 建议: - 等待信号量期间同时监听 `cancel_event`; - 或取消对应的 `asyncio.Task`; - 等待槽位期间保持 `queued`; - 获得槽位后再设置 `running` 和 `started_at`; - 补充排队任务取消测试。 ### [P3] 接口契约仍称 PDF/DOCX 尚未实现 位置: - `docs/contracts/第二阶段接口契约-开发版.md:71` - `docs/contracts/第二阶段接口契约-开发版.md:1083` - `docs/contracts/第二阶段接口契约-开发版.md:1106` - 错误码列表中的 `EXPORT_FORMAT_UNSUPPORTED` 代码现已支持 `html`、`pdf`、`docx`,但团队接口契约仍写着仅 HTML 已实现,PDF/DOCX 返回 `EXPORT_FORMAT_UNSUPPORTED`。 请同步更新接口总表、Export Service 实现状态、请求参数说明、示例和错误码列表,避免前端继续禁用 PDF/DOCX。 ### 其他合并事项 当前分支与最新 `main` 存在冲突: - `.gitignore` - `backend/app/main.py` 另外,PR 标题和正文只描述复杂度与并发修复,没有说明最新加入的 PDF/DOCX 导出功能,建议一并更新。 ### 验证结果 - 后端全量测试:**685 passed** - `git diff --check`:通过 - 远程状态:不可直接合并 请修复上述问题、补充结构内容测试并同步最新 `main` 后再提交复审。
Kronecker closed this pull request 2026-09-06 17:21:37 +08:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
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#33