checkpoint before gfm ast parser design
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
# 4-20 [process] VSCode Explorer 文件树对标设计 v1
|
||||
|
||||
> 更新时间:2026-05-06
|
||||
>
|
||||
> 关联:
|
||||
> - `/mnt/Data1T/mnote/tmp/image copy 47.png`
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.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/04-tree-domain/done/4-14-task-005-file-tree-final-renderer-capability-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/90-reference/90-1-filetree.md`
|
||||
|
||||
## 1. 目的
|
||||
|
||||
这份文档只处理 `filesystem / Explorer` 文件树下一阶段怎么对标 VSCode Explorer。
|
||||
|
||||
它不重新定义树域协议。已有结论继续成立:
|
||||
|
||||
- `tree-first graph kernel` 是长期对象真相层。
|
||||
- `Convex` 继续作为本地自托管存储、实时、文件协作底座。
|
||||
- `mnote-web` 是 Rust Web 主执行面,负责 projection、command、transport 与 realtime stream。
|
||||
- 前端和 tree shell 只消费稳定 projection,并持有本地交互态。
|
||||
|
||||
本设计补的是此前文档没有写细的 Explorer 级显示和操作合同:行模型、选择/焦点、打开、右键、键盘、拖放、剪贴板、新建、重命名、删除、上传、资源移动和验收清单。
|
||||
|
||||
## 2. 当前截图判断
|
||||
|
||||
`/mnt/Data1T/mnote/tmp/image copy 47.png` 显示当前文件树已经有 Explorer 入口,但仍不像 VSCode Explorer:
|
||||
|
||||
- `我的页面` 下直接混排页面节点和 `index.md`,但没有清晰的 workspace root / resource root 语义。
|
||||
- 页面行使用小菱形 / 折叠箭头,文件行使用蓝色文档图标,信息密度接近,但行列网格不稳定。
|
||||
- 当前选中态落在 `index.md` 文件行,和右侧文档页 `新页面` 的 active / focus / selection 关系不够清楚。
|
||||
- `Explorer` 标题像 tab 文案,不像 VSCode Explorer 的 section header + toolbar。
|
||||
- hover 操作只看到 `+`,缺少新建文件、新建文件夹、刷新、全部折叠、更多等容器级动作。
|
||||
- 长列表中大量重复 `新页面/index.md`,缺少资源装饰、路径 disambiguation、命名冲突处理和 reveal 规则。
|
||||
|
||||
结论:当前不是“样式再修一点”的问题,而是 Explorer 行为合同还没有写到足够细。
|
||||
|
||||
## 3. 唯一真源
|
||||
|
||||
### 3.1 结构显示真源
|
||||
|
||||
文件树显示的唯一真源固定为:
|
||||
|
||||
`kernel.project_view(file_tree)` 产出的 `KernelProjectionResult`。
|
||||
|
||||
当前代码证据:
|
||||
|
||||
- 首屏 query 会从 Convex `sidebar.datasetList` 取数据,再通过 `resolveKernelFileTreeProjection` 调 Rust `kernel.project_view(file_tree)` 附加 `kernel_file_tree_projection`。
|
||||
- 实时链路 `/api/mnote-web/stream` snapshot 也会附加同一份 file tree projection。
|
||||
- Rust 侧生成点是 `bridge-runtime` 的 `build_file_tree_projection_result`。
|
||||
- projection item 的核心字段已经在 `core-protocol/src/kernel.rs` 与前端 `tree-protocol.ts` 固定。
|
||||
|
||||
因此,Explorer 不能在 UI 层重新推导层级、排序、资源类型、child count、capabilities 或合法性。UI 只能把 projection item materialize 成 row。
|
||||
|
||||
### 3.2 持久化操作真源
|
||||
|
||||
持久化动作的唯一正式命令面固定为:
|
||||
|
||||
- 页面:`tree.node.*`、`tree.subtree.*`
|
||||
- 资源:`tree.resource.*`
|
||||
- 文件树预检:`tree.filetree.*.preflight`
|
||||
|
||||
当前已经成立:
|
||||
|
||||
- 页面新建、重命名、移动、归档、恢复、永久删除、嵌入、复制子树已经接到 `tree.*`。
|
||||
- 文件树 `drop/delete/paste/upload-target` 已有 Rust preflight。
|
||||
- 资源 `copy/move/upload` 已有 `tree.resource.copy/move/upload`。
|
||||
|
||||
当前仍未完成:
|
||||
|
||||
- 资源 `rename/delete/restore` 仍经过 legacy `/api/media/batch` 直接 patch / restore。
|
||||
- `cut` 还没有成为剪贴板动作。
|
||||
- 资源“移动到...”仍有 `prompt("输入目标页面 ID")` 的临时 UX。
|
||||
|
||||
### 3.3 本地交互态
|
||||
|
||||
这些状态只能属于 tree shell / runtime 本地态,不得写进 projection:
|
||||
|
||||
- `expanded`
|
||||
- `selected`
|
||||
- `focused`
|
||||
- `hovered`
|
||||
- `editingName`
|
||||
- `clipboard`
|
||||
- `dragging`
|
||||
- `dropTarget`
|
||||
- `contextMenuOpen`
|
||||
- `keyboardAnchor`
|
||||
- `revealPending`
|
||||
|
||||
`expandedByDefault` 只是 projection 给初始展开的建议,不是展开态真相。
|
||||
|
||||
## 4. VSCode Explorer 对标边界
|
||||
|
||||
### 4.1 必须对齐
|
||||
|
||||
文件树必须对齐 VSCode Explorer 的资源管理器模型:
|
||||
|
||||
- 一行就是一个资源命中目标。
|
||||
- `twistie` 只负责展开/折叠,打开资源是另一种动作。
|
||||
- 图标来自 `rowKind/resourceMeta/iconHint/assetKind`,不是装饰。
|
||||
- `selection`、`focus`、`hover`、`active` 分离。
|
||||
- 右键点击已选区内资源时保留选区,右键点击未选资源时切换到该资源。
|
||||
- 键盘能完整操作可见行。
|
||||
- 内部拖拽、外部文件拖入、剪贴板和 context menu 都基于同一 selection/focus 状态。
|
||||
- 所有危险或跨资源类型动作先走 preflight plan,再落正式 command。
|
||||
|
||||
### 4.2 不能照搬
|
||||
|
||||
文件树不能照搬 VSCode 的真实文件系统语义:
|
||||
|
||||
- mnote 的文件树是页面、`index.md`、附件、mindmap 文件夹、导图子资源、table/book/pdf 等复合资源投影,不是磁盘目录树。
|
||||
- `page -> index.md -> asset-folder -> asset` 这类多语义链路不能用 VSCode `compact folders` 强行压成一行。
|
||||
- “在终端打开”“Reveal in Finder/Explorer”“文件比较”等 OS 动作不进入默认合同,除非资源模型已有明确承载。
|
||||
- VSCode 的单击预览 / 双击固定标签页节奏不能直接照搬。mnote 的打开必须服从当前文档页、Page Aggregate、附件打开链路和权限边界。
|
||||
- Explorer 是 sidebar 中的创新视图,不是整个产品的唯一主导航。`section` 页面树仍保留 Wolai / Notion 方向。
|
||||
|
||||
## 5. 显示合同
|
||||
|
||||
### 5.1 Row Grid
|
||||
|
||||
每个 file tree row 固定为以下视觉列:
|
||||
|
||||
| 列 | 说明 |
|
||||
| --- | --- |
|
||||
| indent | 只由 `depth` 决定,步长固定 |
|
||||
| twistie | 仅 `expandable=true` 时可见并可点击 |
|
||||
| icon | 由 `iconHint/assetKind/resourceKind` 决定 |
|
||||
| label | 主名称,单行截断 |
|
||||
| decoration | 条件性 badge、状态色、资源提示 |
|
||||
| actions | hover / focus 时出现的轻量动作 |
|
||||
|
||||
不得让 label 长度、hover 按钮或选中态改变行高和缩进。
|
||||
|
||||
### 5.2 Row Kind
|
||||
|
||||
当前稳定行类型继续保留:
|
||||
|
||||
- `doc`
|
||||
- `index`
|
||||
- `asset-folder`
|
||||
- `asset`
|
||||
|
||||
后续如果需要真实目录或资源组,必须先扩展 Rust projection 的 `rowKind/resourceKind`,不能在 UI 层把某类 asset 临时伪装成目录。
|
||||
|
||||
### 5.3 Root
|
||||
|
||||
Explorer 顶部应有明确 root:
|
||||
|
||||
- 单工作区:显示 workspace root,root 下再进入页面/资源树。
|
||||
- 多工作区:允许多个 workspace root。
|
||||
- root 行是否可折叠由 projection capability 决定。
|
||||
|
||||
当前截图中 `我的页面 -> Explorer` 的组合不够清晰,后续应改为:
|
||||
|
||||
- section header:`EXPLORER`
|
||||
- root row:当前 workspace 名称
|
||||
- toolbar:新建页面 / 新建资源 / 刷新 / 全部折叠 / 更多
|
||||
|
||||
### 5.4 Icon 与 Decoration
|
||||
|
||||
图标语义至少覆盖:
|
||||
|
||||
- workspace / folder / page / index
|
||||
- asset
|
||||
- image / video / audio
|
||||
- pdf / book
|
||||
- table
|
||||
- mindmap
|
||||
- unknown
|
||||
|
||||
Decoration 只表达轻量状态,例如:
|
||||
|
||||
- stale / indexing / uploading
|
||||
- read-only / shared / public
|
||||
- conflict / invalid target
|
||||
- cut clipboard visual
|
||||
|
||||
Decoration 不得变成第二行副标题。
|
||||
|
||||
### 5.5 Active、Selection、Focus
|
||||
|
||||
三者必须分离:
|
||||
|
||||
- `active`:右侧主区域正在打开的 document / asset。
|
||||
- `selection`:Explorer 当前选中集合,支持多选。
|
||||
- `focus`:键盘焦点所在 row。
|
||||
|
||||
截图中 `index.md` 选中与右侧 `新页面` active 的关系不清晰。目标规则:
|
||||
|
||||
- 打开文档时,`doc` 与 `index` 可共享 active 文档语义,但只能有一个 focused row。
|
||||
- 如果 active document 映射到 `index.md`,应显式定义 active 高亮落在 `index` 还是 `doc`。建议:active 落 `index.md`,doc 行只作为目录/页面容器。
|
||||
- 多选时 active 不覆盖 selection,selection 背景优先。
|
||||
|
||||
## 6. 操作合同
|
||||
|
||||
### 6.1 打开
|
||||
|
||||
P0:
|
||||
|
||||
- `Enter` 打开 focused row。
|
||||
- 双击 row 打开资源。
|
||||
- 单击只负责 selection/focus,不自动打开重资源。
|
||||
- `index.md` 打开对应文档页。
|
||||
- asset 打开对应资源 viewer / editor。
|
||||
|
||||
P1:
|
||||
|
||||
- 支持“Reveal active resource in Explorer”。
|
||||
- 页面切换后自动展开祖先链并定位 active row。
|
||||
- 搜索过滤后保留 active/reveal 的路径上下文。
|
||||
|
||||
说明:当前代码已明确与 VSCode 单击打开不同,这是合理差异,但必须写成正式合同,避免后续反复摇摆。
|
||||
|
||||
### 6.2 展开 / 折叠
|
||||
|
||||
P0:
|
||||
|
||||
- 点击 twistie 只展开/折叠,不打开。
|
||||
- `ArrowRight`:可展开行未展开则展开;已展开或叶子则可进入打开动作,具体由 runtime 定义。
|
||||
- `ArrowLeft`:已展开则折叠;未展开则焦点移动到父节点。
|
||||
- `Home/End` 跳到第一/最后可见行。
|
||||
|
||||
P1:
|
||||
|
||||
- `Collapse All` 容器动作。
|
||||
- 外部拖拽悬停到可展开目标一段时间后自动展开。
|
||||
|
||||
### 6.3 选择与右键
|
||||
|
||||
P0:
|
||||
|
||||
- 单击:单选并设置 anchor/focus。
|
||||
- `Ctrl/Cmd + Click`:切换选中。
|
||||
- `Shift + Click`:从 anchor 到目标做范围选。
|
||||
- 右键已选区内 row:保留选区并打开菜单。
|
||||
- 右键未选 row:切换 selection 到该 row,再打开菜单。
|
||||
- 空白区点击:清空 selection/focus。
|
||||
|
||||
P1:
|
||||
|
||||
- `Ctrl/Cmd + A` 选中当前 Explorer 可见资源。
|
||||
- 过滤态下范围选只覆盖可见 row。
|
||||
|
||||
### 6.4 键盘
|
||||
|
||||
P0 最小按键:
|
||||
|
||||
| 按键 | 动作 |
|
||||
| --- | --- |
|
||||
| `ArrowUp/ArrowDown` | 上下移动 focus |
|
||||
| `ArrowLeft/ArrowRight` | 折叠 / 展开 / 父子导航 |
|
||||
| `Home/End` | 首尾行 |
|
||||
| `Enter` | 打开 focused row |
|
||||
| `F2` | 重命名 |
|
||||
| `Delete/Backspace` | 删除 / 归档 |
|
||||
| `Ctrl/Cmd+C` | copy |
|
||||
| `Ctrl/Cmd+X` | cut |
|
||||
| `Ctrl/Cmd+V` | paste |
|
||||
| `Esc` | 取消 inline rename / 关闭菜单 / 清理 drag feedback |
|
||||
|
||||
当前缺口:已有 copy/paste,但缺 `cut`;键盘能力需要以 runtime contract 覆盖 file tree,而不是散在 `sidebar.tsx`。
|
||||
|
||||
### 6.5 新建
|
||||
|
||||
P0:
|
||||
|
||||
- `New Page`:映射 `tree.node.create`,在目标 doc/root 下创建页面,并进入 inline rename。
|
||||
- `New File`:只允许创建 mnote 资源模型内真实存在的资源类型;如果当前没有通用文件创建命令,入口应隐藏或置灰。
|
||||
- `New Folder`:当前没有真实目录语义,不提供默认入口;mindmap asset-folder 不是通用文件夹。
|
||||
|
||||
P1:
|
||||
|
||||
- 支持上传文件作为 `New File` 的可用替代路径,走 `tree.filetree.upload-target.preflight -> tree.resource.upload`。
|
||||
- 如果未来引入通用资源目录,先扩展 kernel projection / command,再开启 `New Folder`。
|
||||
|
||||
### 6.6 重命名
|
||||
|
||||
P0:
|
||||
|
||||
- `F2` 和 context menu `Rename` 进入 inline rename,不再使用 `prompt`。
|
||||
- doc / index 重命名最终落 `tree.node.rename` 或 Page Aggregate title command 的正式命名面。
|
||||
- asset 重命名必须从 legacy `/api/media/batch` 收口到 `tree.resource.rename` 或等价正式命令。
|
||||
- `Enter` 提交,`Esc` 取消。
|
||||
- 空名、重名、非法字符、只读资源必须由 preflight / command 返回可解释错误。
|
||||
|
||||
P1:
|
||||
|
||||
- 文件型 asset 默认只选中文件名 stem,不误选扩展名。
|
||||
- rename 期间保留 row 高度和缩进,不触发布局跳动。
|
||||
|
||||
### 6.7 删除 / 恢复 / 永久删除
|
||||
|
||||
P0:
|
||||
|
||||
- doc 删除走 `tree.node.archive`。
|
||||
- doc 恢复走 `tree.node.restore`。
|
||||
- doc 永久删除走 `tree.node.purge`,必须是更强确认路径。
|
||||
- asset 删除 / 恢复必须补正式 `tree.resource.archive/restore` 或等价命令,不能长期停留在 legacy `/api/media/batch`。
|
||||
- 多选删除先走 `tree.filetree.delete.preflight`,展示将影响的 doc/asset 集合。
|
||||
|
||||
P1:
|
||||
|
||||
- 支持删除后 undo / toast action,命令结果仍以 kernel event 和 projection resync 为准。
|
||||
|
||||
### 6.8 剪贴板
|
||||
|
||||
P0:
|
||||
|
||||
- `copy`:复制选中 doc subtree / asset resource。
|
||||
- `cut`:进入 cut clipboard 状态,源 row 降噪显示;paste 成功后清空 cut 状态。
|
||||
- `paste`:目标由 focused row / context target / active doc 推导,必须先走 `tree.filetree.paste.preflight`。
|
||||
- 多选 paste 保持原有可见顺序,并过滤父子重复选择。
|
||||
|
||||
P1:
|
||||
|
||||
- 系统剪贴板只保存安全 payload,不暴露敏感内部 token。
|
||||
- 跨窗口 paste 需要校验 workspace 和权限。
|
||||
|
||||
### 6.9 拖放
|
||||
|
||||
P0:
|
||||
|
||||
- 内部拖放支持 move/copy,修饰键控制 copy。
|
||||
- 外部文件拖入支持 upload target preflight。
|
||||
- 非法目标即时反馈,不发持久化命令。
|
||||
- doc 拖到自身或后代节点必须拒绝。
|
||||
- asset-folder / asset 的可投放能力由 `capabilities` 和 preflight 决定。
|
||||
|
||||
P1:
|
||||
|
||||
- 拖到可展开目标 hover 延迟后自动展开。
|
||||
- 显示 before / inside / after 三种 drop indicator,而不是只有整行背景。
|
||||
- 多选拖放过滤已被父节点包含的子节点。
|
||||
|
||||
### 6.10 Context Menu
|
||||
|
||||
菜单按 `rowKind/resourceKind/selection` 裁剪:
|
||||
|
||||
| 目标 | P0 动作 |
|
||||
| --- | --- |
|
||||
| root | 新建页面、上传文件、刷新、全部折叠 |
|
||||
| doc | 打开、新建子页面、重命名、复制、剪切、粘贴、移动到、归档、复制链接 |
|
||||
| index | 打开、复制、剪切、粘贴、Reveal 页面 |
|
||||
| asset-folder | 打开、复制、剪切、粘贴、重命名、删除、移动到 |
|
||||
| asset | 打开、复制、剪切、重命名、删除、下载、移动到 |
|
||||
| 多选 | 打开禁用、复制、剪切、删除、移动到 |
|
||||
|
||||
当前缺口:
|
||||
|
||||
- mindmap rename 提示不支持。
|
||||
- table move/download 提示不支持。
|
||||
- asset 移动仍靠输入页面 ID。
|
||||
|
||||
这些必须进入 P0/P1 差异清单,不得被样式修复掩盖。
|
||||
|
||||
## 7. 资源排序与混排规则
|
||||
|
||||
P0 固定当前最小规则:
|
||||
|
||||
1. workspace root
|
||||
2. doc row
|
||||
3. doc 的 `index.md`
|
||||
4. doc 的 direct assets / asset folders
|
||||
5. child documents
|
||||
|
||||
排序由 Rust projection 的 `position` 与 preorder 决定。UI 只渲染。
|
||||
|
||||
需要新增到 projection meta 的候选规则:
|
||||
|
||||
- `ordering: "kernel_file_tree_preorder"`
|
||||
- `folderPolicy: "mnote_composite_resource"`
|
||||
- `indexPlacement: "first_child"`
|
||||
- `assetPlacement: "after_index_before_child_documents"`
|
||||
|
||||
这样可以避免 UI 通过 hardcode 解释为什么 `index.md` 总是出现在页面下第一行。
|
||||
|
||||
## 8. Search / Filter / Reveal
|
||||
|
||||
P0:
|
||||
|
||||
- Explorer filter 只改变可见 row,不改变 projection truth。
|
||||
- filter 命中子节点时必须补全祖先链。
|
||||
- active row 在 filter 后不可见时,保留 active 状态但不强制显示。
|
||||
- `Reveal in Explorer` 必须展开祖先链、滚动到目标 row、设置 focus,不破坏 selection,除非调用者明确要求 select。
|
||||
|
||||
P1:
|
||||
|
||||
- 支持 `maxResults` 下的 ancestor completion。
|
||||
- 支持 indexing visibility,展示 stale / refreshing 状态。
|
||||
|
||||
当前 `KernelFileTreeProjection.meta.search.indexingVisibility` 已有雏形,后续应把它用于 UI 状态,而不是在 Explorer 自行发明刷新状态。
|
||||
|
||||
## 9. 分阶段任务
|
||||
|
||||
### P0:让当前 Explorer 行为不再含糊
|
||||
|
||||
- 固定 active / selection / focus 的视觉优先级。
|
||||
- 补齐 file tree keyboard runtime:`Arrow/Home/End/Enter/F2/Delete/Cut/Copy/Paste/Esc`。
|
||||
- 补 `cut` clipboard 状态和 cut row decoration。
|
||||
- 把 doc / index / asset / asset-folder 的 context menu 动作裁剪成合同。
|
||||
- 移除资源移动的 `prompt("输入目标页面 ID")`,改用 tree picker / move target picker。
|
||||
- asset rename/delete/restore 制定 `tree.resource.*` 正式命令或等价 bridge 命名,并保留 legacy alias。
|
||||
- 写 smoke 固化当前截图中重复页面 + `index.md` 场景的 active/selection/reveal 行为。
|
||||
|
||||
### P1:让资源管理器达到 VSCode 常用操作预期
|
||||
|
||||
- inline rename 全面替代 prompt。
|
||||
- root toolbar 对齐 Explorer:新建页面、上传文件、刷新、全部折叠、更多。
|
||||
- 拖放支持 before / inside / after indicator 与 hover auto expand。
|
||||
- 上传 target preflight 失败时在 drop target 上显示明确反馈。
|
||||
- 多选 delete / move / copy 的 preflight plan 显示影响集合。
|
||||
- mindmap/table 的 rename/move/download 能力从“不支持”变为可解释菜单状态或正式实现。
|
||||
|
||||
### P2:高级 Explorer 能力
|
||||
|
||||
- compact folders 只用于未来真实目录链,不用于页面/附件复合链。
|
||||
- git-like / sync-like decorations,如果未来有对应数据源再加入。
|
||||
- 多 workspace root。
|
||||
- 大树虚拟化和增量加载。
|
||||
- 跨窗口剪贴板。
|
||||
|
||||
## 10. 验收
|
||||
|
||||
### 10.1 单元 / 合同测试
|
||||
|
||||
必须覆盖:
|
||||
|
||||
- projection item 到 row model 的稳定映射。
|
||||
- active / selection / focus 分离。
|
||||
- `Ctrl/Cmd` 多选、`Shift` 范围选、右键保留选区。
|
||||
- 可见 row 变化后的 selection normalize。
|
||||
- `cut/copy/paste` payload encode/decode 与目标推导。
|
||||
- drop preflight payload 构造。
|
||||
- illegal drop:自身、后代、跨 workspace、只读资源。
|
||||
- inline rename submit/cancel。
|
||||
|
||||
### 10.2 Rust 测试
|
||||
|
||||
必须覆盖:
|
||||
|
||||
- `kernel.project_view(file_tree)` 输出 `doc/index/asset-folder/asset`。
|
||||
- `resourceMeta`、`capabilities`、`iconHint` 稳定。
|
||||
- `tree.filetree.*.preflight` 对多选和非法目标返回 plan。
|
||||
- 新增 `tree.resource.rename/archive/restore` 或等价命令后,覆盖 command event 与 projection resync。
|
||||
|
||||
### 10.3 浏览器 smoke
|
||||
|
||||
必须覆盖:
|
||||
|
||||
- 当前截图同类数据:多个同名 `新页面`,每个页面下有 `index.md`。
|
||||
- 点击 `index.md` 后右侧打开对应页面,Explorer active/focus/selection 可解释。
|
||||
- 右键已选区保留多选。
|
||||
- `F2` inline rename,`Esc` 取消,`Enter` 提交。
|
||||
- `Ctrl/Cmd+X` 后 cut decoration 出现,paste 成功后消失。
|
||||
- 外部文件拖入 doc / index / asset-folder 的目标差异。
|
||||
- 资源移动不再弹 `prompt`。
|
||||
|
||||
## 11. 非目标
|
||||
|
||||
- 不把文件树改成真实磁盘文件系统。
|
||||
- 不把 `Explorer` 替代 `section` 页面树作为唯一导航。
|
||||
- 不在 UI 层重新拼排序、层级、资源类型或权限。
|
||||
- 不为了像 VSCode 而绕过 Page Aggregate、tree projection 或 `tree.*` 命令。
|
||||
- 不把 `90-1-filetree.md` 的 Rust 生态调研当作实现模板;它只作为参考资料。
|
||||
|
||||
## 12. 完成判定
|
||||
|
||||
这份设计进入 done 的条件:
|
||||
|
||||
- P0 项全部有代码和 smoke 覆盖。
|
||||
- file tree 的 active / selection / focus / keyboard / context menu / clipboard / DnD 有稳定 runtime contract。
|
||||
- 资源 rename/delete/restore 不再只依赖 legacy `/api/media/batch` 主路径。
|
||||
- 当前截图暴露的 Explorer 行为歧义能通过测试说明和验证。
|
||||
- 新增能力没有制造第二套树真相。
|
||||
@@ -0,0 +1,586 @@
|
||||
# 4-21 [process] Local Folder / Convex 统一树源架构 v1
|
||||
|
||||
> 更新时间:2026-05-06
|
||||
>
|
||||
> 关联:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/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`
|
||||
|
||||
## 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 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` 是树域的存储源适配层。
|
||||
|
||||
最低接口:
|
||||
|
||||
- `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 或制造第二套树真相。
|
||||
+590
@@ -0,0 +1,590 @@
|
||||
# 4-22 [process] Local Folder / Convex 统一树源顺序执行清单 v1
|
||||
|
||||
> 更新时间:2026-05-06
|
||||
>
|
||||
> 来源:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-20-vscode-explorer-file-tree-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-21-local-folder-convex-unified-tree-source-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`
|
||||
|
||||
## 0. 执行原则
|
||||
|
||||
本清单按顺序执行,不跳阶段。
|
||||
|
||||
总目标:
|
||||
|
||||
> **只维护一套 tree projection、tree command、preflight、runtime state 和 UI;Local Folder 与 Convex 只作为不同 `WorkspaceSource` executor 存在。**
|
||||
|
||||
硬约束:
|
||||
|
||||
- [x] 不让本地文件夹先导入 Convex 再渲染。
|
||||
- [x] 不在 UI 层用本地 path 或 Convex record 拼第二套树。
|
||||
- [x] 不新增 local 专用文件树 UI。
|
||||
- [x] 不绕过 Rust kernel 写页面树、文件树、排序或权限。
|
||||
- [x] 不把 `expanded/selected/focus/drag/drop/contextMenu` 写进 projection。
|
||||
- [x] 所有写操作先 preflight,再 execute,再 projection resync。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 本清单对应实现中,Local 和 Convex 都通过同一 command envelope 进入 Rust command router。
|
||||
- `file_tree/page_tree/page_aggregate` 的 UI 消费路径不因 source 分叉。
|
||||
- 本地文件夹与 Convex 的差异只通过 capability / preflight / executor 表达。
|
||||
|
||||
## 1. Phase A:冻结 Source 与协议边界
|
||||
|
||||
### A1. 定义 `WorkspaceSource` 合同
|
||||
|
||||
- [x] 在 Rust 侧定义 `WorkspaceSource` trait / interface 草案。
|
||||
- [x] 字段至少覆盖 `sourceKind`、`rootUri`、`workspaceId`、`capabilities`。
|
||||
- [x] 方法至少覆盖 `loadSnapshot`、`watch`、`preflightCommand`、`executeCommand`、`resolvePageAggregate`。
|
||||
- [x] 明确 `local_folder` 和 `convex_workspace` 两个 source kind。
|
||||
- [x] 写 contract test,验证 source kind 和 root uri 会进入 command envelope。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- Rust 代码中存在统一 `WorkspaceSource` 抽象。
|
||||
- 至少有 mock source 能输出 `file_tree/page_tree` snapshot。
|
||||
- 前端没有新增 source-specific tree component。
|
||||
|
||||
### A2. 扩展 projection source metadata
|
||||
|
||||
- [x] 为 projection item 补 source metadata 方案。
|
||||
- [x] 字段候选固定为 `sourceKind`、`sourceUri`、`relativePath`、`storageIdentity`、`operationProfile`。
|
||||
- [x] 优先放入 `resourceMeta.extra`,除非需要正式扩展 `core-protocol`。
|
||||
- [x] 更新 TS 类型映射。
|
||||
- [x] 补 fixture:local folder item、convex item 同时存在。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- `file_tree` item 可以区分来自 local 还是 convex。
|
||||
- UI 仍只读 projection 字段,不直接访问 source adapter。
|
||||
- fixture 覆盖 `directory/md file/normal file/document/index/asset`。
|
||||
|
||||
### A3. 统一 command envelope
|
||||
|
||||
- [x] 定义 `TreeCommandEnvelope` 的 source 字段。
|
||||
- [x] envelope 至少包含 `sourceKind`、`workspaceId`、`rootUri`、`targetNodeId`、`targetResourceMeta`、`selection`、`operation`。
|
||||
- [x] 现有 `tree.node.*`、`tree.subtree.*`、`tree.resource.*` 进入统一 router。
|
||||
- [x] router 按 source kind 分发到 executor。
|
||||
- [x] 对 unknown source / capability missing 返回可解释错误。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 同一个 UI action 可以构造同一种 envelope。
|
||||
- local / convex 的 executor 可以在测试中分别接收 envelope。
|
||||
- 没有新增 `/api/local-filetree/*` 这类平行命令入口。
|
||||
|
||||
## 2. Phase B:Local Folder 只读主链
|
||||
|
||||
### B1. 打开本地文件夹入口
|
||||
|
||||
- [x] 增加选择本地文件夹的入口。
|
||||
- [x] 生成 `SpaceRoot=file://...`。
|
||||
- [x] 记录最近打开的 local root。
|
||||
- [x] 禁止 root 外路径访问。
|
||||
- [x] 对无权限目录返回明确错误。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 用户能选择一个本地文件夹并进入 local workspace。
|
||||
- 3000 `desktop:hot` 主入口不能跳到 `/tree` debug shell,也不能落入 `legacy_next_compat_disabled`。
|
||||
- Convex workspace 入口不受影响。
|
||||
- smoke 能断言当前 source kind 为 `local_folder`。
|
||||
|
||||
### B2. LocalFolderSource 只读扫描
|
||||
|
||||
- [x] 扫描目录。
|
||||
- [x] 识别 directory。
|
||||
- [x] 识别 `.md` 文件。
|
||||
- [x] 识别普通文件。
|
||||
- [x] 默认忽略 `.git`、`node_modules`、`.mnote/trash` 等高噪音目录。
|
||||
- [x] 处理 symlink:P0 只显示,不默认跟随写入。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 本地文件夹能输出 read-only snapshot。
|
||||
- snapshot 不越过 root。
|
||||
- 扫描失败不会让整个 app 崩溃。
|
||||
|
||||
### B3. 输出 local `file_tree`
|
||||
|
||||
- [x] directory 映射为 folder row。
|
||||
- [x] `.md` 映射为 markdown file row。
|
||||
- [x] 普通文件映射为 asset row。
|
||||
- [x] iconHint 覆盖 markdown/image/video/audio/pdf/book/table/unknown。
|
||||
- [x] childCount、depth、parentNodeId、position 由 kernel 生成。
|
||||
- [x] 排序规则先固定为 VSCode default:folder first,name sort。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- `kernel.project_view(file_tree)` 可以返回 local folder projection。
|
||||
- UI 使用同一 Explorer surface 渲染 local projection。
|
||||
- 浏览器 smoke 可看到真实目录、`.md`、普通文件。
|
||||
|
||||
### B4. 输出 md-only `page_tree`
|
||||
|
||||
- [x] 只把 `.md` 文件纳入 page tree。
|
||||
- [x] 目录只作为页面树分组/父级,不作为正文页面打开。
|
||||
- [x] 页面标题优先级固定为 frontmatter `title` > 第一行 H1 > 文件名。
|
||||
- [x] parent/child 来自目录层级。
|
||||
- [x] 同名 md 文件必须通过 path / page id 区分。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 同一个 local root 下,Explorer 显示所有文件,页面树只显示 `.md`。
|
||||
- 点击页面树 md 节点能定位到同一个 `.md` resource。
|
||||
- 不因过滤普通文件影响 page tree 层级。
|
||||
|
||||
### B5. `.md` 只读 Page Aggregate
|
||||
|
||||
- [x] 读取 `.md` 文件。
|
||||
- [x] 解析 frontmatter。
|
||||
- [x] 解析标题。
|
||||
- [x] 生成只读 `page_aggregate`。
|
||||
- [x] 文档页用现有 `DocumentShell / DocumentContent` 消费。
|
||||
- [x] 文件读取失败显示错误页,不清空 Explorer。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 点击本地 `.md` 文件能打开文档阅读态。
|
||||
- 文档页不关心内容来自 local 还是 Convex。
|
||||
- `page_aggregate` 字段满足 `5-5-1` 最小契约。
|
||||
|
||||
## 3. Phase C:页面身份与本地元数据
|
||||
|
||||
### C1. 本地 page id 策略
|
||||
|
||||
- [x] 支持 frontmatter `mnote_id`。
|
||||
- [x] 支持 `.mnote/page-ids.json`。
|
||||
- [x] 支持 path-derived 临时 id,但仅用于未初始化只读模式。
|
||||
- [x] page id 生成必须稳定且不与 Convex document id 冲突。
|
||||
- [x] 写测试覆盖 rename/move 后 id 保持。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 重命名或移动 `.md` 后,page id 不变。
|
||||
- 没有 id 的文件能被安全初始化。
|
||||
- 同名不同路径 `.md` 不冲突。
|
||||
|
||||
### C2. 本地 `.mnote` 元数据目录
|
||||
|
||||
- [x] 定义 `.mnote/page-ids.json`。
|
||||
- [x] 定义 `.mnote/page-options.json`。
|
||||
- [x] 定义 `.mnote/trash` 或系统 trash 策略。
|
||||
- [x] `.mnote` 不出现在默认 Explorer 展示中,除非开启 debug。
|
||||
- [x] 元数据写入必须原子化或有失败回滚策略。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- `.mnote` 元数据能支持 page id 和页面设置。
|
||||
- 默认用户视图不被 `.mnote` 噪音污染。
|
||||
- 元数据损坏时有可解释错误和恢复建议。
|
||||
|
||||
## 4. Phase D:统一 Explorer P0 交互
|
||||
|
||||
### D1. Active / Selection / Focus 分离
|
||||
|
||||
- [x] 统一 row state:`activeRowId`、`selectedRowIds`、`focusedRowId`。
|
||||
- [x] active 表示右侧打开资源。
|
||||
- [x] selection 表示 Explorer 操作集合。
|
||||
- [x] focus 表示键盘焦点。
|
||||
- [x] local 和 convex 共用 reducer。
|
||||
- [x] 多选时 selection 视觉优先于 active。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 截图中的 `index.md` active/focus/selection 歧义被测试覆盖。
|
||||
- local `.md` 和 Convex `index.md` 使用同一视觉规则。
|
||||
- selection 不回写 projection。
|
||||
|
||||
### D2. 键盘导航
|
||||
|
||||
- [x] `ArrowUp/ArrowDown` 移动 focus。
|
||||
- [x] `ArrowLeft/ArrowRight` 折叠 / 展开 / 父子导航。
|
||||
- [x] `Home/End` 跳首尾。
|
||||
- [x] `Enter` 打开 focused row。
|
||||
- [x] `F2` 进入 inline rename。
|
||||
- [x] `Delete/Backspace` 触发删除 preflight。
|
||||
- [x] `Ctrl/Cmd+C` copy。
|
||||
- [x] `Ctrl/Cmd+X` cut。
|
||||
- [x] `Ctrl/Cmd+V` paste。
|
||||
- [x] `Esc` 取消 rename / menu / drag feedback。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 键盘能力在 runtime contract 中测试,不只散落在 React handler。
|
||||
- local 和 convex 使用同一组按键浏览器 smoke。
|
||||
- `cut` 不再缺失。
|
||||
|
||||
### D3. 选择与右键
|
||||
|
||||
- [x] 单击单选并设置 anchor/focus。
|
||||
- [x] `Ctrl/Cmd + Click` 切换选中。
|
||||
- [x] `Shift + Click` 范围选。
|
||||
- [x] 右键已选区内 row 保留选区。
|
||||
- [x] 右键未选 row 切换到该 row。
|
||||
- [x] 空白区点击清空 selection/focus。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 多选 context menu 不会被右键误清空。
|
||||
- local/convex 的 selection reducer 共用。
|
||||
- 过滤可见行变化后 selection normalize 正确。
|
||||
- 已选区内右键、未选项右键、空白区点击三条路径都必须在真实浏览器中验证。
|
||||
|
||||
### D4. Row grid 与 root toolbar
|
||||
|
||||
- [x] 固定 indent / twistie / icon / label / decoration / actions 六列。
|
||||
- [x] section header 使用 `EXPLORER`。
|
||||
- [x] root row 明确显示 local folder 或 cloud workspace。
|
||||
- [x] toolbar 至少有新建、上传/导入、刷新、全部折叠、更多。
|
||||
- [x] toolbar action 由 capability 控制可用性。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- hover action 不改变行高和缩进。
|
||||
- local root 与 Convex root 在同一 Explorer 框架下展示。
|
||||
- toolbar 不创建 source-specific UI 分支。
|
||||
- hover / focus 显示 actions 时,row height、indent、twistie/icon/label 列位置必须有浏览器截图或 DOM 断言。
|
||||
|
||||
## 5. Phase E:统一 Preflight 与 Context Menu
|
||||
|
||||
### E1. Context menu profile
|
||||
|
||||
- [x] root 菜单:新建页面、新建文件夹、上传/导入、刷新、全部折叠。
|
||||
- [x] md/page 菜单:打开、重命名、复制、剪切、粘贴、移动到、删除、Reveal。
|
||||
- [x] folder 菜单:新建页面、新建文件夹、粘贴、重命名、删除。
|
||||
- [x] asset 菜单:打开、重命名、复制、剪切、删除、下载、移动到。
|
||||
- [x] 多选菜单:复制、剪切、删除、移动到。
|
||||
- [x] share/publish 只在 Convex capability 存在时出现。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 菜单项由 rowKind/resourceKind/capability/selection 共同裁剪。
|
||||
- 不支持的能力有禁用原因或隐藏规则。
|
||||
- mindmap/table 的“不支持”状态被 capability 表达,而不是散落提示。
|
||||
|
||||
### E2. Preflight result UI
|
||||
|
||||
- [x] delete preflight 展示影响集合。
|
||||
- [x] move preflight 展示目标和冲突。
|
||||
- [x] paste preflight 展示命名冲突策略。
|
||||
- [x] upload/drop preflight 展示目标是否可写。
|
||||
- [x] readonly / permission denied / root escape 返回统一错误结构。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- local 和 convex 的 preflight 使用同一 UI 组件展示。
|
||||
- 失败时不发 execute。
|
||||
- 危险动作不会静默执行。
|
||||
|
||||
## 6. Phase F:Local 写操作 P1
|
||||
|
||||
### F1. 新建
|
||||
|
||||
- [x] `tree.node.create` 在 local source 创建 `.md`。
|
||||
- [x] `tree.folder.create` 或等价命令创建目录。
|
||||
- [x] 新建后进入 inline rename。
|
||||
- [x] 命名冲突走 incremental naming 或 preflight 提示。
|
||||
- [x] Convex 对 `New Folder` 按 capability 禁用,除非已有 folder resource。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 同一个新建入口可在 local 创建文件,在 Convex 创建 document。
|
||||
- Convex 不支持 folder 时不会显示假入口。
|
||||
- 新建结果通过 projection resync 出现。
|
||||
|
||||
### F2. Inline rename
|
||||
|
||||
- [x] 本地文件 / 目录 rename 写入文件系统。
|
||||
- [x] `.md` rename 保持 page id。
|
||||
- [x] Convex document rename 继续走 `tree.node.rename`。
|
||||
- [x] asset rename 收口到 `tree.resource.rename` 或等价正式命令。
|
||||
- [x] `Enter` 提交,`Esc` 取消。
|
||||
- [x] 文件型资源默认选中文件名 stem。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 不再使用 `prompt` 做重命名。
|
||||
- local 和 convex 的 rename UI 完全一致。
|
||||
- rename 失败不破坏 selection/focus。
|
||||
|
||||
### F3. Move / Copy / Cut / Paste
|
||||
|
||||
- [x] local move/copy 使用文件系统操作。
|
||||
- [x] Convex move/copy 使用 existing tree/resource command。
|
||||
- [x] cut 状态有 decoration。
|
||||
- [x] paste 成功后清空 cut。
|
||||
- [x] 多选时过滤父子重复选择。
|
||||
- [x] 禁止 move 到自身或后代。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- local/convex 都通过同一 clipboard payload。
|
||||
- cut/copy/paste 浏览器 smoke 覆盖两个 source。
|
||||
- projection resync 后 selection/focus 合理保留。
|
||||
|
||||
### F4. Delete / Restore / Purge
|
||||
|
||||
- [x] local delete 进入 `.mnote/trash` 或系统 trash,策略需冻结。
|
||||
- [x] local restore 从 trash 恢复。
|
||||
- [x] local purge 永久删除。
|
||||
- [x] Convex delete/restore/purge 继续走 `tree.node.*` / `tree.resource.*`。
|
||||
- [x] asset delete/restore 从 legacy `/api/media/batch` 收口。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 删除不是直接 `rm` 静默执行。
|
||||
- restore 在 local 和 Convex 都有统一菜单语义。
|
||||
- purge 有更强确认。
|
||||
|
||||
### F5. Drop 外部文件
|
||||
|
||||
- [x] local drop 文件复制进目标目录。
|
||||
- [x] Convex drop 文件上传对象存储。
|
||||
- [x] drop 到 readonly 目标被拒绝。
|
||||
- [x] drop 到 md 文件时目标归属规则明确。
|
||||
- [x] before/inside/after indicator 覆盖本地目录和 Convex document/asset-folder。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 外部拖入在 local 和 convex 使用同一 drop preflight UI。
|
||||
- 非法目标不执行写入。
|
||||
- 文件写入后 projection 自动刷新。
|
||||
- 外部文件 drop、内部拖拽、before/inside/after indicator、hover auto expand 都必须在真实浏览器中验收,不能只靠 contract test 或 jsdom。
|
||||
- 至少覆盖自拖自身、拖到后代、跨 workspace、只读目标四类非法拖拽场景。
|
||||
|
||||
**验收记录:**
|
||||
|
||||
- local 外部 drop:`scripts/task163-local-folder-unified-tree-browser-smoke.js` 使用真实 Chromium `DataTransfer/File`,覆盖取消 preflight 不写入、确认后复制进目录、drop 到 `.md` 时写入父目录、readonly 目标拒绝且不发 execute。
|
||||
- Convex 外部 drop:同一 smoke 通过 iframe 宿主监听 `tree.filetree.external-drop`,覆盖 Convex document 与 asset-folder 两类目标;宿主侧继续由 `tree-shell-iframe-host.tsx -> Sidebar.handleResourcePaneDropFiles -> uploadFileTreeResourceAsset -> /api/media/upload` 上传对象存储。
|
||||
- 对象存储链路:`resource-command-client.test.ts` 覆盖 `uploadFileTreeResourceAsset`,`app/api/media/upload/route.test.ts` 覆盖 `tree.resource.upload` plan、Convex upload URL、`mediaAssets:createWithStorage` 与资源 delta artifact。
|
||||
- indicator:同一 smoke 覆盖本地目录 before/inside/after、hover auto expand,以及 Convex document before/inside/after 与 asset-folder inside。
|
||||
|
||||
## 7. Phase G:Watcher / Realtime / Refresh
|
||||
|
||||
### G1. Local file watcher
|
||||
|
||||
- [x] 监听 root 下文件新增、删除、重命名、修改。
|
||||
- [x] 忽略 `.git/node_modules/.mnote/trash` 等目录。
|
||||
- [x] 防抖合并事件。
|
||||
- [x] source 输出 tree delta 或触发 snapshot refresh。
|
||||
- [x] 写操作自身产生的 watcher 事件不造成重复刷新风暴。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 外部编辑器新增 `.md` 后 page_tree 自动更新。
|
||||
- 外部删除文件后 Explorer 自动更新。
|
||||
- watcher 不越过 root。
|
||||
|
||||
**验收记录:**
|
||||
|
||||
- Rust 侧新增 `/api/tree/local-folder-watch`,由 `LocalFolderSource` 计算 root revision,只在可见文件/目录变化时变更 revision,并复用 root escape 与 ignore 规则。
|
||||
- 浏览器端 local source 不再固定 2.5 秒 reload,而是轮询 revision,变化后防抖触发 snapshot refresh。
|
||||
- `cargo test -p mnote-web local_ -- --nocapture` 覆盖 `.mnote` 噪音不改变 revision、可见文件内容变更会改变 revision、root escape 防护。
|
||||
- `scripts/task163-local-folder-unified-tree-browser-smoke.js` 覆盖外部新增 `.md` 后 page_tree 自动更新,以及外部新增/删除普通文件后 Explorer 自动更新。
|
||||
|
||||
### G2. Convex realtime parity
|
||||
|
||||
- [x] Convex 继续使用现有 tree stream。
|
||||
- [x] preferred snapshot 选择不因 local source 破坏。
|
||||
- [x] local source 和 Convex source 都能进入统一 tree event consumer。
|
||||
- [x] source 切换时清理上一 source 的 watcher / stream。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- local 和 convex 的实时刷新状态都可观测。
|
||||
- 切换 workspace 不出现旧 source 事件污染新 workspace。
|
||||
- 当前 Convex smoke 不回退。
|
||||
|
||||
**验收记录:**
|
||||
|
||||
- Convex 侧继续由 `useSidebarTreeStream` / tree stream server 处理 realtime,`pnpm vitest run src/lib/tree-stream/use-sidebar-tree-stream.test.tsx src/components/sidebar/use-preferred-sidebar-snapshot.test.tsx src/lib/tree-stream/server.test.ts` 通过。
|
||||
- local 侧通过同一 tree shell source metadata 和 revision watcher 触发 snapshot refresh,不引入第二套 UI。
|
||||
- 真实浏览器 smoke 覆盖 local -> Convex source 切换,断言旧 local watcher 内容和 selection 不污染 Convex file tree。
|
||||
|
||||
## 8. Phase H:Markdown 编辑与写回 P2
|
||||
|
||||
### H1. Markdown AST 到编辑器文档
|
||||
|
||||
- [x] Markdown parse 成 editor document。
|
||||
- [x] frontmatter 保留。
|
||||
- [x] 基础块覆盖 paragraph / heading / list / quote / code / divider。
|
||||
- [x] 附件链接解析为 resource reference。
|
||||
- [x] 不支持块有降级显示。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 本地 `.md` 可进入可编辑态。
|
||||
- 基础 Markdown 往返不丢主体内容。
|
||||
- 不支持格式不会导致保存破坏原文件。
|
||||
|
||||
**验收记录:**
|
||||
|
||||
- `LocalFolderSource` 的 Markdown 读取侧已覆盖 heading 1-6、paragraph、bullet / numbered list、quote、fenced code、divider、相对附件链接 `media` reference;未知/不支持语法降级为 paragraph。
|
||||
- `cargo test -p mnote-web local_markdown -- --nocapture` 覆盖基础块解析、附件链接解析、unsupported table 降级显示。
|
||||
- 真实浏览器 smoke 打开 `docs/blocks.md`,断言 frontmatter title、列表、quote、code 与附件链接文本均可在文档页读出。
|
||||
|
||||
### H2. 编辑器文档写回 Markdown
|
||||
|
||||
- [x] 保存正文写回 `.md`。
|
||||
- [x] 标题编辑规则冻结:文件名 / H1 / frontmatter 如何同步。
|
||||
- [x] 页面设置写入 frontmatter 或 `.mnote/page-options.json`。
|
||||
- [x] 保存失败保留编辑器状态并显示错误。
|
||||
- [x] watcher 与保存事件去重。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 修改本地 `.md` 后刷新仍能看到新内容。
|
||||
- 文件名、H1、frontmatter title 不互相打架。
|
||||
- Convex 编辑链路不受本地写回影响。
|
||||
|
||||
**验收记录:**
|
||||
|
||||
- 正文保存通过 `/api/documents/save` 写回 `.md`,标题编辑通过 `/api/documents/title` 只写 frontmatter `title`,文件名变更仍由 Explorer rename 负责;三者职责分离。
|
||||
- 页面设置写入 `.mnote/page-options.json`,不混入正文。
|
||||
- `cargo test -p mnote-web local_ -- --nocapture` 覆盖本地正文、标题和页面设置写回;`local_markdown_save_preserves_frontmatter_and_writes_basic_blocks` 覆盖 frontmatter 保留与基础块 Markdown 输出。
|
||||
- 真实浏览器 smoke 通过 document APIs 保存本地 Markdown,刷新后仍能看到新标题和正文;revision watcher 只在本地 root revision 变化后防抖刷新,避免固定刷新风暴。
|
||||
|
||||
## 9. Phase I:Local / Cloud 显式桥接 P3
|
||||
|
||||
### I1. Import / Export
|
||||
|
||||
- [x] local folder import 到 cloud workspace。
|
||||
- [x] cloud workspace export 到 local folder。
|
||||
- [x] 冲突策略明确。
|
||||
- [x] page id 映射保留。
|
||||
- [x] 附件路径和对象存储映射保留。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- import/export 是显式动作,不是 local 打开时自动发生。
|
||||
- 用户可预览影响集合。
|
||||
- 失败可重试或回滚。
|
||||
|
||||
**验收记录:**
|
||||
|
||||
- P3 候选合同已冻结在 `4-23-local-cloud-explicit-bridge-p3-candidate-v1.md`。
|
||||
- import/export 均被定义为 `preview -> execute` 显式动作,不能在打开 local folder、保存 local `.md`、watcher 或普通树操作中隐式发生。
|
||||
- 冲突策略固定为 preview 输出冲突集合,execute 默认 incremental naming,不静默覆盖。
|
||||
- page id 映射分别通过 `mnote_id` / `.mnote/page-ids.json`、`cloud_document_id`、`.mnote/cloud-export-map.json` 保留。
|
||||
- 附件路径和对象存储 id 必须进入 export/import map;execute 失败必须返回 retry cursor 和 rollback plan。
|
||||
|
||||
### I2. Publish / Sync 候选
|
||||
|
||||
- [x] local page publish 到 Convex public page。
|
||||
- [x] 手动 sync 候选设计。
|
||||
- [x] 不在 P0/P1 引入隐式双向同步。
|
||||
- [x] 权限模型不混淆:local OS 权限不同于 Convex ACL。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 本地 workspace 不依赖 Convex 才能工作。
|
||||
- Cloud 功能通过显式桥接启用。
|
||||
- 没有自动双真相。
|
||||
|
||||
**验收记录:**
|
||||
|
||||
- `4-23` 固定 publish 为 `tree.bridge.publish.preview/execute`,只发布显式选择的单页或子树。
|
||||
- sync 只进入 `tree.bridge.sync.preview` 候选,不提供默认 `sync.execute`,避免形成自动双真相。
|
||||
- local OS 权限与 Convex ACL 在 preview 中必须单独展示,不互相推导。
|
||||
- 当前实现中的 local folder 读写、watcher、Markdown 保存均不依赖 Convex;Convex 能力只通过显式 bridge 候选启用。
|
||||
|
||||
## 10. 全局验收矩阵
|
||||
|
||||
### 10.1 Contract tests
|
||||
|
||||
- [x] `WorkspaceSource` mock local / mock convex。
|
||||
- [x] command envelope source routing。
|
||||
- [x] projection source metadata。
|
||||
- [x] capability-driven menu profile。
|
||||
- [x] preflight result schema。
|
||||
- [x] active / selection / focus reducer。
|
||||
- [x] cut / copy / paste reducer。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- 不启动浏览器也能验证核心协议。
|
||||
- local/convex 至少各有一套同命令不同 executor 的测试。
|
||||
- Contract tests 只负责协议、schema、reducer、payload 与 executor 分发,不替代拖拽、右键、多选、外部 drop、source 切换的真实浏览器验收。
|
||||
|
||||
### 10.2 Rust tests
|
||||
|
||||
- [x] local folder scan。
|
||||
- [x] md-only page_tree。
|
||||
- [x] file_tree folder first sorting。
|
||||
- [x] page id persistence。
|
||||
- [x] root escape 防护。
|
||||
- [x] local write executor。
|
||||
- [x] Convex executor regression。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- Rust 测试覆盖 source adapter 主路径。
|
||||
- 安全边界有失败用例。
|
||||
- Convex 旧路径没有被本地 source 破坏。
|
||||
|
||||
### 10.3 Browser smoke
|
||||
|
||||
- [x] 打开 local folder。
|
||||
- [x] 3000 `desktop:hot` 主入口覆盖打开本地文件夹:不触发原生 prompt,不跳 `/tree`,不落入 `legacy_next_compat_disabled`。
|
||||
- [x] Explorer 显示目录、md、普通文件。
|
||||
- [x] 页面树只显示 md。
|
||||
- [x] 打开 md 到文档页。
|
||||
- [x] 同一套右键菜单在 local / convex 按 capability 裁剪。
|
||||
- [x] 已选区内右键保留多选,未选项右键切换 selection 后开菜单,空白区点击清空 selection / focus。
|
||||
- [x] `Ctrl/Cmd+Click`、`Shift+Click`、`Ctrl/Cmd+A` 多选链路。
|
||||
- [x] 3000 主 Sidebar file tree 的 `Shift+Click` 范围多选由 `scripts/task164-desktop-hot-local-folder-main-entry-smoke.js` 验收。
|
||||
- [x] inline rename。
|
||||
- [x] `Ctrl/Cmd+C/X/V` 与菜单 Copy / Cut / Paste 都覆盖,cut decoration 出现,paste 成功后消失。
|
||||
- [x] 内部拖拽覆盖单选、多选、move / copy 修饰键切换。
|
||||
- [x] 内部拖拽覆盖 before / inside / after 三态 indicator。
|
||||
- [x] 内部拖拽覆盖 hover auto expand。
|
||||
- [x] 内部拖拽覆盖自拖自身、拖到后代、跨 workspace、只读目标四类非法场景,并断言不会发 execute。
|
||||
- [x] 外部文件 drop 使用真实 `DataTransfer/File` 或等价 Playwright 文件拖入,覆盖 local folder、Convex document/asset-folder、readonly 目标、md 文件目标。
|
||||
- [x] 外部文件 drop 覆盖 preflight UI、写入/上传结果、projection refresh。
|
||||
- [x] watcher 外部修改刷新。
|
||||
- [x] 切回 Convex workspace 后原有页面树正常。
|
||||
- [x] source 切换后旧 watcher / stream 已清理,selection / focus / active 不被旧 source 污染。
|
||||
- [x] local 与 Convex 上同一动作保持同一 UI 和同一命令名,但 executor 行为可不同。
|
||||
- [x] 组合场景覆盖多同名 `新页面`、每页下 `index.md`、多选、右键、`F2`、`Ctrl/Cmd+X/V`、外部文件拖入、切回 Convex workspace。
|
||||
|
||||
**Done 条件:**
|
||||
|
||||
- smoke 覆盖 source 切换。
|
||||
- smoke 覆盖 3000 主入口本地文件夹链路;不能只用 `/tree` debug shell 作为完成证据。
|
||||
- smoke 覆盖至少一个同 UI、同命令、不同 executor 的动作。
|
||||
- 截图中 Explorer root / active / selection / focus 可解释。
|
||||
- 浏览器 smoke 必须是真实浏览器自动化,不接受纯 jsdom、DOM 单测或只跑 reducer 的结果。
|
||||
- 拖拽、外部 drop、上下文菜单、hover 和键盘导航必须在浏览器里触发真实事件链,不接受手工只看截图的验收。
|
||||
|
||||
## 11. 最终 done 条件
|
||||
|
||||
只有同时满足以下条件,`4-20/4-21/4-22` 才能整体移动到 `done/`:
|
||||
|
||||
- [x] `WorkspaceSource` 抽象落地。
|
||||
- [x] `LocalFolderSource` 支持本地只读 `file_tree/page_tree/page_aggregate`。
|
||||
- [x] `ConvexWorkspaceSource` 继续通过同一 projection/command 消费路径工作。
|
||||
- [x] Explorer P0 交互完成:active/selection/focus、keyboard、context menu、inline rename、cut/copy/paste、drag/drop。
|
||||
- [x] 至少一组写操作在 local 和 convex 上共用 UI 和 command envelope。
|
||||
- [x] 本地文件夹不经过 Convex 中介。
|
||||
- [x] page id 不因本地 rename/move 断裂。
|
||||
- [x] UI 没有 source-specific tree fork。
|
||||
- [x] 所有危险写操作都有 preflight。
|
||||
- [x] 浏览器 smoke 和 Rust/TS contract tests 通过。
|
||||
- [x] 3000 `desktop:hot` 主入口 smoke 通过,覆盖本地文件夹入口、legacy fallback 拒绝和主 Sidebar `Shift+Click` 多选。
|
||||
- [x] 拖拽、外部 drop、右键、多选、键盘、source 切换的关键路径在真实浏览器中逐项验收通过;这些 hard gate 未通过时,即使 contract/Rust 测试通过也不得移动到 `done/`。
|
||||
|
||||
## 12. 明确非目标
|
||||
|
||||
- [x] 不在 P0 实现本地和云端自动同步。
|
||||
- [x] 不在 P0 承诺 Markdown 与编辑器完全无损互转。
|
||||
- [x] 不用本地 per-user 文件夹替代 Convex 协作权限。
|
||||
- [x] 不把 VSCode 源码作为直接复制对象;只参考 Explorer 行为模型。
|
||||
- [x] 不把 Convex 虚拟空间强行伪装成真实磁盘目录。
|
||||
- [x] 不把 Local Folder 模式做成必须登录才能使用。
|
||||
Reference in New Issue
Block a user