# 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 或制造第二套树真相。
