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

212 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.png``image-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. 目录与命名规则
默认目标目录:
```text
<md-dir>/<md-base>.assets/
```
示例:
```text
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 图片上传
```text
编辑器 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 附件上传
```text
编辑器 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. 接口设计
新增本地上传入口:
```http
POST /api/local-folder/assets/upload
multipart/form-data
```
字段:
- `file`: 上传文件。
- `rootUri`: local folder root URI。
- `documentId`: 当前 local markdown document id。
- `kind`: `image | attachment`,可由 MIME 推断兜底。
响应:
```json
{
"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 })`
- 附件:插入 link`href=sourcePath`,不生成 `/onlyoffice?...assetId=...`
这能让现有 `inlineTextNodes -> styles.link -> save_local_markdown_page` 路径写出普通 Markdown 链接。
## 9. Checklist
- [x] 设计稿落入 `design/03-rust-web/process/`
- [x] Rust route 新增 local markdown asset upload handler。
- [x] handler 能解析 `rootUri + documentId` 到本地 `.md` 文件路径。
- [x] handler 能创建 `{mdBase}.assets/` 并递增命名避免覆盖。
- [x] handler 拒绝 `..` 逃逸、空文件名、非 local folder 请求。
- [x] 前端上传分流:local folder 走本地 route,云端继续走 `/api/media/upload`
- [x] 前端 local image 插入相对路径 image node。
- [x] 前端 local attachment 插入相对路径 link,不生成 OnlyOffice assetId URL。
- [x] `save_local_markdown_page` 写回 `![alt](path)``[name](path)`
- [x] 单测覆盖本地上传目标路径和冲突递增。
- [x] 单测覆盖 local markdown 图片/附件 roundtrip。
- [x] HTTP smoke 覆盖 `/api/local-folder/assets/upload` 真实 multipart 上传与落盘。
- [x] 浏览器 smoke 覆盖本地 `.md` 页面上传图片/附件、刷新后恢复、文件树可见(`scripts/task443-local-markdown-asset-upload-smoke.js`)。
## 10. 验收命令
```bash
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 后续新增:
```bash
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task443-local-markdown-asset-upload-smoke.js
```