feat/export-service
main
交付「多格式文档导出」第一步:Markdown → HTML 的完整导出链路,对应契约 §10 Export Service。
app/export/
document.py
markdown.py
exporters/html.py
service.py
app/contracts.py
app/routes.py
POST /api/exports
GET /api/exports
GET /api/exports/{job_id}
GET /api/exports/{job_id}/file
POST /api/exports/{job_id}/cancel
app/config.py
exports_path
pyproject.toml
mistune>=3.0,<4.0
.gitignore
backend/data/exports/
tests/test_export.py
cd backend && uv run pytest -q → 467 passed
实现 Export Service 完整生命周期:mistune AST → Document AST → HtmlExporter 渲染完整 HTML5,异步任务注册表 + 取消 + 24h 产物过期。新增 5 个 /api/exports 端点与 15 项测试;pdf/docx 与函数图像静态渲染留待后续 PR。
审阅版本:5c24414。
5c24414
本轮发现 1 个 P1、5 个 P2,均已补充复现,需要修复并增加回归测试。
位置:backend/app/export/exporters/html.py:192–198
backend/app/export/exporters/html.py:192–198
当前仅对链接执行 html.escape,不能过滤 javascript: 等危险协议。复现中,恶意 Markdown 链接被原样保留为可执行 URL,且未产生 warning。
影响:用户打开导出文件并点击恶意链接时存在脚本执行风险。
建议:在输出边界规范化 URL 并校验协议白名单;不安全链接降级为普通文本并返回 warning。图片地址也应采用明确的安全策略。
位置:backend/app/export/markdown.py:146–153
backend/app/export/markdown.py:146–153
Mistune 的图片地址位于 attrs.url,替代文本位于 children;当前读取 attrs.src 和 attrs.alt。
复现:普通 Markdown 图片被输出为 <img src="" alt="" title="Chart">。
<img src="" alt="" title="Chart">
建议:按实际 token 结构映射图片地址和替代文本,补充图片解析与 HTML 输出测试。
位置:backend/app/export/markdown.py:88–91
backend/app/export/markdown.py:88–91
原始 HTML 块被转换为只有 text、没有 children 的 paragraph,但 HtmlExporter 渲染 paragraph 时仅处理 children。
复现:<div>IMPORTANT BODY</div> 最终导出为 <p></p>,且 warnings 为空。
<div>IMPORTANT BODY</div>
<p></p>
建议:将原始内容映射为文本子节点,或安全渲染 paragraph.text;降级输出应提供 warning,不能静默丢失正文。
位置:backend/app/export/service.py:64–67、262–263
backend/app/export/service.py:64–67、262–263
过期检查只拒绝下载,任务淘汰只删除内存记录,均未删除对应 HTML 文件。
复现:产物过期返回 410 后文件仍存在;任务被淘汰后,文件继续残留且失去任务记录。
影响:持续导出会不断累积包含笔记正文的文件,MAX_JOBS 无法限制磁盘占用。
建议:实现过期产物与淘汰产物清理,并处理服务重启后遗留的孤立文件。
位置:backend/app/export/service.py:169–177
backend/app/export/service.py:169–177
parse_document 和 HtmlExporter.export 的实际计算均同步执行。后者虽然声明 async,但内部没有 await,因此调用它不会让出事件循环。
复现:解析期间调度取消回调,任务仍先完成并写出文件,取消回调随后才执行。
建议:将耗时解析、渲染移出事件循环,在提交产物前处理取消,并补充运行中取消测试。现有“创建后立即取消”测试不能覆盖该问题。
位置:backend/app/export/markdown.py:16–18
backend/app/export/markdown.py:16–18
技术栈与分工文档使用 function-plot,但实现只识别 function_plot 和 functionplot。
复现:function-plot 被当作普通代码块,未生成专用节点,也没有未渲染 warning。
建议:支持 function-plot 作为规范名称,保留现有别名,并补充对应测试。
git diff --check
请修复上述问题并补充回归测试后,再次提交审阅。
No dependencies set.
The note is not visible to the blocked user.
概述
交付「多格式文档导出」第一步:Markdown → HTML 的完整导出链路,对应契约 §10 Export Service。
变更内容
app/export/包:document.py(Document AST 内部协议 + DocumentExporter Protocol + ExportResult)、markdown.py(mistune ast renderer → Document AST)、exporters/html.py(HtmlExporter)、service.py(异步任务注册表 + 取消 + 24h 产物过期)。app/contracts.py:新增 Export 系列 DTO(ExportStatus/Format/Source/Options/Job 等)。app/routes.py:新增 5 个端点:POST /api/exports(202)、GET /api/exports、GET /api/exports/{job_id}、GET /api/exports/{job_id}/file、POST /api/exports/{job_id}/cancel。app/config.py增加exports_path;pyproject.toml引入mistune>=3.0,<4.0;.gitignore忽略backend/data/exports/。tests/test_export.py(15 项),覆盖解析、渲染、service 端到端与契约校验。测试
审阅结论:请求修改,暂不合并
审阅版本:
5c24414。本轮发现 1 个 P1、5 个 P2,均已补充复现,需要修复并增加回归测试。
[P1] 链接未校验协议,导出 HTML 保留危险 URL
位置:
backend/app/export/exporters/html.py:192–198当前仅对链接执行 html.escape,不能过滤 javascript: 等危险协议。复现中,恶意 Markdown 链接被原样保留为可执行 URL,且未产生 warning。
影响:用户打开导出文件并点击恶意链接时存在脚本执行风险。
建议:在输出边界规范化 URL 并校验协议白名单;不安全链接降级为普通文本并返回 warning。图片地址也应采用明确的安全策略。
[P2] 图片 AST 字段映射错误,图片地址和替代文本丢失
位置:
backend/app/export/markdown.py:146–153Mistune 的图片地址位于 attrs.url,替代文本位于 children;当前读取 attrs.src 和 attrs.alt。
复现:普通 Markdown 图片被输出为
<img src="" alt="" title="Chart">。建议:按实际 token 结构映射图片地址和替代文本,补充图片解析与 HTML 输出测试。
[P2] 原始 HTML 块中的正文被静默删除
位置:
backend/app/export/markdown.py:88–91原始 HTML 块被转换为只有 text、没有 children 的 paragraph,但 HtmlExporter 渲染 paragraph 时仅处理 children。
复现:
<div>IMPORTANT BODY</div>最终导出为<p></p>,且 warnings 为空。建议:将原始内容映射为文本子节点,或安全渲染 paragraph.text;降级输出应提供 warning,不能静默丢失正文。
[P2] 过期和淘汰只处理任务状态,没有清理产物文件
位置:
backend/app/export/service.py:64–67、262–263过期检查只拒绝下载,任务淘汰只删除内存记录,均未删除对应 HTML 文件。
复现:产物过期返回 410 后文件仍存在;任务被淘汰后,文件继续残留且失去任务记录。
影响:持续导出会不断累积包含笔记正文的文件,MAX_JOBS 无法限制磁盘占用。
建议:实现过期产物与淘汰产物清理,并处理服务重启后遗留的孤立文件。
[P2] 渲染阶段阻塞事件循环,运行中取消不能生效
位置:
backend/app/export/service.py:169–177parse_document 和 HtmlExporter.export 的实际计算均同步执行。后者虽然声明 async,但内部没有 await,因此调用它不会让出事件循环。
复现:解析期间调度取消回调,任务仍先完成并写出文件,取消回调随后才执行。
建议:将耗时解析、渲染移出事件循环,在提交产物前处理取消,并补充运行中取消测试。现有“创建后立即取消”测试不能覆盖该问题。
[P2] 未识别项目约定的 function-plot 围栏
位置:
backend/app/export/markdown.py:16–18技术栈与分工文档使用 function-plot,但实现只识别 function_plot 和 functionplot。
复现:function-plot 被当作普通代码块,未生成专用节点,也没有未渲染 warning。
建议:支持 function-plot 作为规范名称,保留现有别名,并补充对应测试。
验证情况
git diff --check通过。请修复上述问题并补充回归测试后,再次提交审阅。
Pull request closed