feat: align local-first workspace direction
Document the VSCode-like local-first product shape, demote Convex to a control-plane role, and retire stale architecture drafts. Add local workspace migration/export references plus smoke coverage for no-Convex managed workspace startup, local markdown title/body/options persistence, asset upload behavior, and Convex fixture export. Verification: git diff --cached --check; node scripts/check-local-first-convex-guard.js --staged; node scripts/task444-convex-workspace-export-local-fixture-smoke.js; node scripts/task166-local-first-managed-workspace-no-convex-smoke.js; node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
# 3-15 [process] 本地 Markdown 图片与附件上传相对路径设计 v1
|
||||
|
||||
> 创建时间:2026-05-18
|
||||
>
|
||||
> 状态:`[process]`(代码、单测和 HTTP smoke 已完成;真实浏览器 smoke 待补)
|
||||
>
|
||||
> 所属主线:`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 时使用相对当前文档目录的路径。
|
||||
- 图片使用 ``,普通文件使用 `[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.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 node,src=相对路径
|
||||
→ /api/documents/save?sourceKind=local_folder
|
||||
→ save_local_markdown_page 写 
|
||||
```
|
||||
|
||||
### 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` 写回 `` 与 `[name](path)`。
|
||||
- [x] 单测覆盖本地上传目标路径和冲突递增。
|
||||
- [x] 单测覆盖 local markdown 图片/附件 roundtrip。
|
||||
- [x] HTTP smoke 覆盖 `/api/local-folder/assets/upload` 真实 multipart 上传与落盘。
|
||||
- [ ] 浏览器 smoke 覆盖本地 `.md` 页面上传图片/附件、刷新后恢复、文件树可见。
|
||||
|
||||
## 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/task-local-markdown-asset-upload-smoke.js
|
||||
```
|
||||
@@ -1,18 +1,26 @@
|
||||
# 3-3 [process] Rust Web Tree Realtime Event Stream 方案 v1
|
||||
|
||||
> 更新时间:2026-05-17(WS push 迁移后口径更新)
|
||||
> 更新时间:2026-05-18(local-first 口径更新)
|
||||
> 关联新设计稿:`design/03-rust-web/done/3-14-rust-web-tree-realtime-ws-push-v1.md`
|
||||
> 关联 local-first 上位设计:`design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md`
|
||||
>
|
||||
> 2026-05-18 口径更新:
|
||||
> - 本文的 Convex realtime substrate 只适用于 `convex_workspace`、同步副本和后续协作场景。
|
||||
> - 当前早期产品默认 source 是 `local_folder`;本地树变化应优先通过 LocalFS watcher / rescan / Rust projection event 进入同一条前端 projection consumer。
|
||||
> - `/api/realtime/ws` 与 `/api/tree/events` 的长期职责是统一 transport;本地工作区的数据真相仍是 LocalFS / WorkspaceSource。
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`(历史过渡背景)
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
这份文档用于固定 Stage C-1 的正式实时链路口径:
|
||||
这份文档用于固定 Stage C-1 的正式实时链路口径。2026-05-18 后,它应按 `WorkspaceSource` 区分底层事件来源:
|
||||
|
||||
- 保留 Convex 作为 realtime substrate
|
||||
- `local_folder`:LocalFS watcher / rescan / command result 是默认事件来源
|
||||
- `convex_workspace`:Convex 作为 realtime substrate
|
||||
- Rust 成为 tree-first graph 的 semantic owner
|
||||
- Rust Web 负责正式页面 transport 与实时事件流
|
||||
- 前端只消费 projection 与 delta,不再消费实验壳真相
|
||||
@@ -26,18 +34,16 @@
|
||||
|
||||
## 2. 职责划分
|
||||
|
||||
### 2.1 Convex substrate
|
||||
### 2.1 Workspace event substrate
|
||||
|
||||
Convex 继续承担:
|
||||
不同 `WorkspaceSource` 使用不同 substrate:
|
||||
|
||||
- 持久化
|
||||
- mutation / query 底座
|
||||
- 实时订阅底座
|
||||
- 文件 / 对象存储协作
|
||||
- `local_folder`:本地文件系统、watcher、rescan、命令执行结果。
|
||||
- `convex_workspace`:Convex 持久化、mutation / query、实时订阅、对象存储协作。
|
||||
|
||||
Convex 在这里是:
|
||||
Convex 在这里是可选云端 / 协作 source 的 substrate,而不是所有工作区的默认 substrate:
|
||||
|
||||
> storage / realtime substrate
|
||||
> source-specific storage / realtime substrate
|
||||
|
||||
而不是页面树语义 owner。
|
||||
|
||||
@@ -100,13 +106,15 @@ Rust Web 负责:
|
||||
|
||||
正式链路建议固定为四层:
|
||||
|
||||
### 4.1 Convex 持久化/订阅底座
|
||||
### 4.1 Source-specific 持久化/订阅底座
|
||||
|
||||
这里负责:
|
||||
|
||||
- 命令落账
|
||||
- 持久化页面树状态
|
||||
- 输出 mutation 后可订阅的数据变化
|
||||
- 持久化页面树状态或本地文件状态
|
||||
- 输出 command / watcher / mutation 后可订阅的数据变化
|
||||
|
||||
对于 local-first 默认路径,这层是 LocalFS watcher / rescan / command result;对于云端、同步和协作路径,这层才是 Convex。
|
||||
|
||||
### 4.2 Rust kernel 语义 owner
|
||||
|
||||
|
||||
Reference in New Issue
Block a user