feat(export): 交付 Markdown → HTML 导出服务 #17

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

概述

交付「多格式文档导出」第一步: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/exportsGET /api/exports/{job_id}GET /api/exports/{job_id}/filePOST /api/exports/{job_id}/cancel
  • app/config.py 增加 exports_pathpyproject.toml 引入 mistune>=3.0,<4.0.gitignore 忽略 backend/data/exports/
  • 测试:新增 tests/test_export.py(15 项),覆盖解析、渲染、service 端到端与契约校验。

测试

cd backend && uv run pytest -q
→ 467 passed
## 概述 交付「多格式文档导出」第一步: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 端到端与契约校验。 ## 测试 ```text cd backend && uv run pytest -q → 467 passed
yxx added 1 commit 2026-09-04 09:06:34 +08:00
实现 Export Service 完整生命周期:mistune AST → Document AST → HtmlExporter 渲染完整 HTML5,异步任务注册表 + 取消 + 24h 产物过期。新增 5 个 /api/exports 端点与 15 项测试;pdf/docx 与函数图像静态渲染留待后续 PR。
Owner

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

审阅版本: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–153

Mistune 的图片地址位于 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–177

parse_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 作为规范名称,保留现有别名,并补充对应测试。

验证情况

  • 隔离环境补齐锁定版本 Mistune 后,后端完整测试:467 项通过。
  • 上述问题通过补充验证复现,现有测试未覆盖。
  • git diff --check 通过。
  • 与当前 main 无合并冲突。

请修复上述问题并补充回归测试后,再次提交审阅。

## 审阅结论:请求修改,暂不合并 审阅版本:`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–153` Mistune 的图片地址位于 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–177` parse_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 作为规范名称,保留现有别名,并补充对应测试。 ## 验证情况 - 隔离环境补齐锁定版本 Mistune 后,后端完整测试:467 项通过。 - 上述问题通过补充验证复现,现有测试未覆盖。 - `git diff --check` 通过。 - 与当前 main 无合并冲突。 请修复上述问题并补充回归测试后,再次提交审阅。
Kronecker closed this pull request 2026-09-04 10:41:16 +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#17