# [recycle] 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`,但不能因此维护两套文件树操作。 新的长期结构应固定为: ```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 承接。 - 两者共用同一套 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。 - 本地全文索引。 ### 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 渲染,就会变成: ```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 或制造第二套树真相。