feat/export-service
main
上一轮已分别限制单表达式 AST 深度/节点数、单块表达式数、文档图像数、输入大小与产物大小,但缺少整篇文档累计预算,组合起来仍允许高开销输入。本轮:
app/plot/model.py
app/plot/parser.py
FunctionPlot
node_count
parse_source
app/export/exporters/html.py
_MAX_TOTAL_PLOT_NODES = 8000
app/export/service.py
MAX_CONCURRENT_RENDERS = 2
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。
docs/development/Export开发说明.md
EXPORT_OUTPUT_TOO_LARGE
EXPORT_RENDER_FAILED
202
GET /api/exports/{job_id}
200
{"status": "failed", "error_code": "..."}
新增 2 条回归测试(文档级节点预算、并发渲染上限),全量 627 passed。
🤖 Generated with Claude Code
实现 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>
审阅提交:406dd42
406dd42
上轮提出的文档级复杂度预算和并发渲染限制已完成。本次提交还新增了 PDF、DOCX 导出与 StaticRenderer 契约。复审发现 3 个 P2、1 个 P3。
StaticRenderer
位置:
backend/app/export/exporters/pdf.py:173–174
backend/app/export/exporters/docx.py:129–134
blockquote 的直接子节点是 paragraph,但两个导出器都将这些块级节点传给了只支持行内节点的 _render_inline。
blockquote
paragraph
_render_inline
实测导出以下内容:
> quoted **content**
PDF 和 DOCX 均未包含 quoted content,只产生“无法表示的行内节点:paragraph”警告。
quoted content
建议逐个处理引用块中的 paragraph、list 等块级子节点,并继承引用缩进和文字颜色。回归测试需要验证实际产物中的正文,而不只是文件魔法字节。
list
位置:backend/app/export/exporters/pdf.py:201–212
backend/app/export/exporters/pdf.py:201–212
父列表项的 Paragraph 在遍历结束后才加入 story,但遇到嵌套列表时会立即递归输出。
Paragraph
story
实测:
- parent - child
生成的 PDF 流程顺序为:
child parent
建议先输出父列表项正文,再递归输出其子列表,或者按照 AST 子节点顺序分段生成内容。
位置:backend/app/export/service.py:224–239
backend/app/export/service.py:224–239
任务在获得 _render_slots 之前就被标记为 running,取消标志却要在获得槽位之后才检查。
_render_slots
running
实测第二个任务等待单个渲染槽位时,调用取消后仍保持 running,必须等前一个任务渲染完成,才能变为 cancelled。如果前面的任务耗时较长,取消会延迟数十秒甚至更久。
cancelled
建议:
cancel_event
asyncio.Task
queued
started_at
docs/contracts/第二阶段接口契约-开发版.md:71
docs/contracts/第二阶段接口契约-开发版.md:1083
docs/contracts/第二阶段接口契约-开发版.md:1106
EXPORT_FORMAT_UNSUPPORTED
代码现已支持 html、pdf、docx,但团队接口契约仍写着仅 HTML 已实现,PDF/DOCX 返回 EXPORT_FORMAT_UNSUPPORTED。
html
pdf
docx
请同步更新接口总表、Export Service 实现状态、请求参数说明、示例和错误码列表,避免前端继续禁用 PDF/DOCX。
当前分支与最新 main 存在冲突:
.gitignore
backend/app/main.py
另外,PR 标题和正文只描述复杂度与并发修复,没有说明最新加入的 PDF/DOCX 导出功能,建议一并更新。
git diff --check
请修复上述问题、补充结构内容测试并同步最新 main 后再提交复审。
No dependencies set.
The note is not visible to the blocked user.
修复内容(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
审阅结论:请求修改,暂不建议合并
审阅提交:
406dd42上轮提出的文档级复杂度预算和并发渲染限制已完成。本次提交还新增了 PDF、DOCX 导出与
StaticRenderer契约。复审发现 3 个 P2、1 个 P3。[P2] PDF/DOCX 导出会丢失引用块正文
位置:
backend/app/export/exporters/pdf.py:173–174backend/app/export/exporters/docx.py:129–134blockquote的直接子节点是paragraph,但两个导出器都将这些块级节点传给了只支持行内节点的_render_inline。实测导出以下内容:
PDF 和 DOCX 均未包含
quoted content,只产生“无法表示的行内节点:paragraph”警告。建议逐个处理引用块中的
paragraph、list等块级子节点,并继承引用缩进和文字颜色。回归测试需要验证实际产物中的正文,而不只是文件魔法字节。[P2] PDF 嵌套列表的输出顺序颠倒
位置:
backend/app/export/exporters/pdf.py:201–212父列表项的
Paragraph在遍历结束后才加入story,但遇到嵌套列表时会立即递归输出。实测:
生成的 PDF 流程顺序为:
建议先输出父列表项正文,再递归输出其子列表,或者按照 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:71docs/contracts/第二阶段接口契约-开发版.md:1083docs/contracts/第二阶段接口契约-开发版.md:1106EXPORT_FORMAT_UNSUPPORTED代码现已支持
html、pdf、docx,但团队接口契约仍写着仅 HTML 已实现,PDF/DOCX 返回EXPORT_FORMAT_UNSUPPORTED。请同步更新接口总表、Export Service 实现状态、请求参数说明、示例和错误码列表,避免前端继续禁用 PDF/DOCX。
其他合并事项
当前分支与最新
main存在冲突:.gitignorebackend/app/main.py另外,PR 标题和正文只描述复杂度与并发修复,没有说明最新加入的 PDF/DOCX 导出功能,建议一并更新。
验证结果
git diff --check:通过请修复上述问题、补充结构内容测试并同步最新
main后再提交复审。Pull request closed