- 为 Rust Web 主入口补齐本地文件夹/云空间切换、最近目录与路径回填体验\n- 对齐 local markdown media 与 inline marks 的 Rust shell / TipTap converter 语义\n- 让 documents/page 优先消费 Rust page aggregate snapshot,并保留 TS fallback\n- 补强 tree live、local markdown 与主入口 smoke,并同步设计稿状态
17 KiB
4-21 [done] Local Folder / Convex 统一树源架构 v1
更新时间:2026-05-06
关联:
/mnt/Data1T/mnote/design/04-tree-domain/done/4-20-vscode-explorer-file-tree-alignment-v1.md/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 项。
1. 结论
支持引入 Local Folder Workspace,但不能因此维护两套文件树操作。
新的长期结构应固定为:
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 workspace 的 source adapter,负责同步、协作、权限、分享和远程资源。- 两者共用同一套 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 是树域的存储源适配层。
最低接口:
sourceKindrootUriworkspaceIdcapabilitiesloadSnapshotwatchpreflightCommandexecuteCommandresolvePageAggregate
当前建议先支持两类:
local_folderconvex_workspace
3.2 SpaceRoot
SpaceRoot 是用户在 UI 里选择的工作区入口。
示例:
file:///home/lix/Documents/mnote-vaultconvex://workspace/ws_123
SpaceRoot 可以出现在同一个 Explorer 列表里,但每个 root 只能由一个 source adapter 持有。
3.3 Source Capability
capability 描述当前 source 对某项操作的真实支持程度。
示例:
create-filecreate-folderrenamemovecopytrashrestorepurgeuploadwatchsharepublishcollaborate
capability 不改变 UI 命令名,只控制菜单显隐、禁用原因和 preflight 结果。
4. 真源分层
4.1 存储真源
存储真源由 source 持有:
| source | 存储真源 |
|---|---|
local_folder |
本地文件系统 |
convex_workspace |
Convex 数据库 + 对象存储 |
本地模式不得先导入 Convex 再渲染。那会破坏本地文件夹模式的价值。
4.2 语义真源
树、页面和资源语义由 Rust kernel 统一生成:
KernelNodeKernelEdgefile_treepage_treepage_aggregatetree.*command result
source adapter 只负责读写存储,不允许 UI 直接从本地路径或 Convex record 拼树语义。
4.3 UI 本地态
继续服从 4-7:
expandedselectedfocusedhoverededitingNameclipboarddraggingdropTargetcontextMenuOpen
这些不进入 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 采用三层策略:
.mdfrontmatter 中存在mnote_id时使用它。- 不存在时在
.mnote/page-ids.json记录relativePath -> pageId。 - 临时未初始化文件夹可用 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.mdview- asset-folder
- asset
共同字段继续使用 4-4:
rowIdnodeIdparentNodeIdnodeTypeprojectionKindtitledepthpositionchildCountexpandablecapabilitiesresourceMetaiconHint
新增建议:
sourceKindsourceUrirelativePathstorageIdentityoperationProfile
这些应进入 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 命令路由
UI action
-> tree command envelope
-> Rust command router
-> source adapter preflight
-> source adapter execute
-> kernel event / projection resync
命令 envelope 必须携带:
sourceKindworkspaceIdrootUritargetNodeIdtargetResourceMetaselectionoperation
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。
如果用户想“本地建立每个用户的文件夹”,可以作为部署策略:
/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/filessrc/vs/platform/filessrc/vs/base/browser/ui/treesrc/vs/base/browser/ui/listsrc/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。
- 本地全文索引。
P3:Local 与 Cloud 的显式桥接
- Import local folder to cloud。
- Publish local page to cloud。
- Cloud workspace export to folder。
- 可选 sync,不作为 P0/P1。
13. 风险
13.1 最大风险:双真相
如果本地文件夹先导入 Convex,再从 Convex 渲染,就会变成:
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 或制造第二套树真相。