Files
mnote/design/04-tree-domain/done/4-21-local-folder-convex-unified-tree-source-v1.md
T

589 lines
17 KiB
Markdown
Raw Normal View History

# 4-21 [done] Local Folder / Convex 统一树源架构 v1
2026-05-08 00:41:03 +08:00
> 更新时间:2026-05-06
>
> 关联:
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-20-vscode-explorer-file-tree-alignment-v1.md`
2026-05-08 00:41:03 +08:00
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-7-tree-shell-ui-state-boundary-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/vscode`
>
> 状态说明:当前 `3000` 主入口、本地文件夹打开、统一 `file_tree / page_tree / page_aggregate` 消费链已进入已完成基线;剩余收口继续由 `3-13`、`3-3` 等后续稿跟踪,本稿不再承载新的 process 项。
2026-05-08 00:41:03 +08:00
## 1. 结论
支持引入 `Local Folder Workspace`,但不能因此维护两套文件树操作。
新的长期结构应固定为:
```text
LocalFolderSource -> Tree Kernel -> file_tree / page_tree / page_aggregate -> UI
ConvexWorkspaceSource -> Tree Kernel -> file_tree / page_tree / page_aggregate -> UI
```
也就是说:
- `Convex` 不再是所有 workspace 的必经中介。
- `Local Folder` 不经过 Convex,直接由 Rust source adapter 读取和写入本地文件系统。
- `Convex` 仅作为显式 cloud / compat workspace 的 source adapter 或 sync replica;同步、协作、权限、分享默认控制面已由 Rust SQLite control-plane 承接。
2026-05-08 00:41:03 +08:00
- 两者共用同一套 projection、command、preflight、runtime state 和 UI。
- 差异只允许出现在 `WorkspaceSource` 的 capability、preflight plan 和 executor 中。
核心原则:
> **维护一套树操作语义,不维护两套文件树产品。**
## 2. 为什么需要这个方案
当前 mnote 的 Explorer 视图仍然基于 Convex 虚拟用户空间,这让 VSCode Explorer 对标天然别扭:
- 没有真实文件夹 root。
- `New File / New Folder / Rename / Move / Cut / Paste / Drop` 都需要模拟文件系统语义。
- `index.md`、页面、附件、mindmap asset-folder 混成复合投影后,无法完全照搬 VSCode 文件夹操作。
如果允许用户打开一个本地文件夹,就能获得真实文件系统语义:
- 文件夹就是文件夹。
- `.md` 文件就是页面内容的本体。
- 普通文件就是资源。
- 拖入、拖动、剪切、复制、粘贴、重命名、删除都可以映射到文件系统操作。
- Git、外部编辑器、脚本、ripgrep 等外部工具自然成立。
但这不表示 Convex 失去意义。Convex 的定位应从“所有树的底座”调整为:
> **云工作区 source:负责多端、协作、权限、分享、发布、异步处理和远程资源。**
## 3. 术语
### 3.1 `WorkspaceSource`
`WorkspaceSource` 是树域的存储源适配层。
最低接口:
- `sourceKind`
- `rootUri`
- `workspaceId`
- `capabilities`
- `loadSnapshot`
- `watch`
- `preflightCommand`
- `executeCommand`
- `resolvePageAggregate`
当前建议先支持两类:
- `local_folder`
- `convex_workspace`
### 3.2 `SpaceRoot`
`SpaceRoot` 是用户在 UI 里选择的工作区入口。
示例:
- `file:///home/lix/Documents/mnote-vault`
- `convex://workspace/ws_123`
`SpaceRoot` 可以出现在同一个 Explorer 列表里,但每个 root 只能由一个 source adapter 持有。
### 3.3 `Source Capability`
capability 描述当前 source 对某项操作的真实支持程度。
示例:
- `create-file`
- `create-folder`
- `rename`
- `move`
- `copy`
- `trash`
- `restore`
- `purge`
- `upload`
- `watch`
- `share`
- `publish`
- `collaborate`
capability 不改变 UI 命令名,只控制菜单显隐、禁用原因和 preflight 结果。
## 4. 真源分层
### 4.1 存储真源
存储真源由 source 持有:
| source | 存储真源 |
| --- | --- |
| `local_folder` | 本地文件系统 |
| `convex_workspace` | Convex 数据库 + 对象存储 |
本地模式不得先导入 Convex 再渲染。那会破坏本地文件夹模式的价值。
### 4.2 语义真源
树、页面和资源语义由 Rust kernel 统一生成:
- `KernelNode`
- `KernelEdge`
- `file_tree`
- `page_tree`
- `page_aggregate`
- `tree.*` command result
source adapter 只负责读写存储,不允许 UI 直接从本地路径或 Convex record 拼树语义。
### 4.3 UI 本地态
继续服从 `4-7`
- `expanded`
- `selected`
- `focused`
- `hovered`
- `editingName`
- `clipboard`
- `dragging`
- `dropTarget`
- `contextMenuOpen`
这些不进入 source,也不进入 projection。
## 5. Local Folder Workspace
### 5.1 P0 范围
P0 只支持:
- 打开一个本地文件夹作为 workspace root。
- `file_tree` 显示该文件夹下的目录、`.md` 文件和普通文件。
- `page_tree` 只显示 `.md` 文件。
- 打开 `.md` 文件时进入文档页。
- 普通文件作为 asset 打开或预览。
- 对本地文件和目录执行 VSCode-like 操作。
暂不支持:
- 多用户本地 ACL。
- 云同步。
- 多端协作。
- 非 Markdown 正文格式的页面编辑。
- 本地文件夹自动发布到 Convex。
### 5.2 本地页面身份
不能只用文件路径作为长期 page id。
建议 P0 采用三层策略:
1. `.md` frontmatter 中存在 `mnote_id` 时使用它。
2. 不存在时在 `.mnote/page-ids.json` 记录 `relativePath -> pageId`
3. 临时未初始化文件夹可用 path-derived id,但只作为只读过渡。
原因:
- 重命名文件时页面引用不能断。
- 移动文件时历史、链接、backlink 和 AI 索引需要能追踪。
- 同名文件在不同目录下必须稳定区分。
### 5.3 Markdown 到 Page Aggregate
本地 `.md` 文件映射到 `page_aggregate`
| Page Aggregate 字段 | 本地来源 |
| --- | --- |
| `page_identity` | `mnote_id` / `.mnote/page-ids.json` |
| `page_head.title` | frontmatter `title` / 第一行 H1 / 文件名 |
| `page_body` | Markdown AST / Tiptap document adapter |
| `page_tree` | 文件夹层级派生的 md-only projection |
| `page_layout` | frontmatter / `.mnote/page-options.json` |
写回时:
- 正文保存写回 `.md`
- 标题重命名优先改文件名,必要时同步 H1/frontmatter。
- 页面设置写入 frontmatter 或 `.mnote/page-options.json`,具体由后续编辑器设计冻结。
### 5.4 本地资源
`.md` 文件作为 resource
- image / video / audio
- pdf / book
- table-like 文件
- unknown file
资源链接优先使用相对路径。
附件从页面正文引用时,应解析成 `resourceMeta`,但不能把正文引用本身变成树结构真相。
## 6. Convex Workspace
Convex 继续存在,但定位更明确:
- 云工作区。
- 权限 / 成员 / 分享。
- 评论、协作状态、公开页面。
- 远程附件存储。
- OCR、AI、索引、异步任务。
- 多设备访问。
Convex source adapter 输出同一套 kernel 输入:
- document record -> page/document node
- media asset -> asset/resource node
- mindmap/table/book/pdf -> resource node
- share/public/template 状态 -> resource meta / decoration meta
Convex 不再被要求模拟真实本地文件系统;它只需要实现同一组 tree command 语义。
## 7. 统一 Projection
### 7.1 `file_tree`
`file_tree` 表达 source 的资源管理结构。
本地 source
- root folder
- directory
- markdown file
- normal file
Convex source
- workspace root
- page/document
- `index.md` view
- asset-folder
- asset
共同字段继续使用 `4-4`
- `rowId`
- `nodeId`
- `parentNodeId`
- `nodeType`
- `projectionKind`
- `title`
- `depth`
- `position`
- `childCount`
- `expandable`
- `capabilities`
- `resourceMeta`
- `iconHint`
新增建议:
- `sourceKind`
- `sourceUri`
- `relativePath`
- `storageIdentity`
- `operationProfile`
这些应进入 `resourceMeta.extra` 或后续协议扩展,不能散落在 UI。
### 7.2 `page_tree`
`page_tree` 表达可作为知识页面打开的页面集合。
本地 source
- 只显示 `.md` 文件。
- 默认层级来自目录结构。
- 排序可先使用文件系统排序,后续支持 frontmatter `order`
Convex source
- 继续显示 document/page 层级。
- 不显示普通 asset。
关键判断:
> Wolai 页面树不是文件树的替代,而是 markdown/page resource 的语义视图。
### 7.3 `page_aggregate`
无论 source 是本地还是 Convex,文档页只消费 `page_aggregate`
前端不应该关心正文来自:
- 本地 `.md`
- Convex document content
- 未来同步副本
差异由 page aggregate loader / source adapter 吸收。
## 8. 统一 Command
UI 只发一套命令。
### 8.1 命令路由
```text
UI action
-> tree command envelope
-> Rust command router
-> source adapter preflight
-> source adapter execute
-> kernel event / projection resync
```
命令 envelope 必须携带:
- `sourceKind`
- `workspaceId`
- `rootUri`
- `targetNodeId`
- `targetResourceMeta`
- `selection`
- `operation`
### 8.2 命令语义
| UI 操作 | 统一命令 | Local executor | Convex executor |
| --- | --- | --- | --- |
| 新建页面 | `tree.node.create` | 创建 `.md` | 创建 document |
| 新建文件夹 | `tree.folder.create``tree.resource.createFolder` | 创建目录 | 按 capability 决定是否支持 |
| 重命名页面 | `tree.node.rename` | rename `.md` / 更新 title | rename document |
| 移动页面 | `tree.subtree.move` | move file / folder | move document subtree |
| 复制页面 | `tree.subtree.copy` | copy file / folder | copy document subtree |
| 删除页面 | `tree.node.archive` | 移到 `.mnote/trash` 或系统 trash | 标记 archived |
| 永久删除 | `tree.node.purge` | 删除文件 | purge document |
| 上传 / 拖入文件 | `tree.resource.upload` | copy into folder | upload object storage |
| 移动资源 | `tree.resource.move` | move file | update asset parent/document |
| 复制资源 | `tree.resource.copy` | copy file | copy asset |
| 重命名资源 | `tree.resource.rename` | rename file | rename asset |
| 删除资源 | `tree.resource.archive` | trash file | archive asset |
| 恢复资源 | `tree.resource.restore` | restore from trash | restore asset |
说明:
- `tree.folder.create` 是否新增,需要单独冻结命名。若不新增,也可以把 folder 视作 `tree.resource.create``resourceKind=folder`
- 命名必须在后续 `4-6` 的 command protocol 补稿里冻结。
## 9. 统一操作但不同 capability
用户担心“两套文件树操作”是合理的。解决方式不是把底层做成一样,而是统一 capability 解释。
### 9.1 统一 UI
以下 UI 行为必须完全共用:
- row model
- active / selection / focus
- keyboard
- inline rename
- context menu
- clipboard
- drag/drop
- reveal
- filter/search
- preflight result display
### 9.2 不同 source capability
差异只表现为:
- 菜单项隐藏。
- 菜单项禁用并显示原因。
- preflight plan 返回不同风险。
- execute adapter 调用不同存储 API。
示例:
- 本地文件夹支持 `New Folder`
- Convex 虚拟空间默认不支持真实 `New Folder`,除非引入 folder resource。
- Convex 支持 `Share / Publish`
- 本地文件夹默认不支持 `Share / Publish`,除非执行显式 publish 到 cloud。
## 10. 权限模型
### 10.1 本地权限
本地权限来自:
- 操作系统文件权限。
- 用户选择文件夹的显式授权。
- workspace trust。
- mnote 自己的安全限制,例如禁止越过 root。
如果用户想“本地建立每个用户的文件夹”,可以作为部署策略:
```text
/data/mnote-users/alice/
/data/mnote-users/bob/
```
但这只是本地 source 的 root 管理方式,不等价于 Convex 的协作权限模型。
### 10.2 Convex 权限
Convex 权限来自:
- workspace membership
- document ACL
- public/share state
- group permission
- server-side policy
这类能力本地文件夹不能自动获得。
### 10.3 不混淆
不要让本地文件夹伪装成云协作空间。
正确表达:
- local:你有这个文件夹的本机读写权。
- cloud:你有这个 workspace 的协作权限。
## 11. VSCode 源码参考
本轮已把 VSCode 源码浅克隆到:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/vscode`
当前 checkout
- commit`b5c5f2f`
- sparse paths
- `src/vs/workbench/contrib/files`
- `src/vs/platform/files`
- `src/vs/base/browser/ui/tree`
- `src/vs/base/browser/ui/list`
- `src/vs/workbench/browser/parts/views`
- command / action / context key 相关基础目录
### 11.1 值得参考的点
VSCode Explorer 有几个重要设计值得直接吸收:
- `IExplorerService` 同时管理 roots、context、editable、copy/cut、refresh、select/reveal 和 bulk edit。
- context key 明确区分 folder、readonly、root、cut、focus、find provider 等状态。
- `ExplorerItem` 是资源模型,持有 `resource/name/isDirectory/isReadonly/parent/root/children`
- renderer 把 cut 状态变成 row class,并把 inline rename 做在 row 内。
- inline rename 默认只选中文件名 stem`F2` 可在 stem/all/extension 三种选择之间切换。
- DnD 会区分 native drop、Explorer 内部 drop、root reorder、readonly、self/descendant invalid、copy vs move、before/after/over indicator 和 auto expand。
- sorter 支持 default/mixed/filesFirst/type/modified/foldersNestsFiles,但排序逻辑仍在模型层,不在 UI 临时判断。
### 11.2 不能照搬的点
不能照搬:
- 真实磁盘路径作为唯一页面身份。
- compact folders 到 page/resource 复合链。
- OS 级菜单动作作为默认能力。
- VSCode 的 editor tab preview/pin 语义。
- 文件 nesting 规则到 mnote 的页面引用或附件关系。
## 12. 推荐实施顺序
### P0:统一 Source 抽象和本地只读投影
- 增加 `WorkspaceSource` 设计与 Rust trait。
- 增加 `LocalFolderSource` 只读扫描。
- 本地 source 输出 `file_tree`
- 本地 source 输出 md-only `page_tree`
- 打开 `.md` 生成只读 `page_aggregate`
- UI 可从入口选择 `convex workspace``local folder`
验收:
- 打开一个本地文件夹后,Explorer 显示真实目录和 `.md`
- 页面树只显示 `.md`
- 点击 `.md` 能进入文档页阅读态。
- Convex 现有 workspace 不受影响。
### P1:本地写操作与统一 command
- 本地新建 `.md`
- 本地新建目录。
- inline rename。
- move / copy / cut / paste。
- delete 到 `.mnote/trash` 或系统 trash。
- drop 外部文件。
- 文件 watcher 刷新 projection。
验收:
- 同一套 UI 对 local 和 Convex 发同一类 command envelope。
- source adapter 分别执行。
- preflight UI 共用。
### P2:本地 Markdown Page Aggregate 写回
- Markdown AST 与 Tiptap document 双向转换。
- 标题编辑写回文件名 / H1 / frontmatter 的规则冻结。
- page id 管理。
- backlink / reference scan。
- 本地全文索引。
### P3Local 与 Cloud 的显式桥接
- Import local folder to cloud。
- Publish local page to cloud。
- Cloud workspace export to folder。
- 可选 sync,不作为 P0/P1。
## 13. 风险
### 13.1 最大风险:双真相
如果本地文件夹先导入 Convex,再从 Convex 渲染,就会变成:
```text
file system -> Convex mirror -> projection -> UI
```
这会制造双真相,也会让 VSCode-like 操作变慢、变怪。
必须避免。
### 13.2 页面身份风险
如果 page id 只依赖路径,移动/重命名会断引用。
P0 可以先只读,P1 写操作前必须完成 page id 方案。
### 13.3 Markdown 往返风险
Tiptap 文档和 Markdown 不是无损双向转换。
P0 可以先阅读态或有限编辑;P2 再处理复杂块、附件、引用、frontmatter。
### 13.4 安全风险
本地 source 必须限制在用户选择的 root 下:
- 禁止 `../` 越界。
- symlink 默认只显示,不默认跟随写入。
- 删除 / 覆盖必须有 preflight。
- watcher 不监听 root 外路径。
## 14. 非目标
- 不让 Convex 成为本地文件夹模式的中介。
- 不维护 local file tree 和 convex file tree 两套 UI。
- 不在前端用路径和 Convex record 临时拼第二套树。
- 不在 P0 做本地/云自动同步。
- 不在 P0 承诺 Markdown 与 Tiptap 完全无损互转。
- 不用本地 per-user 文件夹替代 Convex 协作权限。
## 15. 完成判定
本文进入 done 的条件:
- `WorkspaceSource` 抽象落地。
- local 和 convex 都能通过同一 command envelope 接入 tree runtime。
- `file_tree/page_tree/page_aggregate` 对 UI 的消费路径不分叉。
- 本地文件夹 P0 只读投影和 `.md` 页面打开可验证。
- 至少一组写操作在 local 和 Convex 上共用 UI、共用命令名、只分 source executor。
- 新增能力没有绕开 Rust kernel 或制造第二套树真相。