Files
mnote/design/03-rust-web/done/3-15-local-markdown-asset-upload-relative-path-v1.md
T
lix-2026 1956a8a21a chore: align mvp design governance
- 统一 local-first MVP 后阶段架构口径,补充 process 执行总序和 Reasonix 协作记录

- 归档已完成的 design checklist,标注参考型 process,更新 AGENTS/REASONIX/架构文档

- 补充文件树/主编辑器下载与上下文菜单相关实现、bug 记录和 smoke 脚本

验证:git diff --check;codegraph sync .;cargo test -p mnote-web;node --check scripts/task476-filetree-editor-context-menu-download-smoke.js
2026-05-21 09:04:13 +08:00

8.0 KiB
Raw Blame History

3-15 [done] 本地 Markdown 图片与附件上传相对路径设计 v1

创建时间:2026-05-18

状态:[done]

归档说明(2026-05-21):代码、单测、HTTP smoke 与真实浏览器 smoke 均已完成;scripts/task443-local-markdown-asset-upload-smoke.js 覆盖本地 .md 页面上传图片/附件、刷新后恢复与文件树可见。

所属主线:03-rust-web / local_folder / local markdown

参考:design/05-editor-mainline/reference-code/vscode

实现与缺陷闭环:/mnt/Data1T/mnote/bugs/03-rust-web/done/3-16-local-markdown-upload-uses-convex-media-asset-v1.md

1. 背景

云端页面的图片与附件上传当前走 /api/media/upload,生成 Convex media asset,并在正文中写入 image block 或 OnlyOffice 附件链接。

本地文件夹模式不同:页面真源是本地 .md 文件,资源真源是同一 local root 下的真实文件。若本地 .md 页面内的“上传图片/附件”继续走云端 media asset,会导致 Markdown 文件不可迁移、离开 MNote 后链接失效,也会破坏 local folder 的文件系统可理解性。

当前本地 Markdown 读写已具备基础能力:

  • local_markdown_parser.rs 能把非 .md 相对链接解析为 media 块。
  • save_local_markdown_page 能把 media 块写回 [name](sourcePath)
  • 本地文件树已能显示 local root 下的普通资源文件,并支持资源生命周期。

缺口是:编辑器上传文件时,还没有把外部文件复制到本地 Markdown 资源目录,并把页面正文写成标准相对 Markdown 链接。

2. VS Code 参考结论

参考文件:

  • extensions/markdown-language-features/src/languageFeatures/copyFiles/newFilePathGenerator.ts
  • extensions/markdown-language-features/src/languageFeatures/copyFiles/copyFiles.ts
  • extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts

VS Code 的关键做法:

  • 复制文件时先计算目标路径,默认目标在当前 Markdown 文档旁边。
  • 支持 markdown.copyFiles.destination 配置,目标路径可包含 ${documentBaseName}${fileName} 等变量。
  • 冲突策略默认递增命名,如 image.pngimage-1.png
  • 插入 Markdown 时使用相对当前文档目录的路径。
  • 图片使用 ![alt](relative/path),普通文件使用 [text](relative/path)
  • 路径包含空格或括号不匹配时用 <...> 包裹。

MNote 不需要先完整复刻 VS Code 的配置系统,但应采用同一底层原则:本地 Markdown 写标准相对路径,资源文件留在 local root 内。

3. 目标

本阶段目标是给本地 .md 页面提供可验证的最小上传链路:

  1. sourceKind=local_folder 的文档编辑器中上传图片或附件时,不调用云端 /api/media/upload
  2. 外部文件复制到当前 .md 文件旁的 {documentBaseName}.assets/ 目录。
  3. 页面正文写入标准 Markdown 相对路径:
    • 图片:![file.png](README.assets/file.png)
    • 附件:[file.docx](README.assets/file.docx)
  4. 保存后 .md 文件落盘包含相对路径;刷新页面能恢复图片/附件块。
  5. local file tree 能通过现有 watch / projection 看到新增资源文件。

非目标:

  • 不做 VS Code 风格可配置 copyFiles.destination
  • 不自动删除未引用资源文件。
  • 不把本地附件接入 Convex media asset。
  • 不在本阶段实现 OnlyOffice 对本地 Office 文件的完整编辑写回;可先作为本地文件链接/附件行打开或下载。

4. 目录与命名规则

默认目标目录:

<md-dir>/<md-base>.assets/

示例:

docs/README.md
docs/README.assets/image.png
docs/README.assets/spec.docx

命名规则:

  • 原始文件名做 UTF-8 保留,但必须去掉路径分隔符和空名。
  • 若目标已存在且内容未判断相同,使用递增后缀:
    • image.png
    • image-1.png
    • image-2.png
  • 生成的相对路径使用 /,不使用平台分隔符。
  • 返回给编辑器的 sourcePath 必须是相对当前 .md 文件所在目录的路径。

5. 安全边界

所有本地上传必须在 Rust Web 层校验:

  • rootUri 必须解析为已存在 local root。
  • documentId 必须解析到 local root 下的 .md 文件。
  • 目标目录和目标文件必须在 local root 内。
  • 禁止绝对目标路径、.. 逃逸、空文件名、目录文件名。
  • 不覆盖已有文件,除非后续有显式配置;本阶段只递增命名。

6. 数据流

6.1 图片上传

编辑器 slash 图片 / 拖入图片
→ local markdown asset upload route
→ 复制文件到 README.assets/
→ 返回 local asset descriptor
→ 编辑器插入 image nodesrc=相对路径
→ /api/documents/save?sourceKind=local_folder
→ save_local_markdown_page 写 ![alt](relative/path)

6.2 附件上传

编辑器 slash 附件 / 拖入附件
→ local markdown asset upload route
→ 复制文件到 README.assets/
→ 返回 local asset descriptor
→ 编辑器插入普通 link mark 或 media block
→ /api/documents/save?sourceKind=local_folder
→ save_local_markdown_page 写 [name](relative/path)

7. 接口设计

新增本地上传入口:

POST /api/local-folder/assets/upload
multipart/form-data

字段:

  • file: 上传文件。
  • rootUri: local folder root URI。
  • documentId: 当前 local markdown document id。
  • kind: image | attachment,可由 MIME 推断兜底。

响应:

{
  "ok": true,
  "asset": {
    "id": "local:asset:docs/README.assets/image.png",
    "asset_type": "image",
    "file_name": "image.png",
    "mime_type": "image/png",
    "file_size": 1234,
    "file_url": "README.assets/image.png",
    "sourcePath": "README.assets/image.png",
    "document_id": "local-md:docs/README.md",
    "sourceKind": "local_folder"
  }
}

说明:

  • file_url 在本地模式中不是 HTTP URL,而是给 editor 插入的 Markdown 相对路径。
  • 若前端需要实际预览,可用后续已有 local asset serving route 或补同源读取 route;正文落盘仍保持相对路径。

8. 前端分流

uploadFileToMediaAsset 在上传前判断:

  • currentSourceKind() === "local_folder":调用 /api/local-folder/assets/upload
  • 否则:继续调用 /api/media/upload

insertUploadedAssetIntoEditor 对 local asset 的处理:

  • 图片:setImage({ src: sourcePath, alt: file_name, title: file_name })
  • 附件:插入 linkhref=sourcePath,不生成 /onlyoffice?...assetId=...

这能让现有 inlineTextNodes -> styles.link -> save_local_markdown_page 路径写出普通 Markdown 链接。

9. Checklist

  • 设计稿落入 design/03-rust-web/process/
  • Rust route 新增 local markdown asset upload handler。
  • handler 能解析 rootUri + documentId 到本地 .md 文件路径。
  • handler 能创建 {mdBase}.assets/ 并递增命名避免覆盖。
  • handler 拒绝 .. 逃逸、空文件名、非 local folder 请求。
  • 前端上传分流:local folder 走本地 route,云端继续走 /api/media/upload
  • 前端 local image 插入相对路径 image node。
  • 前端 local attachment 插入相对路径 link,不生成 OnlyOffice assetId URL。
  • save_local_markdown_page 写回 ![alt](path)[name](path)
  • 单测覆盖本地上传目标路径和冲突递增。
  • 单测覆盖 local markdown 图片/附件 roundtrip。
  • HTTP smoke 覆盖 /api/local-folder/assets/upload 真实 multipart 上传与落盘。
  • 浏览器 smoke 覆盖本地 .md 页面上传图片/附件、刷新后恢复、文件树可见(scripts/task443-local-markdown-asset-upload-smoke.js)。

10. 验收命令

cargo fmt --manifest-path rust/Cargo.toml --all --check
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder -- --nocapture

浏览器 smoke 后续新增:

MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task443-local-markdown-asset-upload-smoke.js