Files
mnote/design/10-review/done/07-vscode-explorer-filetree-trash-gap-review.md
T
lix-2026 384da4e44c feat(tree): checkpoint resource lifecycle work
提交当前顶层 mnote Git 工作区,范围集中在 04-tree-domain 的 resource/trash/filetree 生命周期、mnote-web resource_trash 路由、Convex/Next 兼容接口、sidebar/file-tree 客户端适配、smoke 脚本与对应设计/bug 记录。

不包含被 ignore 的 design/05-editor-mainline/reference-code/leptos-tiptap 嵌套仓库改动。新增 smoke 的测试密码改为运行时读取 MNOTE_E2E_PASSWORD,避免提交明文 credential assignment。
2026-05-16 07:38:45 +08:00

610 lines
77 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 07 VSCode Explorer 文件树与垃圾箱闭环增量审查
> 更新时间:2026-05-15
> 状态:done
> 范围:`04-tree-domain` 中 File Tree / Page Tree / Resource Tree 与垃圾箱、删除、恢复、永久删除、VSCode Explorer 对标相关缺口。
> 结论:本审查 checklist 已闭合。当前可表述为文件树多选、删除进入垃圾箱、资源生命周期、恢复位置、双浏览器 no-refresh、本地 folder watcher 与本地非 md 资源 trash baseline 已完成最低可用闭环;但 VSCode Explorer 对标仍应按“最低可用完成 / parity backlog”分层,不应把禁用态或后续增强项描述为完整 parity。
## 1. 本轮判断
- `design/04-tree-domain/done/4-12-*``4-14-*` 的 done 口径是 Page Tree / File Tree initial DOM、hydration、row contract、selection state family 与能力保留完成,不等于 final DOM shell、VSCode Explorer 体验或垃圾箱产品闭环完成。
- `design/04-tree-domain/done/4-20-vscode-explorer-file-tree-alignment-v1.md` 已经把 VSCode Explorer 对标列为 done,但文档自身仍明确写着资源 `rename/delete/restore` 不能长期停留在 legacy `/api/media/batch`,需要 `tree.resource.rename/archive/restore` 或等价正式命令。
- 当前 `wolai-frontend/src/components/sidebar/sidebar.tsx` 已经存在文件树多选删除和垃圾桶 Drawer,但仍以页面、附件、mindmap、table 的分散 route 组合闭环;这更像“可用基础”,不是统一 tree-first command 产品面。
- 初始审查时 `rust/crates/mnote-web/src/ssr/pages/layout.rs` 中“删除到垃圾桶”上下文菜单实际 dispatch `action: 'purge'`,这是已在阶段 1 止血的最高优先级语义风险;后续仍需防回归。
## 2. 主要缺口
### F-01 P0Rust Web 主壳“删除到垃圾桶”实际触发永久删除
初始证据:
- `rust/crates/mnote-web/src/ssr/pages/layout.rs``delete-trash` 分支显示确认文案“删除到垃圾桶”,但 dispatch `action: 'purge'`
- `wolai-frontend/src/app/api/tree/commands/route.ts``purge` 对应 `tree.node.purge`,语义是永久删除。
当前状态:
- 阶段 1 已改为 dispatch `archive`,并由 `sidebar_tree_delete_to_trash_dispatches_archive_not_purge` 与真实浏览器 smoke 防回归。
影响:
- 用户以为页面进入垃圾桶,实际可能走永久删除。
- 这会破坏“默认 Delete = archive / trash,永久删除只在 Trash 工作台强确认”的 VSCode Explorer 基线。
应执行:
- `delete-trash` 必须改为 `archive` / `tree.node.archive`
- `purge` 只能从垃圾箱中的“彻底删除 / 清空垃圾箱”入口触发,并保留更强确认。
### F-02 P0:垃圾箱产品面存在,但 `/trash` 与 3000 主壳口径未闭合
证据:
- `wolai-frontend/src/components/sidebar/sidebar.tsx` 已有 `Drawer` 垃圾桶,支持页面 / 附件 tab、搜索、恢复、彻底删除、清空。
- `rust/crates/mnote-web/src/workspace_shell.rs` 暴露 footer entry`href: "/trash"`
- 当前只检索到 `/api/*/empty-trash`,未看到完整 `wolai-frontend/src/app/trash` 或 Rust `/trash` 工作台页面。
影响:
- Sidebar Drawer 可用不等于全局 Trash 工作台可用。
- workspace shell 的“垃圾箱”链接可能成为空入口或体验断点。
应执行:
- 明确产品口径:垃圾箱是 Sidebar Drawer、独立 `/trash` 页面,还是两者都有。
- 若保留 `/trash`,必须有真实路由、真实数据、恢复、彻底删除、清空和浏览器验收。
- 若只保留 Drawer,应移除或改写 `/trash` 链接,避免入口误导。
### F-03 P0:删除默认语义必须先进入垃圾箱,永久删除必须只从垃圾箱触发
证据:
- 页面删除在 `deleteDocumentCommand` 主链中已经走 `archive`Convex `documents.softDelete` 会写 `deleted_at`
- 但 Rust Web 主壳存在 `delete-trash -> purge` 反向危险。
- 附件、mindmap、table 删除链分别走 `/api/media/batch``/api/mindmap/:doc/:id``/api/tables/:id` 等分散 route。
影响:
- 同一个“删除”动作在不同入口、不同资源类型下语义不一致。
- 用户无法建立稳定心智:删除后是否能恢复,永久删除是否真的同步清理 Convex。
应执行:
- 统一命名:默认删除 = `archive` / `trash`;彻底删除 = `purge`
- 所有入口的确认文案、commandName、route、Convex mutation 必须同义。
- 删除后必须能在垃圾箱中看到对象,恢复后回到 projection,彻底删除后 Convex 查询不到对象。
### F-04 P1:文件树多选基础已存在,但页面树与批量命令闭环不足
证据:
- `wolai-frontend/src/lib/file-tree/selection.ts``tree_shell/filetree_selection.rs` 已支持普通点击、Ctrl/Cmd 多选、Shift 范围、多选 toggle、右键保留选区。
- `wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsx` 会把 Delete / Backspace 转给 `onFileTreeDeleteSelection`
- `wolai-frontend/src/components/sidebar/sidebar.tsx` 的批量页面删除是逐个 `deleteDocumentCommand`,不是一个原子批量 tree command。
- 页面树没有同等级多选模型,主要是 focus / active / 单节点 context menu。
影响:
- 用户指出的“多选删除”在文件树有基础,但没有形成 VSCode Explorer 式跨入口一致能力。
- 批量删除部分失败时会出现部分已删、部分未删,缺少统一 plan / rollback / result summary。
应执行:
- 文件树先补真实浏览器 smokeCtrl/Cmd 多选、Shift 范围、右键已选项保持 selection、Delete 批量删除。
- 批量删除应进入统一 preflight plan,至少对父子重复选择去重,并返回可解释的 partial failure。
- 页面树是否需要多选要明确产品口径;若 page tree 也承担 Explorer 行为,应补 selection state family。
### F-05 P1:资源级 `tree.resource.*` 未收口
证据:
- `4-20` 明确写到资源 `copy/move/upload` 已有 `tree.resource.copy/move/upload`,但 `rename/delete/restore` 仍经过 legacy `/api/media/batch`,需要 `tree.resource.rename/archive/restore` 或等价 bridge。
- 当前前端 `handleDeleteAssets` 对 mindmap、table、file asset 分别调用不同 route,并在删除后 `refreshTree()`
影响:
- File Tree / Resource Tree 仍有第二套资源命令真相。
- 后续 07-ai 若依赖资源树状态,容易再次绕过 tree-first command。
应执行:
- 定义并落地 `tree.resource.archive``tree.resource.restore``tree.resource.purge``tree.resource.rename`,或明确等价 bridge 命名。
- legacy `/api/media/batch` 只能作为兼容 alias,不应是主路径。
- command event 与 projection resync 必须覆盖资源恢复、永久删除、重命名。
### F-06 P1:垃圾箱清空需要跨 documents / media / mindmap / table 一致验收
证据:
- 当前存在 `/api/documents/empty-trash``/api/media/empty-trash``/api/mindmap-trash/empty``/api/tables/empty-trash`
- Sidebar 中“页面垃圾桶”和“附件垃圾桶”分别清空:页面只调 documents;附件 tab 并发清空 media、mindmap、table。
影响:
- 用户预期的“垃圾箱删除后 Convex 同步删除”必须跨对象类型验收,而不是只验收页面。
- 如果多个 API 中任一失败,可能出现计数、projection 与 Convex 数据不一致。
应执行:
- 清空垃圾箱需要明确是否是一键全量清空,还是按页面 / 附件 tab 分区清空。
- 验收必须覆盖 Convex `documents``media_assets``mindmaps``document_tables` 以及页面关联数据清理。
- 对 Storage / OCR / LightRAG / 引用 / 最近访问 / 收藏等关联清理给出确认边界。
### F-07 P1:恢复位置、排序与子树还原规则不足
证据:
- 子代理审查指出 Convex 页面 restore 会把根节点 `parent_id` 置为 `null`,子节点按同一 `deleted_at` 级联恢复;原父位置 / 排序不一定完整还原。
影响:
- “恢复”只能证明对象复活,不等于恢复到删除前位置。
- 对文件树体验来说,恢复后 reveal/select/focus 也属于闭环的一部分。
应执行:
- 删除前记录原 parent / order / source view / expanded path,恢复后尽量还原。
- 如果父节点已不存在,应有明确 fallback:恢复到根、恢复到最近有效父、或提示用户选择位置。
- 恢复后必须 reveal 并选中对象,避免用户以为恢复失败。
### F-08 P1VSCode Explorer 的 cut / inline rename / context menu 命令面未完全对齐
证据:
- 文件树键盘层会产生 `cutSelection` reducer 事件,但全局粘贴链主要读取 `action: "copy"` 的剪贴板写入,cut-move paste 未完整确认。
- F2 目前更像触发 `beginRenameFocused`,实际重命名仍大量依赖右键菜单 / prompt,不是 VSCode 式 inline rename。
- 右键菜单缺少或未统一:Cut、Paste Into、New File、New Folder、Refresh 单节点、Collapse All、Reveal、Copy Path / Relative Path 等 Explorer 常见项。
影响:
- 当前“对标 VSCode Explorer”容易被误读成完成全部 Explorer 产品能力。
- 07-ai 若把这些能力当成已有,会在自动操作、上下文菜单和 keyboard workflow 中遇到断点。
应执行:
- P1 先收口 Cut / Paste move、F2 inline rename、右键命令可用性。
- P2 再补 Reveal、Collapse All、Copy Path、Refresh 单节点等效率项。
- 不应照搬 VSCode 文件系统 provider 复杂度,但应照搬 command context 和 selection/focus 规则。
### F-09 P1:本地文件夹与 Convex 不是同一 executorwatch 范围也不一致
证据:
- Rust Web `tree.rs` 支持 `/api/tree/commands`,但 `local_folder` 分支走独立 `execute_local_tree_command`
- 本地 watcher 当前主要监听 markdown 路径事件,未覆盖非 md 资源文件的完整资源树变更。
- `4-22` 已把 Local Folder / Convex 统一 source 大范围标为完成,但 `4-23` import/export/publish/sync 候选合同仍在 `process/`
影响:
- “Local Folder 与 Convex 共用一套 projection / command / preflight / runtime / UI”仍需要拆成已完成和待核验两层。
- 普通附件、OnlyOffice、mindmap、本地资源文件的 Trash / restore / purge 不能默认等同于 markdown page trash。
应执行:
- 10-review 口径中明确:local markdown/page trash baseline 已有,完整 Resource Tree / File Tree Trash 待核验。
- 本地资源文件 watch、trash、restore、purge 需要单独验收。
### F-10 P2:实时刷新仍混合 refetch,缺少 no-refresh 级回归验收
证据:
- Sidebar 删除资产后仍显式 `refreshTree()`
- 当前同时存在 Convex live/query、tree SSE、手动 refetch、本地 watcher 多条刷新链。
影响:
- “刷新浏览器才能看到变化”类问题容易复发。
- 局部 optimistic update、server delta、manual refetch 之间没有统一验收矩阵。
应执行:
- 保留手动 refetch 作为兜底,但主验收必须是无浏览器刷新可见。
- 覆盖双浏览器:A 删除 / 恢复 / 清空,B 通过 Convex live 或 tree stream 自动更新。
### F-11 P1F2 inline rename 仍缺 VSCode 式行内校验体验
证据:
- VSCode Explorer rename 输入框会做空名、非法名、同级重名等 validation,并在输入框上下文展示错误。
- 当前 filetree 已有 F2 inline rename,但主要是 trim 后提交;失败路径仍可能落到 alert / 后端错误提示,不是行内 validation。
- 2026-05-15 二次修正后,`wolai-frontend` DOM host 已补空名、非法路径字符、同级重名行内阻断和普通附件扩展名保留;Rust SSR srcdoc 路径仍需后续 smoke 核验是否同等级。
影响:
- “能 F2 重命名”不等于 VSCode Explorer rename 体验闭环。
- 07-ai 或自动化操作若只看 input 存在,可能无法在非法名、重名、扩展名策略下获得稳定可解释反馈。
应执行:
- 页面、普通附件、本地 Markdown、本地非 md 资源统一行内校验:空名、非法字符、同级重名、扩展名保留 / 改写策略。
- Enter 不提交非法值;Escape 取消;blur 行为必须明确。
- 错误提示必须出现在 rename input 附近,不能只依赖 alert。
### F-12 P2:右键菜单仍是最低项可见,不是完整 VSCode Explorer command surface
证据:
- 当前已补 `New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal` 的最低可见或禁用态。
- `New Folder` 在当前页面树下仍是禁用态;`Paste Into` 已在主 Sidebar 节点右键菜单接入 action target,但空白区右键菜单与 Rust SSR srcdoc 路径仍需后续 smoke / 实现时继续沿同一 target 规则验收。
影响:
- 当前只能说“最低菜单项有口径”,不能说“VSCode Explorer 菜单面已完整对齐”。
- 右键 action target 与键盘 paste target 若不统一,批量选择和右键未选项切换目标会再次出现歧义。
应执行:
- 明确哪些菜单项是长期不做、哪些进入 backlog。
- 至少补 `Copy Relative Path` 的 workspace-relative / local-root-relative 语义和真实复制验收。
- `Paste Into` 应使用右键 action target,并与 Ctrl/Cmd+V 的 focused target 规则写清。
### F-13 P2`resync_required` 事件合同需要防回归
证据:
- `applyTreeStreamDelta``resync_required` 本身不直接替换 tree 数据,必须依赖后续 snapshot / resync envelope。
- 阶段 8 smoke 已证明本轮页面、附件、mindmap、table 链路能触发 no-refresh 更新,但这不能单独证明所有未来 `resync_required` 都会配套 resync。
影响:
- 如果后续 route 只写 `resync_required` delta、不写 snapshot / resyncB 浏览器会停留在旧数据。
- 这类问题表面上会再次表现为“必须刷新浏览器才能看到变化”。
应执行:
- 给 tree event stream 增加合同级回归:每个 `resync_required` 必须伴随后续 snapshot / resync,或客户端必须主动拉取新 snapshot。
- 验收断言包括 cursor 顺序、断线重连、重复 resync 去重。
- smoke 结果中继续记录实际事件来源,不把本地 `refreshTree()` 误标为实时同步。
## 3. 按顺序执行的 checklist
### 阶段 1P0 删除语义止血
- [x] 修正 `rust/crates/mnote-web/src/ssr/pages/layout.rs``delete-trash` 必须 dispatch `archive`,不能 dispatch `purge`
- [x] 补单元测试或字符串级回归:菜单文案“删除到垃圾桶”不得与 `tree.node.purge` 绑定。
- [x] 真实浏览器验证:Rust 3000 主壳右键删除页面后,页面进入垃圾桶,Convex `deleted_at` 非空,未被永久删除。
验收标准:
- Network / bridge log 中默认删除命令为 `tree.node.archive`
- `tree.node.purge` 只出现在垃圾箱“彻底删除 / 清空”路径。
执行记录:
- 2026-05-15`delete-trash` 已改为 `archive`;新增 `sidebar_tree_delete_to_trash_dispatches_archive_not_purge` 回归测试。
- 2026-05-15:浏览器 smoke 使用 `TEST-10REVIEW-07-P0-*` 新建页面后右键删除,捕获请求 `action: "archive"`、bridge command `tree.node.archive`、响应 `deleted_at` 非空、同页面 `purge` 请求数为 0。
### 阶段 2:统一垃圾箱入口与产品口径
- [x] 决定 `/trash` 是独立页面还是移除入口;如果保留,补真实 Trash 工作台。
- [x] Sidebar Drawer 与 `/trash` 使用同一 dataset`trashed_documents``trashed_media_assets``trashed_mindmap_assets``trashed_table_assets`
- [x] 页面、附件、mindmap、table 均支持恢复、彻底删除、清空。
验收标准:
- 不存在点开“垃圾箱”后空路由或 404。
- 垃圾箱计数与列表一致;恢复 / 彻底删除后计数和列表实时更新。
执行记录:
- 2026-05-15Rust 3000 主壳已挂载 `GET /trash`,复用 `sidebar.datasetList``trashed_*` 数据渲染垃圾箱工作台;页面条目提供恢复与彻底删除按钮。
- 2026-05-15:补 `trash_entry_renders_real_workspace_trash_workbench` 路由级回归;浏览器 smoke 访问 `/trash?workspaceId=<active>` 返回 200,出现 `mnote-trash-workbench`、页面区、资源区、恢复按钮和彻底删除按钮。
- 2026-05-15Rust 3000 主壳已挂载 `POST /api/documents/empty-trash`,补 `documents_empty_trash_route_executes_workspace_trash_purge` 回归;`/trash` 页面已出现“清空页面垃圾箱”按钮,并只读浏览器 smoke 验证按钮存在,未点击清空以避免删除既有垃圾箱数据。
- 2026-05-15Rust 3000 主壳已新增资源垃圾箱兼容入口:`POST /api/media/batch` restore、`POST /api/media/purge``POST /api/media/empty-trash``PATCH /api/mindmap/{docId}/{mindmapId}` restore / purge、`POST /api/mindmap-trash/empty``POST /api/tables/restore``POST /api/tables/purge``POST /api/tables/empty-trash`;补 `media_trash_routes_restore_purge_and_empty``mindmap_trash_routes_restore_purge_and_empty``table_trash_routes_restore_purge_and_empty` 回归。
- 2026-05-15`/trash` 资源区已从“待迁移”改为真实恢复 / 彻底删除按钮,并新增“清空资源垃圾箱”;路由级 fixture 覆盖附件、mindmap、table 三类资源按钮。
- 2026-05-15`cargo test -p mnote-web trash` 通过 7 项;Playwright 只读 smoke 访问 `http://localhost:3001/trash?workspaceId=ws_demo` 返回 200,确认工作台、页面区、资源区、“清空页面垃圾箱”和“清空资源垃圾箱”存在。真实数据当前没有垃圾箱资源行,未执行清空或彻底删除动作。
- 注意:本阶段完成的是 `/trash` 产品入口和 Rust 兼容 route 止血;附件 / mindmap / table 尚未正式收口为 `tree.resource.*` 命令,继续由阶段 5 跟踪。页面恢复 / 彻底删除 / 清空后的跨浏览器实时计数仍需在阶段 10 的 no-refresh 验收覆盖。
### 阶段 3:删除进入垃圾箱,清空后 Convex 同步永久删除
- [x] 创建测试页面、子页面、附件、mindmap、table,删除后均进入对应垃圾箱。
- [x] 清空页面垃圾箱后,确认 `documents.emptyTrashByWorkspace` 或等价 command 删除文档子树及关联数据。
- [x] 清空附件垃圾箱后,确认 `mediaAssets.emptyTrashByWorkspace``mindmaps.emptyTrashByWorkspace``tables.emptyTrashByWorkspace` 或统一 route 均执行成功。
- [x] 记录 Storage / OCR / LightRAG / page references / recent pages / favorites 的清理边界。
验收标准:
- Convex 查询不到已 purge 的 document / asset / mindmap / table。
- 清空失败时 UI 给出明确错误,不出现“UI 消失但 Convex 仍保留”的假成功。
安全前置:
- `emptyTrashByWorkspace` 是 workspace 级破坏性动作;写入型 smoke 必须使用本轮独立 workspace 或可证明只含本轮 `TEST-10REVIEW-07-P3-*` 数据的隔离环境。
- 禁止在默认测试账号的既有 workspace 上直接点击“清空页面垃圾箱 / 清空资源垃圾箱”,否则可能误删同一用户历史垃圾箱数据。
- 若当前没有安全 workspace seed 机制,先保留代码审查与 route 级 fixture 验收,补齐隔离 workspace 后再执行真实清空。
执行记录:
- 2026-05-15 代码审查:`documents.emptyTrashByWorkspace` 会删除当前用户在 workspace 内 `deleted_at != null` 的 documents,并调用 `purgeDocumentRelatedData` 清理关联 `mindmaps``media_assets``document_tables` / `document_table_rows``comment_messages` / `comment_threads``page_references``document_stars``user_recent_pages`;附件底层 Convex Files 只在同一 `storage_id` 没有其它未清理引用时删除。
- 2026-05-15 代码审查:`mediaAssets.emptyTrashByWorkspace` 会清理已过期 `deleted_at` 或历史 `purged_at``media_assets`,并按 `storage_id` 引用计数决定是否删除 Convex Files`mindmaps.emptyTrashByWorkspace` 删除当前用户已软删 mindmap`tables.emptyTrashByWorkspace` 删除已归档且到期的 `document_tables` 及其 `document_table_rows`
- 2026-05-15 边界记录:OCR 文本随 `media_assets` 行删除;LightRAG 已有 `ingest.rag_index_*` 任务入口,但当前 purge 路径未发现反向删除 LightRAG 索引的实现,应作为后续补充边界而非已完成项。
- 2026-05-15 代码缺口止血:Rust 3000 主壳已补资源软删除兼容入口,避免阶段 3 被“资源无法进入垃圾箱”阻塞:
- `POST /api/media/batch` 支持 `action: "delete"`,写入 `media_assets.deleted_at / deleted_by`
- `DELETE /api/mindmap/{docId}/{mindmapId}` 调用 `mindmaps.softDelete`
- `DELETE /api/tables/{tableId}` 调用 `tables.remove`
- `cargo test -p mnote-web trash` 覆盖 `media_trash_routes_delete_restore_purge_and_empty``mindmap_trash_routes_delete_restore_purge_and_empty``table_trash_routes_delete_restore_purge_and_empty`
- 2026-05-15 安全验证结论:未找到现成 smoke 能安全创建独立 workspace 并完成页面 + 附件 + mindmap + table 的真实清空验收;默认测试账号已有 workspace,不能直接对其执行 workspace 级 empty trash。
- 2026-05-15:已补详细执行设计 `design/04-tree-domain/done/4-25-trash-empty-isolated-workspace-smoke-v1.md`,阶段 3 写入型 smoke 必须按一次性用户 / 一次性 workspace 隔离方案执行。
- 2026-05-15:已固化并执行 `scripts/task427-trash-empty-isolated-workspace-smoke.js`,一次性用户 `mnote.trash.1778793827654@example.com` 创建隔离 workspace `ws_req_1778793827876_33`seed 页面 `tree_1778793827972_4`、子页面 `tree_1778793827997_5`、附件 `asset_trash_1778793827654`、mindmap `mind_trash_1778793827654`、table `mp5zx1wjqwssfr4g5jj`;删除后确认进入垃圾箱,随后清空页面与资源垃圾箱,并用 Convex query 确认 document / media_asset / mindmap / document_table / document_table_rows 均不可再查询。
- 2026-05-15:真实 smoke 输出:`documents.emptyTrashByWorkspace` 删除 2 个页面;`mediaAssets.emptyTrashByWorkspace` 删除 1 个附件;`mindmaps.emptyTrashByWorkspace` 删除 1 个 mindmap`tables.emptyTrashByWorkspace` 删除 1 个 table。
### 阶段 4:文件树多选与批量删除
- [x] 文件树创建 3 个页面、2 个附件、1 个 mindmap、1 个 table 测试数据。
- [x] 验证 Ctrl/Cmd 多选、Shift 范围、右键已选项保持 selection、右键未选项切换 action target 的 reducer / DOM host / local-folder 浏览器证据。
- [x] 验证 Convex filetree 下 Ctrl/Cmd、Shift、右键已选 / 未选 action target 的真实端到端行为。
- [x] 验证 Delete / Backspace 批量删除混合 doc + asset。
- [x] 验证父子页面同时选中时 preflight 只生成一次有效删除计划,不重复删除子树或页面下资产。
- [x] 验证部分失败时保留可理解的 result summary,并刷新到真实服务端状态。
验收标准:
- 删除前确认弹窗准确列出页面、附件、mindmap、table 数量。
- 删除后无需刷新浏览器即可从文件树消失,并在垃圾箱中出现。
执行记录:
- 2026-05-15 代码审查:`wolai-frontend/src/lib/file-tree/selection.ts``reduceFileTreeSelection` 已支持 Ctrl/Cmd toggle、Shift range、右键已选项保持 selection、右键未选项切单选;`selection.test.ts` 已覆盖这些 reducer 规则。
- 2026-05-15 代码审查:`wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsx` 已把真实点击、右键、Delete、Backspace 接到 filetree selection 与 `onFileTreeDeleteSelection``tree-shell-host.test.tsx` 已覆盖 Delete / Backspace 使用当前稳定多选 selection,避免旧 runtime selection 回写覆盖新 selection。
- 2026-05-15 代码审查:`handleDeleteResourceSelection` 已走 `preflightFileTreeDelete(buildFileTreeShellDeletePreflightPayload(...))`,确认文案会按页面、附件、思维导图、在线表格分别计数。
- 2026-05-15 代码审查:父子去重已有两层实现证据:TS legacy `computeFileTreeDeleteTargets` 会过滤被父页面覆盖的子页面 / 资产;Rust `build_filetree_delete_plan` 会过滤 top-level docs,并跳过被选中页面或其祖先页面覆盖的资产。`tree_filetree_delete_preflight_plans_doc_and_asset_targets` 已覆盖父页面 + 子 index + 父/子资产 + 另一页面资产同时选中时,最终只保留父页面与未被该子树覆盖的外部资产。
- 2026-05-15 验证:`cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime tree_filetree_delete_preflight_plans_doc_and_asset_targets` 通过,1 项通过、0 失败。
- 2026-05-15 浏览器证据边界:`scripts/task163-local-folder-unified-tree-browser-smoke.js` 已覆盖 local-folder 的 Ctrl/Cmd 多选、右键已选保持、Shift 范围、右键未选切换、Delete preflight 取消;`scripts/task179-tree-create-delete-no-reload-smoke.js` 只覆盖 Convex filetree 单页面 Delete 后无导航 / 无刷新;`scripts/task427-trash-empty-isolated-workspace-smoke.js` 覆盖隔离 workspace 的 API seed 与清空垃圾箱,不是 filetree UI 多选删除。
- 2026-05-15:已补阶段 4 执行设计并归档为 `design/04-tree-domain/done/4-26-filetree-bulk-delete-selection-smoke-v1.md`,明确一次性用户 / workspace seed 与 Convex filetree UI 多选删除验收矩阵。
- 2026-05-15:已新增 `scripts/task428-filetree-bulk-delete-selection-smoke.js`,真实浏览器使用一次性用户 `mnote.filetree.1778795393112@example.com` 和隔离 workspace `ws_req_1778795393339_3` 创建 3 页面、2 附件、1 mindmap、1 table;在 3001 主入口 filetree 验证 Ctrl/Cmd 多选、Shift 范围、右键已选保持、右键未选切换。
- 2026-05-15`task428` Delete 轮选择 root + child + file asset + mindmap + table,确认文案为 `1 个页面 + 1 个附件 + 1 个思维导图 + 1 个在线表格`,请求为 `tree.node.archive``/api/media/batch delete``DELETE /api/mindmap/{docId}/{mindmapId}``DELETE /api/tables/{tableId}`;父子页面去重后只发送 root archivechild 随子树删除。
- 2026-05-15`task428` Backspace 轮选择 sibling + 第二个 file asset,确认文案为 `1 个页面 + 1 个附件`,请求为 `tree.node.archive``/api/media/batch delete`;两轮均验证目标行无需刷新从 filetree 消失,并用 Convex query 确认对应 document / media / mindmap / table 进入垃圾箱。
- 2026-05-15:阶段 4 新增 Rust SSR 主壳键盘批量删除实现。原因是 `task428` 初次失败暴露 3001 主壳 filetree selection 存在,但 Delete / Backspace 未进入删除处理;已补 `deleteSelectedSidebarFileTreeRows`,支持混合页面、附件、mindmap、table 删除,并补 `sidebar_filetree_delete_keys_support_mixed_doc_and_asset_selection` 回归。
- 2026-05-15 验证:`MNOTE_UI_BASE_URL=http://127.0.0.1:3001 node scripts/task428-filetree-bulk-delete-selection-smoke.js` 通过,结果文件 `tmp/task428-filetree-bulk-delete-selection-smoke/result.json` 记录请求、确认文案与部分失败摘要。
- 2026-05-15`task428` 部分失败轮通过 route interception 强制 `/api/media/batch``asset_p4_failure_1778795530948` 返回 500;页面 `tree_1778795531341_8` 成功进入垃圾箱并从 filetree 消失,失败附件仍保留且 `deleted_at` 为空,UI 弹出 `部分对象删除失败:asset_p4_failure_1778795530948`
- 2026-05-15 阶段 4 结论:阶段 4 子项已完成;剩余更长期问题转入阶段 5-8,包括资源正式 `tree.resource.*` 命令、恢复位置、VSCode Explorer 完整体验与双浏览器 no-refresh。
### 阶段 5:资源级 tree command 收口
- [x] 新增或确认 `tree.resource.archive``tree.resource.restore``tree.resource.purge``tree.resource.rename` 正式命令。
- [x] 将普通附件 `/api/media/batch` / `/api/media/purge` 收束为正式 `tree.resource.*` 兼容 alias。
- [x] 将 mindmap / table delete / restore / purge 收束为正式 `tree.resource.archive / restore / purge` 兼容 alias。
- [x] command event、projection resync、sidebar dataset 均能表达普通附件资源恢复和永久删除。
验收标准:
- 文件树资源删除 / 恢复 / 永久删除不再只依赖 legacy `/api/media/batch``/api/mindmap/*``/api/tables/*` 主路径;旧 URL 只能作为兼容 alias。
- 资源 commandName、确认文案、UI 状态一致。
执行记录:
- 2026-05-15 只读核对:阶段 5 未完成。现有正式资源命令只覆盖 `tree.resource.copy``tree.resource.move``tree.resource.upload``tree.resource.rename``tree.resource.archive``tree.resource.restore``tree.resource.purge` 尚未在 Rust runtime / storage-convex mapping 中形成正式命令面。
- 2026-05-15 代码证据:`wolai-frontend/src/lib/file-tree/resource-command-client.ts` 的 rename / delete / restore 默认仍发送 `/api/media/batch``wolai-frontend/src/app/api/media/batch/route.ts` 中 copy / move 会构造 `tree.resource.copy/move`,但 delete / restore / rename 仍直连 `mediaAssets.patchById`
- 2026-05-15 代码证据:`rust/crates/bridge-runtime/src/lib.rs` 只处理 `tree.resource.copy | tree.resource.move``tree.resource.upload``rust/crates/storage-convex-bridge/src/mapping.rs` 也只映射这三类命令。
- 2026-05-15 代码证据:`rust/crates/mnote-web/src/routes/resource_trash.rs``wolai-frontend/src/components/sidebar/sidebar.tsx` 当前把 file asset、mindmap、table 按类型分流到 `/api/media/batch``/api/mindmap/*``/api/tables/*` 兼容入口;`rust/crates/mnote-web/src/routes/gateway.rs` 已在 `/trash` 页面明示“正式 tree.resource.* 命令仍在后续阶段收口”。
- 2026-05-15 测试边界:已有 `media_trash_routes_delete_restore_purge_and_empty``mindmap_trash_routes_delete_restore_purge_and_empty``table_trash_routes_delete_restore_purge_and_empty` 覆盖 Rust 兼容 route;这不能替代 `tree.resource.archive/restore/purge/rename` command 级回归。
- 2026-05-15:已补阶段 5 执行设计 `design/04-tree-domain/process/4-27-resource-lifecycle-command-cutover-v1.md`,明确本轮先完成普通附件 file assetmindmap / table 暂保持兼容 alias,不冒充正式 cutover 完成。
- 2026-05-15`bridge-runtime` 已新增 `tree.resource.archive/restore/purge/rename` 生命周期 command plan,普通附件分别映射到 `mediaAssets:patchById` / `mediaAssets:purgeById`,并携带 `domainEventPlan``streamDeltaHint``storage-convex-bridge` 已补默认命令映射。
- 2026-05-15`wolai-frontend``/api/media/batch` 已将 `delete/restore/rename` 从直连 `mediaAssets.patchById` 改为 `tree.resource.archive/restore/rename` 兼容 alias`/api/media/purge` 已从直连 `mediaAssets.purgeById` 改为 `tree.resource.purge` 兼容 alias,并记录 Rust bridge artifact。
- 2026-05-15Rust 3000 `/api/media/batch``/api/media/purge` 已改为通过 runtime command 执行普通附件 archive / restore / rename / purge,旧 URL 仅保留为兼容入口;`tree_shell` command event schema 已补资源生命周期事件。
- 2026-05-15 验证:`cargo test -p bridge-runtime tree_resource_lifecycle_plans_cover_file_asset_commands``cargo test -p storage-convex-bridge tree_resource_lifecycle_commands_have_convex_mapping``cargo test -p mnote-web trash``cargo test -p mnote-web runtime_command_event_schema_covers_tree_and_resource_command_channels` 均通过。
- 2026-05-15 验证:`pnpm test src/app/api/media/purge/route.test.ts src/app/api/media/batch/route.test.ts src/lib/documents/rust-runtime.test.ts` 通过,26 项通过。
- 2026-05-15 剩余:mindmap / table 的 DELETE / restore / purge 仍是兼容 route 直连对应 Convex mutation;本阶段只完成普通附件 file asset 主链,不把 mindmap / table 误标为已完成正式 command cutover。
- 2026-05-15 二次审计补充:mindmap / table 双浏览器 no-refresh 已由阶段 8 验证,但它们仍不是正式 `tree.resource.*` lifecycle;后续应补 `tree.resource.mindmap.*` / `tree.resource.table.*` 或等价命名合同、command log、domain event 与 projection resync 回归。
- 2026-05-15 执行修正:先补 `mnote-web` 路由测试断言 mindmap / table DELETE、restore、purge 响应必须携带 `canonicalCommand=tree.resource.archive/restore/purge` 与对应 `resourceKind`,红灯确认旧 route 仍只返回 legacy mutation 结果。
- 2026-05-15 执行修正:Rust 3000 的 `/api/mindmap/{docId}/{mindmapId}` DELETE / PATCH restore / PATCH purge 已改为通过 `tree.resource.archive / restore / purge` runtime command 执行;`/api/tables/{tableId}` DELETE、`/api/tables/restore``/api/tables/purge` 已改为通过同一 runtime command 执行;旧 URL 保留为兼容 alias。
- 2026-05-15 执行修正:新增 `bridge-runtime` 合同测试 `tree_resource_lifecycle_plans_cover_mindmap_and_table_commands`,覆盖 mindmap -> `mindmaps:softDelete/restore/purge`table -> `tables:remove/restore/purge`,并断言 `resourceLifecyclePlan``domainEventHint` 与 stream delta hint。
- 2026-05-15 验证:`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime tree_resource_lifecycle_plans_cover_mindmap_and_table_commands -- --nocapture` 通过,1 项通过;`cargo test --manifest-path rust/Cargo.toml -p mnote-web trash_routes -- --nocapture --test-threads=1` 通过,3 项通过;`cargo test --manifest-path rust/Cargo.toml -p storage-convex-bridge tree_resource_lifecycle_commands_have_convex_mapping -- --nocapture` 通过,1 项通过。
- 2026-05-15 阶段 5 当前边界:`tree.resource.rename` 仍只覆盖普通附件 file assetmindmap rename 继续明确不支持,table rename 待结合 `/api/tables/{tableId}` PATCH 与表格标题语义单独收口。`empty-trash` 仍是 workspace 级批量合同,不伪装成逐资源 `tree.resource.purge`
### 阶段 6:恢复位置与 reveal/focus
- [x] 删除时记录恢复所需的 parent / orderview path / expanded path 由恢复后的父链展开补齐。
- [x] 恢复页面子树后验证原父节点、排序、展开路径和 active/focus 的代码路径与单测。
- [x] 父节点已永久删除时,提供明确 fallback。
验收标准:
- 恢复后对象回到可解释位置,并自动 reveal/select。
- 恢复结果不会悄悄落到根目录而无提示。
执行记录:
- 2026-05-15 代码审查:`wolai-frontend/convex/documents.ts``documents.restore` 会把根恢复节点 `parent_id` 置为 `null`,并只按同一 `deleted_at` 级联恢复子节点;当前没有恢复删除前 parent / sort_order / expanded path 的证据。
- 2026-05-15 对照证据:local-folder Markdown trash 已在 `rust/crates/mnote-web/src/routes/local_folder_source.rs` 记录 `original_relative_path`,恢复时可按原路径回放并处理重名冲突;Convex documents restore 没有同等级“删除前位置”记录。
- 2026-05-15 命令证据:`tree.node.restore` 命令链在 Next route 与 Rust route 中存在,`scripts/task163-local-folder-unified-tree-browser-smoke.js` 也覆盖了 Convex restore / purge 请求仍走 `/api/tree/commands`;但这只是请求级证据,不覆盖恢复位置。
- 2026-05-15 风险结论:这能保证“对象复活”,但不能保证恢复到删除前位置。若父节点仍存在,当前也可能恢复到根;若父节点已永久删除,缺少显式 fallback 选择与 UI 提示。
- 2026-05-15 UI 证据边界:Sidebar / tree shell 有 focusedId、selection、active row 的基础状态;Sidebar 垃圾箱恢复后主要 `refetch/refreshTree`,未找到 `router.push`、reveal、select、focus 恢复对象的端到端 smoke。Rust shell 虽有 `tree.filetree.reveal` postToHost 事件,但未找到 Sidebar / DOM host 的完整落地处理。
- 2026-05-15:已补阶段 6 执行设计 `design/04-tree-domain/process/4-28-trash-restore-location-reveal-focus-v1.md`
- 2026-05-15`wolai-frontend/convex/schema.ts` 为 documents 新增 `restore_parent_id / restore_sort_order``documents.softDelete` 删除子树时记录删除前 `parent_id / sort_order``documents.restore` 恢复时优先回放原父节点与排序,并返回 `restore_location.parent_id / sort_order / fallback_reason`
- 2026-05-15:恢复父节点仍存在或属于本次级联恢复子树时回到原父节点;原父节点已不存在或仍在垃圾箱时 fallback 到根目录,并清空恢复位置快照。
- 2026-05-15`restoreDocumentCommand` 已透传恢复位置;Sidebar 垃圾箱恢复后刷新数据、展开父链、滚动并 focus 恢复行;fallback 时提示“原父页面已不存在,已恢复到根目录”。
- 2026-05-15 验证:`pnpm test src/lib/documents/tree-command-client.test.ts src/lib/documents/restore-location.test.ts src/components/sidebar/tree-shell-host.test.tsx src/components/sidebar/sidebar-sync.test.ts` 通过,4 个测试文件、20 项测试通过。
- 2026-05-15 验证:`pnpm exec tsc --noEmit --pretty false 2>&1 | rg "convex/documents.ts\\((160|161|162|163|164|165|166|167|168|169|170|171|172|173|174|175)"` 无输出,阶段 6 修改附近无新增 TypeScript 报错;全量 `tsc` 仍受仓库既有错误影响,不作为本阶段完成条件。
- 2026-05-15 review 修正:发现 `/api/tree/commands restore` 先查 `documents.getMeta`,而 `getMeta` 默认过滤 `deleted_at != null`,会导致垃圾箱页面在进入 restore mutation 前 404;已改为 restore 入口使用 `includeDeleted: true`,并在 route 测试中断言该参数。
- 2026-05-15 review 修正:恢复到原排序位时可能与已有兄弟节点产生重复 `sort_order`;已新增 `buildDocumentRestoreOrderAssignments`,恢复时按目标父级重新编号兄弟节点,补“旧排序位被占用时 restored row 插入并后移兄弟节点”的单测。
- 2026-05-15 验证:`pnpm test src/lib/documents/tree-command-client.test.ts src/lib/documents/restore-location.test.ts src/components/sidebar/tree-shell-host.test.tsx src/components/sidebar/sidebar-sync.test.ts src/app/api/tree/commands/route.test.ts` 通过,5 个测试文件、32 项测试通过。
- 2026-05-15 验证:`pnpm exec tsc --noEmit --pretty false 2>&1 | rg "convex/documents.ts\\((16[5-9][0-9]|17[0-9][0-9]|18[0-4][0-9])"` 无输出,restore / softDelete 修改区间无新增 TypeScript 报错;全量 `tsc` 仍受既有无关错误影响。
- 2026-05-15 运行时刷新:执行 `npx convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping`,把本轮 Convex functions 部署到本地自托管运行时;首次 3000 smoke 在部署前复现旧 runtime 行为:恢复后 `parent_id` 仍为 `null`,部署后恢复位置生效。
- 2026-05-15 浏览器验证:新增并执行 `node scripts/task429-trash-restore-location-reveal-smoke.js` 通过;脚本使用一次性用户 / workspace,在 3000 主入口验证删除子页面后 `tree.node.restore` 返回原父页面与原排序位、兄弟 `sort_order` 后移、当前页面无需刷新重新显示恢复行;同时构造缺失父节点历史数据,验证 fallback 返回 `parent_missing_or_deleted` 并恢复到根目录。结果文件:`tmp/task429-trash-restore-location-reveal-smoke/result.json`
- 2026-05-15 剩余:React Sidebar Drawer 的“恢复后 DOM focus 与 fallback alert”仍缺少 3000 主壳浏览器证据;当前 3000 主壳已切 Rust 垃圾箱入口,React Drawer focus 由代码路径和单测覆盖,后续如恢复 React 入口或补专用 harness 再补截图证据。双浏览器 no-refresh 归入阶段 8。
### 阶段 7VSCode Explorer 体验补齐
- [x] Cut / Paste moveCtrl/Cmd+X 后 Paste 应移动,不是复制或 no-op。
- [x] F2 inline rename:文件树行内编辑,不再只靠 prompt。
- [x] 右键菜单补齐 New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal 的可用性或禁用态。
- [x] DnD 补齐父拖子禁止、拖到自身禁止、readonly 禁止、Alt/Ctrl copy modifier、冲突确认。
- [x] F2 inline rename validation parity:空名、非法名、同级重名、扩展名策略在行内提示并阻断提交。
- [x] 右键菜单增强项:Copy Relative Path。
- [x] 右键菜单增强项:Paste Into action target。
- [x] 右键菜单增强项:New Folder 产品口径明确为“等待 Resource Tree folder 合同,不在页面树里伪造 folder 真相”。
验收标准:
- keyboard 与 context menu 对同一 selection 得到同一 command target。
- 禁用态有可解释条件,不出现点击后无反馈。
执行记录:
- 2026-05-15 代码审查:`wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsx` 已在 filetree 键盘层识别 `F2`、Delete / Backspace、Ctrl/Cmd+C、Ctrl/Cmd+X、Ctrl/Cmd+V,并向 runtime 发 `beginRenameFocused``cutSelection``pasteIntoFocused` 等动作。
- 2026-05-15 浏览器证据边界:`scripts/task163-local-folder-unified-tree-browser-smoke.js` 已在 local-folder 模式覆盖 F2 inline rename、Ctrl/Cmd+X 后 cut decoration、Paste move preflight、确认后移动 Markdown、Alt 内部拖拽复制、外部 File drop、readonly drop 预检失败。
- 2026-05-15 Convex 证据边界:`scripts/task163-local-folder-unified-tree-browser-smoke.js` 也覆盖了 Convex context menu 基础项、copy / cut paste 命令请求、Delete 请求和 DnD indicator;但未覆盖 Convex F2 inline rename、真实 internal drop 执行、父拖子 / 拖自身 / readonly / 冲突确认端到端。
- 2026-05-15 初始缺口结论:上述强证据主要来自 local-folderConvex filetree 下 Cut / Paste move、F2 inline rename、右键 New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Relative Path / Reveal、DnD 禁止父拖子 / 自拖 / readonly / copy modifier 的完整端到端矩阵当时未找到。后续已由 `task430``task431` 补齐阶段 7 必要矩阵。
- 2026-05-15 代码风险:页面树 fallback / iframe host 中仍可见 `window.prompt("重命名页面", ...)` 与 prompt 式重命名路径,不能把“F2 inline rename 已全入口完成”作为当前口径。
- 2026-05-15 代码风险:`wolai-frontend/src/lib/file-tree/clipboard.ts` 的 TS clipboard payload 仍只有 `copy` 类型,Next DOM host / TS 工具层与 Rust srcdoc 路径的 cut 能力不完全一致;Rust shell context menu 的 `Refresh` 当前仍可能走 `window.location.reload()`,不符合 no-refresh 目标。
- 2026-05-15:已补阶段 7 子设计 `design/04-tree-domain/done/4-29-vscode-explorer-cut-paste-move-v1.md`
- 2026-05-15`wolai-frontend` 主 Sidebar 剪贴板 action 已扩展为 `copy | cut``Ctrl/Cmd+X` 写入 cut payload`Ctrl/Cmd+V` 遇到 cut payload 时,Rust family renderer 复用 `tree.filetree.drop.preflight` move plan,页面走 `moveDocumentCommand`,普通附件走 `moveFileTreeResourceAssets`,成功后清空剪贴板。
- 2026-05-15 验证:`pnpm test src/lib/file-tree/clipboard.test.ts src/components/sidebar/sidebar-paste-preflight-source.test.ts` 通过,2 个测试文件、7 项测试通过。
- 2026-05-15 验证:`pnpm exec tsc --noEmit --pretty false 2>&1 | rg "(src/lib/file-tree/clipboard.ts|src/components/sidebar/tree-pane-bindings.ts|src/components/sidebar/sidebar.tsx)"` 无输出,本轮 cut/paste 修改文件无新增 TypeScript 报错;全量 `tsc` 仍受既有无关错误影响。
- 2026-05-15:已补阶段 7 子设计 `design/04-tree-domain/done/4-30-vscode-explorer-f2-inline-rename-v1.md`
- 2026-05-15`tree-shell-dom-host.tsx` filetree row 已新增行内 `tree-rename-input`F2 进入 inline renameEnter / blur 提交,Escape 取消。页面 / index row 提交 `tree.node.rename` 并通过 `onTreeMutation` 回写;asset row 走 `/api/media/batch rename` 并广播 `wolai:assets-changed`
- 2026-05-15 验证:`pnpm test src/components/sidebar/tree-shell-host.test.tsx` 通过,1 个测试文件、8 项测试通过;新增测试覆盖 F2 inline input 与 `tree.node.rename` 提交。
- 2026-05-15:已补阶段 7 子设计 `design/04-tree-domain/done/4-31-vscode-explorer-context-menu-minimum-v1.md`
- 2026-05-15:主 Sidebar 右键菜单已显式补齐 `New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal`。其中 `New File` 复用新建子页面,`Refresh``refreshTree()``Collapse All` 清空展开集合,`Copy Path` 复制页面标题路径,`Reveal` 展开父链并滚动;`New Folder``Paste Into` 以禁用态展示原因,避免伪造未完成的 folder / right-click paste target 合同。
- 2026-05-15 验证:`pnpm test src/components/sidebar/sidebar-context-menu-source.test.ts` 通过,1 个测试文件、1 项测试通过;覆盖菜单项存在、禁用态存在、未引入 `window.location.reload`
- 2026-05-15:已补阶段 7 子设计 `design/04-tree-domain/done/4-32-vscode-explorer-dnd-modifier-guard-v1.md`
- 2026-05-15filetree DnD copy modifier 已从仅 Alt 扩展为 Alt / Ctrl / Meta;同时覆盖 `wolai-frontend` DOM host 与 Rust SSR 主壳 `layout.rs`。父拖子 / 自拖继续由 Rust `tree.filetree.drop.preflight` 校验,不在 UI 层新增第二套祖先判断。
- 2026-05-15 验证:`pnpm test src/components/sidebar/sidebar-dnd-source.test.ts` 通过,1 个测试文件、1 项测试通过。
- 2026-05-15 3000 主入口修正:Rust SSR 主壳补齐文件树 F2 inline rename、Ctrl/Cmd+X/V cut-paste move、右键菜单最低项 `New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal``Refresh` 只发局部刷新事件,不使用 `window.location.reload()``New Folder``Paste Into` 保持禁用态并给出原因。
- 2026-05-15 3000 主入口修正:新增 `POST /api/tree/filetree/drop-preflight`,通过 Rust bridge `tree.filetree.drop.preflight` 返回真实 preflight plan;自拖自身与父拖子返回 400copy modifier plan 保留 `copy: true`
- 2026-05-15 资源 rename 修正:`tree.resource.rename` 的 bridge plan 补 `patch.file_name`Rust `/api/media/batch rename` 在保留 `tree.resource.rename` artifact command 的同时先执行兼容 `mediaAssets:patchById`,避免 3000 主入口附件 F2 后 Convex 文件名不更新。
- 2026-05-15 真实浏览器 smoke:新增并执行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task430-vscode-explorer-stage7-smoke.js` 通过。脚本使用一次性用户 / workspace,验证:
- 页面 F2 inline rename:请求 `/api/tree/commands action=rename`,Convex 标题更新,无需刷新可见。
- 附件 F2 inline rename:请求 `/api/media/batch action=rename`Convex `media_assets.file_name` 更新,无需刷新可见。
- Cut / Paste move`Ctrl/Cmd+X` 后目标页面 `Ctrl/Cmd+V` 触发 `action=move`,子页面 `parent_id` 变为目标父页面。
- 右键菜单最低项:`New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal` 可见,禁用项有 title 原因。
- DnD preflight:自拖自身与父拖子均被拒绝;copy modifier preflight 返回 `copy: true`
- 结果文件:`tmp/task430-vscode-explorer-stage7-smoke/result.json`
- 2026-05-15 阶段 7 中途剩余:`task430` 之后 readonly 与命名冲突确认仍未在主 Sidebar Convex filetree 构造真实 route 矩阵;当时 DnD 总项因 readonly / conflict 尚未勾选完成。后续已由 `task431` 补齐。
- 2026-05-15`bridge-runtime``tree.filetree.drop.preflight` payload / plan 已补 `sourceCapabilities``targetCapabilities``targetChildren``conflictPolicy``requiresConfirmation``conflicts`readonly target/source 会在 preflight 阶段返回 validation error,同名目标会返回 `requiresConfirmation=true`,普通 copy modifier 不被 readonly source move 规则误拒绝。
- 2026-05-15:主 Sidebar `buildFileTreeShellInternalDropPreflightPayload` 已把 row title、target children 与 capability payload 透传到 preflight`handleResourcePaneInternalDrop` 与 cut-paste move 路径在 `requiresConfirmation` 时调用确认,用户取消时不执行后续 move/copy。
- 2026-05-15 验证:`cargo test -p bridge-runtime tree_filetree_drop_preflight -- --nocapture` 通过 4 项;`pnpm test src/lib/file-tree/shell.test.ts src/lib/file-tree/resource-command-client.test.ts src/components/sidebar/sidebar-paste-preflight-source.test.ts src/components/sidebar/sidebar-dnd-source.test.ts` 通过 4 个测试文件、21 项。
- 2026-05-15 真实 route smoke:新增并执行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task431-vscode-explorer-dnd-readonly-conflict-smoke.js` 通过。脚本使用当前代码新启动的 3001 mnote-web 与 3000 auth cookie,验证 readonly target/status 400、readonly source/status 400、同名目标返回 `requiresConfirmation=true` 与 conflict 明细、copy modifier 保持 `copy=true` 且无误报。结果文件:`tmp/task431-vscode-explorer-dnd-readonly-conflict-smoke/result.json`
- 2026-05-15 注意:本次 `task431` 未直接复用常驻 3000 进程,因为 3000 当时仍运行旧 `mnote-web` 二进制,第一次对 3000 执行脚本复现旧 preflight 放行 readonly 的行为;新代码已在 3001 当前编译进程验证通过,3000 需要重启后才具备同一行为。
- 2026-05-15 二次审计补充:阶段 7 已完成最低可用体验,不等于完整 VSCode Explorer parity。后续仍需补 F2 行内 validation、`Copy Relative Path``Paste Into` 右键 action target、`New Folder` 长期产品口径;这些列为 P1/P2,不阻塞已完成的 cut / paste move、F2 基础行内重命名、菜单最低项与 DnD guard。
- 2026-05-15 执行修正:先补 `tree-shell-host.test.tsx` 红灯测试,断言 F2 行内重命名必须在输入框内阻断空名、非法路径字符、同级重名,且普通附件输入不带扩展名时保留原扩展名。
- 2026-05-15 执行修正:`tree-shell-dom-host.tsx` 已新增 F2 rename 行内错误状态、`aria-invalid`、错误文案、`CSS.escape` fallback、普通附件扩展名保留;非法输入不再发 rename 请求。
- 2026-05-15 验证:`cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/tree-shell-host.test.tsx -- --runInBand` 通过,1 个测试文件、10 项通过。
- 2026-05-15 执行修正:`ContextMenu` 新增 `Copy Relative Path`,复用页面标题路径计算但复制不带根斜杠的 workspace-relative 路径;`New Folder` 继续明确禁用,原因是当前页面树没有 folder 对象合同,不能临时伪造第二套 folder 真相。
- 2026-05-15 验证:`cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/sidebar-context-menu-source.test.ts -- --runInBand` 通过,1 个测试文件、1 项通过。
- 2026-05-15 执行修正:先补 `sidebar-paste-preflight-source.test.ts` 红灯测试,断言 `ContextMenuProps` 暴露 `onPasteInto`、右键 `Paste Into` 调用 `onPasteInto(node)`,并要求 paste helper 使用 `targetDocumentIdOverride` 进入 Rust paste / drop preflight。
- 2026-05-15 执行修正:主 Sidebar 已抽出 `executeFileTreePaste(targetDocumentIdOverride)``Ctrl/Cmd+V``null` 保留 focused target 规则,右键 `Paste Into` 传当前菜单节点 `node.id` 作为目标页面。空剪贴板时右键入口会提示“剪贴板没有可粘贴的文件树内容”,不再是点击无反馈。
- 2026-05-15 验证:`cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/sidebar-paste-preflight-source.test.ts -- --runInBand` 通过,1 个测试文件、3 项通过。
- 2026-05-15 验证:`cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/sidebar-context-menu-source.test.ts -- --runInBand` 通过,1 个测试文件、1 项通过。
- 2026-05-15 验证:`cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/tree-shell-host.test.tsx -- --runInBand` 通过,1 个测试文件、10 项通过。
- 2026-05-15 验证边界:`pnpm exec eslint src/components/sidebar/sidebar.tsx --quiet``pnpm exec eslint src/components/sidebar/sidebar-paste-preflight-source.test.ts src/components/sidebar/sidebar-context-menu-source.test.ts src/components/sidebar/tree-shell-host.test.tsx --quiet` 无 error;全量 `pnpm lint` 仍被仓库既有 lint 问题阻塞,本轮输出显示 `823 problems (2 errors, 821 warnings)`
### 阶段 8:实时刷新与双浏览器回归
- [x] A 浏览器新建、删除、恢复、彻底删除页面,B 浏览器无需刷新可见更新。
- [x] A 浏览器对普通附件 file asset 执行上传、删除、恢复、彻底删除、清空资源垃圾箱,B 浏览器无需刷新可见更新。
- [x] A 浏览器对 mindmap / table 执行新建、删除、恢复、彻底删除、清空资源垃圾箱,B 浏览器无需刷新可见更新。
- [x] 本地 folder 模式验证 markdown 新建 / 重命名 / 删除 watcher 不触发浏览器 reload;非 md 资源文件 watcher create / rename / delete 已覆盖,trash / restore / purge 单列待补。
- [x] `resync_required` 事件合同防回归:`resync_required` 本身只推进 cursor,不替换数据;后续 snapshot / resync 才替换数据,已补前端 hook 与 Rust stream 合同测试。
验收标准:
- 所有主链操作不依赖浏览器刷新。
- `refreshTree()` 可以作为兜底,但不作为唯一可见性来源。
执行记录:
- 2026-05-15 代码审查:Rust Web `/api/tree/events` 已有 snapshot / delta / resync 路由与单元测试;`scripts/task120-rust-web-tree-integration-smoke.js``scripts/task123-rust-web-tree-live-stream-consumer-smoke.js``scripts/task177-tree-move-archive-live-smoke.js``scripts/task179-tree-create-delete-no-reload-smoke.js` 覆盖了 tree stream、页面 rename / move / archive、单页面 create/delete no-refresh 等部分链路。
- 2026-05-15 代码审查:`wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts` 已接入 EventSource `/api/tree/events` 并将 delta 交给 `applyTreeStreamDelta``scripts/task426-mnote-web-main-no-reload-smoke.js` 覆盖了单浏览器新建页面、新建 mindmap asset、删除页面本地 apply。
- 2026-05-15 代码审查:资源删除与部分 sidebar 操作仍显式调用 `refreshTree()` 或依赖局部 state 移除;这可以作为兜底,但不能证明双浏览器实时同步闭环。
- 2026-05-15 浏览器证据边界:`scripts/task169-mindmap-realtime-smoke.js` 已使用双浏览器上下文覆盖 mindmap realtime 相关场景,但未覆盖文件树中页面、附件、mindmap、table 的删除 / 恢复 / 彻底删除 / 清空垃圾箱全矩阵。
- 2026-05-15 未闭合结论:阶段 8 仍需新增双浏览器 smoke,验证 A 浏览器对页面和三类资源执行 create / archive / restore / purge / empty-trash 后,B 浏览器无需刷新可见更新,并记录事件来源是 tree delta / resync 还是 Convex live query。
- 2026-05-15 补充审查结论:阶段 8 需要显式拆成对象类型 × 生命周期矩阵。页面、附件、mindmap、table 都必须分别覆盖 create / archive / restore / purge / empty-trash,且验收口径是 A 浏览器执行、B 浏览器不刷新即可在 File Tree 与 Trash 工作台看到变化。现有 `task177/task179/task426/task428/task429` 主要是单浏览器 no-refresh 或局部 live probe`task427` 是 API/Convex 级 purge 验证,`task169` 是 mindmap 内容实时稳定性;这些不能替代文件树/垃圾箱双浏览器生命周期矩阵。实现边界上,`useSidebarTreeStream` 可消费 snapshot/delta/resync`useConvexSidebarData` 依赖 Convex live,但 Trash Drawer 仍大量使用 `refreshTree()/refetch()` 作为本地补偿;阶段 8 应记录每个矩阵项实际由 tree delta、tree resync snapshot、Convex live,还是手动 refreshTree 兜底驱动,避免把本地刷新补偿误标为实时闭环。
- 2026-05-15 页面双浏览器 smoke 初次失败暴露根因:`documents.emptyTrashByWorkspace` 只执行 Convex mutation,没有写入 tree command/domain event,导致 B 浏览器 `/trash` 收不到清空事件,必须刷新后才消失。已给 `documents.emptyTrashByWorkspace` command plan 补 `tree.trash.documents.emptied` domain event 与 `resync_required` stream hint,并把 `/api/documents/empty-trash` 改为 artifact-aware 执行;同时补 Convex mutation validator 的 `streamDeltaHint / domainEventHint / domainEventPlan / domainEventPlans` 参数,避免运行时 502。
- 2026-05-15 验证:`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime documents_empty_trash_plan_emits_tree_resync_event -- --nocapture` 通过,断言 `documents.emptyTrashByWorkspace` command plan 携带 `tree.trash.documents.emptied``resync_required``cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_empty_trash_route_executes_workspace_trash_purge -- --nocapture` 通过,断言 empty-trash 返回 `documents.emptyTrashByWorkspace` command log、`tree.trash.documents.emptied` domain event 与 `resync_required` stream delta`pnpm test src/app/api/tree/commands/route.test.ts src/lib/documents/tree-command-client.test.ts` 通过,2 个测试文件、14 项通过。
- 2026-05-15 运行时刷新:执行 `npx convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping`,把本轮 Convex validator 修改部署到本地自托管运行时。
- 2026-05-15 真实双浏览器 smoke:新增并执行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js` 通过。脚本使用一次性用户 `mnote.stage8.page.1778804544630@example.com` 与隔离 workspace `tree_1778804545042_7`B1 打开 `/documents/tree_1778804545042_6?workspaceId=tree_1778804545042_7` 观察 File TreeB2 打开 `/trash?workspaceId=tree_1778804545042_7` 观察 Trash 工作台。A 侧依次执行页面 create / archive / restore / archive+purge / create+archive+empty-trashB 侧每一步均无需刷新可见更新,`navigationEvents` 始终为空。事件来源记录:create 使用 `tree:resync`archive 使用 `tree:delta remove_document`restore 使用 `tree:delta upsert_document`purge 使用 `tree:resync`empty-trash 使用 `tree:resync`;结果文件为 `tmp/task432-filetree-trash-page-dual-browser-no-refresh-smoke/result.json`
- 2026-05-15 普通附件双浏览器 smoke 初次失败暴露身份与事件两层根因:`tree.resource.archive/restore/purge` 计划中 route 已放入真实 Convex userId,但 `mnote-web` transport 在 legacy `mediaAssets:*` 参数转换时又用 HTTP context actor 覆盖,导致 `mediaAssets.patchById``assertWorkspaceMember` 处返回“无权访问该工作空间”;已修正为优先保留 `args_json.userId`。随后 `mediaAssets.emptyTrashByWorkspace` 只执行 Convex mutation、不写 tree domain event,导致 B 浏览器 `/trash` 不刷新看不到清空;已在 Rust `/api/media/empty-trash` 成功后记录 `tree.trash.media.emptied` domain event 与 `resync_required` stream delta。
- 2026-05-15 验证:`cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_resource_lifecycle_args_keep_effective_user_id -- --nocapture` 通过,断言资源 lifecycle 参数转换保留真实 Convex userId`cargo test --manifest-path rust/Cargo.toml -p mnote-web media_trash_routes_delete_restore_purge_and_empty -- --nocapture` 通过。
- 2026-05-15 真实双浏览器 smoke:新增并执行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke.js` 通过。脚本使用一次性用户 `mnote.stage8.file.1778806752763@example.com` 与隔离 workspace `tree_1778806753148_2`B1 打开 `/documents/tree_1778806753148_1?workspaceId=tree_1778806753148_2` 观察 File TreeB2 打开 `/trash?workspaceId=tree_1778806753148_2` 观察 Trash 工作台。A 侧依次执行普通附件 upload / archive / restore / archive+purge / upload+archive+empty-trashB 侧每一步均无需刷新可见更新,执行阶段 `navigationEvents` 为空。事件来源记录:upload 使用 `tree:delta upsert_assets`archive / restore / purge / empty-trash 使用 `tree:resync`;结果文件为 `tmp/task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke/result.json`
- 2026-05-15 mindmap/table 双浏览器 smoke 初次实现时发现 table create 只是测试前 seed,不能证明“新建 table 后 B 无刷新出现”;已补 Rust `/api/tables/create` 兼容入口,创建成功后记录 `tree.resource.table.created` domain event 与 `resync_required` stream delta。mindmap / table 的 delete / restore / purge / empty-trash compat route 也已补 tree resync artifacts,避免 B 浏览器只能依赖刷新。
- 2026-05-15 验证:`cargo test --manifest-path rust/Cargo.toml -p mnote-web trash_routes -- --nocapture --test-threads=1` 通过,覆盖 media / mindmap / table 资源垃圾箱 route`node --check scripts/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js` 通过。
- 2026-05-15 真实双浏览器 smoke:新增并执行 `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js` 通过。脚本使用一次性用户 `mnote.stage8.mindtable.1778808180081@example.com` 与隔离 workspace `tree_1778808180480_2`A 侧依次执行 mindmap create / archive / restore / archive+purge、table create / archive / restore / archive+purge、mindmap+table create / archive / empty-trashB 侧 File Tree 与 `/trash` 每一步均无需刷新可见更新,执行阶段 `navigationEvents` 为空。事件来源记录:mindmap create/delete/restore 主要为 `tree:delta resync_required`mindmap purge 为 `tree:resync`table create/delete/restore 主要为 `tree:delta resync_required`table purge / empty-trash 为 `tree:resync`。结果文件为 `tmp/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke/result.json`
- 2026-05-15 local folder 二次审计:`scripts/task163-local-folder-unified-tree-browser-smoke.js` 在 extended 模式覆盖本地 Markdown 新建、删除进入 `.mnote/trash`、restore、purge,也覆盖外部新增 `watcher-added.md` 后页面树可见;但当前本地 folder watcher 主壳在 revision 变化后调用 `window.location.reload()`,因此不能算 no-refresh watcher 闭环。
- 2026-05-15 local folder 二次审计:同一脚本也覆盖 `watcher-asset.txt` 外部新增 / 删除后 filetree 可见 / 消失;但 Rust `LocalFolderWatcherRegistry` 只广播 markdown 事件,当前 filetree 资产可见性主要依赖 `/api/tree/local-folder-watch` revision 轮询后重载,不是非 md 资源事件级 no-refresh。
- 2026-05-15 local folder 二次审计:`LocalFolderWatcherRegistry` 底层理论覆盖 Markdown create / remove / name / data / metadata 事件,但现有 smoke 只实测外部新增 Markdown;外部 Markdown 重命名、删除尚未形成浏览器验收。非 md 资源文件在 filetree 中能作为 `asset` 出现,inline rename 可写入文件系统,但 copy / cut / delete / download 当前仍是禁用态,本地 `delete / restore / purge` command 只覆盖 Markdown 页面。
- 2026-05-15 阶段 8 中途状态:页面 document、普通附件 file asset、mindmap、table 的 Convex 双浏览器生命周期已闭合;本地 folder markdown watcher 当时仍是 reload 型刷新,非 md 本地资源文件 watcher / trash / restore / purge 当时仍未闭合;`resync_required` 事件合同当时还需要防回归测试。后续已由阶段 8/9 继续执行记录闭合。
- 2026-05-15 继续执行审计:local folder no-refresh 的实际刷新来源在 Rust Web 内嵌 tree shell runtime`rust/crates/mnote-web/src/routes/tree.rs` 的 local folder revision polling 命中 `/api/tree/local-folder-watch` 后调用 `window.location.reload()`;同文件的 filetree 右键 `Refresh` 当前也直接 `window.location.reload()`。因此现有能力只能称为 revision polling + reload 型可见性,不是 no-refresh。
- 2026-05-15 继续执行审计:`/api/local-folder/events``LocalFolderWatcherRegistry` 提供 SSE,但 watcher 当前 `is_markdown_path` 过滤为 `.md/.markdown`,只对 Markdown 事件广播;非 md 资源文件变化不会走该 SSE,主要依赖 `/api/tree/local-folder-watch` 的全目录 revision hash。
- 2026-05-15 下一步最小实现口径:先不要扩散到本地非 md trash / restore / purge。优先把 local folder filetree/page tree 的 revision polling 从 `window.location.reload()` 改为“拉取最新 local folder snapshot 并替换当前内存行模型后 `renderTree()`”,并新增回归测试断言 local folder watch 分支不含 `window.location.reload()`,再补浏览器 smoke 记录 `navigationEvents.length === 0`
- 2026-05-15 执行修正:先补 `rust/crates/mnote-web/src/routes/tree.rs` debug/internal tree shell 回归,断言 local folder watch 分支包含 `/api/tree/local-folder-watch``refreshLocalFolderSnapshot`,且不含 `window.location.reload`;随后发现 3000 主入口实际走 `rust/crates/mnote-web/src/ssr/pages/layout.rs` 的主 Sidebar runtime,不能只改 debug tree shell。
- 2026-05-15 执行修正:主 Sidebar runtime 已新增 `startLocalFolderSidebarWatch()`,对 `sourceKind=local_folder` 轮询 `/api/tree/local-folder-watch`revision 变化后 fetch 当前 HTML 并用 `replaceSidebarTreeFromDocument()` 原地替换 `sidebar-tree-root``sidebar-file-tree-root`;启动后立即执行一次 baseline poll,避免外部变更发生在首轮定时轮询前被误当作初始 revision。
- 2026-05-15 验证:`cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_polls_local_folder_without_browser_reload -- --nocapture` 通过,断言主 Sidebar runtime 具备 local folder polling / snapshot apply 且不使用 `window.location.reload``cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_tree_shell_does_not_reload_page_for_refresh -- --nocapture` 通过,保留 debug/internal tree shell no-reload 防回归。
- 2026-05-15 真实浏览器 smoke:新增并执行 `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3427 node scripts/task435-local-folder-watch-no-reload-smoke.js` 通过。脚本使用临时本地目录,验证 Markdown 外部 create / rename / delete 在 page tree 与 filetree 原地更新;`.txt``.png` 两类非 md 资源外部 create / rename / delete 在 filetree 原地更新;所有步骤 `navigationEventsBefore=0``navigationEventsAfter=0`,结果文件 `tmp/task435-local-folder-watch-no-reload-smoke/result.json`
- 2026-05-15 `resync_required` 合同修正:只读审计确认前端 `applyTreeStreamDelta``op=resync_required` 不替换数据,只推进 cursor;已补 `wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.test.tsx`,断言 `resync_required` delta 保持旧数据、后续 `resync` 才替换数据。已补 `rust/crates/mnote-web/src/routes/stream_support.rs`,断言 matching command/domain event 中的 `streamDelta.op=resync_required` 会作为 delta 透传并推进 cursor。
- 2026-05-15 验证:`cd wolai-frontend && pnpm test src/lib/tree-stream/use-sidebar-tree-stream.test.tsx -- --runInBand` 通过,1 个测试文件、4 项通过;`cargo test --manifest-path rust/Cargo.toml -p mnote-web stream_change_preserves_resync_required_delta_contract -- --nocapture` 通过,1 项通过。
### 阶段 9Local Folder watcher 与本地资源生命周期
- [x] Markdown 外部 watcher 三件套:外部创建 `.md`、外部重命名 `.md`、外部删除 `.md`page tree 与 filetree 均无需手动刷新可见。
- [x] 打开的本地 Markdown 文档在外部修改 / 重命名 / 删除时,能收到明确刷新或冲突提示,不静默覆盖用户编辑。
- [x] 非 md 本地资源文件外部 create / rename / delete 在 filetree 可见,并记录实际机制是 `/api/tree/local-folder-watch` revision polling + HTML snapshot apply,不是 `LocalFolderWatcherRegistry` SSE delta,也不是 browser reload。
- [x] 非 md 本地资源文件 delete / restore / purge 支持本地垃圾箱或明确不支持;若支持,必须记录原路径、冲突策略与 purge 后索引清理。
- [x] 本地 folder watcher 验收脚本必须记录 `navigationEvents` / reload 次数;若发生 `window.location.reload()`,不得标为 no-refresh 完成。
验收标准:
- Markdown watcher 不依赖手动刷新;若内部实现仍需 reload,文档必须标为“reload 型刷新”,不能写成 no-refresh。
- 非 md 资源文件至少覆盖 `.txt/.png/.pdf/.xlsx` 中两类,create / rename / delete 后 filetree 状态与本地文件系统一致。
- 本地资源进入垃圾箱时必须写入 `.mnote/trash` 或等价索引;restore 回原路径,冲突时有可解释 fallback;purge 后原文件、trash 文件、索引均不可再查询。
执行记录:
- 2026-05-15:只读审计确认 `LocalFolderWatcherRegistry` 递归监听 root,但 `is_markdown_path` 只允许 `.md/.markdown`,非 md 直接过滤;`/api/local-folder/events` 主要服务打开的 Markdown session。
- 2026-05-15:只读审计确认主 tree 对 local folder 不接 `/api/tree/events`,而是用 `/api/tree/local-folder-watch` 轮询 revisionrevision 扫描可见条目并 hash path / name / dir / symlink / readonly / size / mtime,非 md 文件也会改变 revision,但当前变化后会触发 `window.location.reload()`
- 2026-05-15:只读审计确认 `task163` 覆盖外部新增 `watcher-added.md` 后 page tree 出现,以及外部新增 / 删除 `watcher-asset.txt` 后 filetree 出现 / 消失;缺少外部 Markdown rename / delete、非 md rename、非 md trash / restore / purge 的写入型浏览器验收。
- 2026-05-15 继续执行审计:`rust/crates/mnote-web/src/local_folder_watcher_registry.rs``should_emit_event_kind` 接受 create/remove/name/data/metadata,但实际发送前仍按 `is_markdown_path` 过滤;`rust/crates/mnote-web/src/routes/local_folder_events.rs` 也会按 `documentId` 限定单个 Markdown 相对路径,因此它更适合打开文档内容刷新,不足以承担完整 filetree 资源 watcher。
- 2026-05-15 继续执行审计:`rust/crates/mnote-web/src/routes/local_folder_source.rs``local_folder_watch_revision_for_root` 会遍历 visible files 并把 path/name/dir/symlink/readonly/size/mtime 纳入 hash,因此它可以发现 Markdown rename/delete 与非 md create/rename/delete;缺口在客户端收到 revision 变化后执行整页 reload,而不是应用 snapshot delta。
- 2026-05-15 执行记录:本轮已把 3000 主 Sidebar local folder watch 从“static / reload 型”改为 revision polling + HTML snapshot apply。`task435` 记录 `navigationEvents=[]`,覆盖 Markdown page tree 三件套、Markdown filetree 三件套、`.txt``.png` 非 md filetree 三件套。
- 2026-05-15 打开态本地 Markdown 修正:新增 `scripts/task436-local-markdown-open-document-external-change-smoke.js`,先红灯确认外部重命名后的打开 session 未进入 `external-change-conflict`,根因是 `/api/page-aggregate/<旧 documentId>` 404 被静默忽略,且 local folder SSE 对 rename/remove 事件按新 `documentId` 过滤后不能可靠触达旧 session。
- 2026-05-15 打开态本地 Markdown 修正:`rust/crates/mnote-web/src/routes/web_shell.rs` 已改为本地 Markdown 外部刷新遇到非 OK aggregate 响应时标记 `external-change-conflict`;对 `eventKind` 包含 `Remove` / `Name` 的 local folder 事件,不再仅按新 `documentId` 过滤,而是让同 root 打开的 session 重查当前 aggregate,由 conflict key 或 404 决定自动同步或冲突提示。
- 2026-05-15 验证:`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3427 node scripts/task436-local-markdown-open-document-external-change-smoke.js` 通过,覆盖打开文档外部修改自动同步、dirty 文档外部修改冲突提示、打开文档外部重命名冲突提示、打开文档外部删除冲突提示;四步 `navigationEventsBefore=0``navigationEventsAfter=0`,结果文件 `tmp/task436-local-markdown-open-document-external-change-smoke/result.json`
- 2026-05-15 验证:`cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_renders_local_markdown_with_same_sidebar_surfaces -- --nocapture``cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_polls_local_folder_without_browser_reload -- --nocapture``cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_tree_shell_does_not_reload_page_for_refresh -- --nocapture` 均通过。
- 2026-05-15 非 md 本地资源生命周期设计:已在 `design/04-tree-domain/process/4-27-resource-lifecycle-command-cutover-v1.md` 追加 `local_folder file_path resource lifecycle` 子阶段,明确 `tree.resource.archive/restore/purge``resourceKind=local_file``trashEntryId`、trash index、restore 冲突与 purge 幂等策略。
- 2026-05-15 非 md 本地资源生命周期实现:先补 `tree_command_local_folder_asset_trash_restore_and_purge_use_trash_index` 红灯,确认 `local:asset:docs/photo.png` delete 原本掉到 Markdown 删除路径;随后扩展 local folder executor,使 `local:asset:*` / `local:node:*` raw file 通过 `tree.resource.archive/restore/purge` 进入 `.mnote/trash``trash-index.json`restore 回原路径,purge 删除 trash 文件并清理索引。Markdown 页面原有 trash / restore / purge 回归保持不变。
- 2026-05-15 UI 接入:Rust 3000 主壳 filetree delete plan 对 `sourceKind=local_folder && rowKind=asset` 使用 rowId 作为本地资源 identityDelete / Backspace 走 `/api/tree/commands`,不再落到 `/api/media/batch`;补 `sidebar_filetree_delete_keys_support_mixed_doc_and_asset_selection` 字符串级回归。
- 2026-05-15 真实浏览器 smoke:新增并执行 `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3427 node scripts/task437-local-folder-asset-trash-lifecycle-smoke.js` 通过。脚本在本地临时 root 下创建 `docs/asset.txt`,主壳 filetree 选中后按 Delete,确认源文件消失、`.mnote/trash/asset.txt` 出现、trash index 包含 `local-file:docs/asset.txt`;随后通过 tree command restore 回原路径,再次 delete 后 purge,确认源文件、trash 文件和 trash index entry 均不存在;全程 `navigationEvents=[]`
- 2026-05-15 阶段 9 结论:Markdown 外部 watcher、打开态 Markdown 外部变更提示、非 md 外部可见性、本地非 md 资源 delete / restore / purge 与 no-reload 记录均已闭合;后续更完整的 `.pdf/.xlsx` 类型矩阵、restore 冲突 UI、Trash 工作台展示 local_file entry 可作为下一轮增强,不阻塞本阶段 checklist。
### 阶段 10Explorer parity 收口项
- [x] F2 inline rename validation:空名、非法名、同级重名、扩展名策略行内提示,并阻断提交。
- [x] Copy Relative PathConvex workspace-relative 路径已接入页面右键菜单;local root-relative 仍归入 local folder 阶段 9 验收。
- [x] Paste Into action target:主 Sidebar 节点右键使用当前菜单节点作为目标;Ctrl/Cmd+V 继续使用 focused target;空白区当前没有独立右键菜单入口,后续若新增必须复用同一 helper。
- [x] New Folder:当前 Convex workspace 页面树不支持 folder 概念,禁用态保留解释;后续若支持,必须等待 Resource Tree folder 合同并走 tree command。
- [x] `resync_required` 合同:前端明确 `resync_required` 只推进 cursor、不替换数据;后续 snapshot / resync 才替换数据。Rust stream 合同测试覆盖 matching command/domain event 的 `resync_required` delta 透传。
验收标准:
- VSCode Explorer 对标只能按“最低可用完成 / parity backlog”分层汇报,不能把禁用态或待补项描述成完整完成。
- 所有右键命令与键盘命令必须共享同一 action target 规则。
- 事件合同测试能复现“只发 resync_required 不发 resync 时 B 端不会更新”的失败模式,并用实现修复后变绿。
执行记录:
- 2026-05-15`use-sidebar-tree-stream.test.tsx` 已补 `resync_required delta 只推进 cursor,后续 resync 才替换数据``stream_support.rs` 已补 `stream_change_preserves_resync_required_delta_contract`。这固定了当前合同口径:`resync_required` 是“需要 resync 的信号”,不是可直接应用的数据 delta。
## 4. 最小浏览器验收脚本建议
测试数据前缀:`TEST-10REVIEW-07-<timestamp>`
必须覆盖:
- 登录 `http://localhost:3000/auth`,使用测试账号快速登录。
- 在 3000 主入口创建父页面、子页面、附件、mindmap、table。
- 文件树多选父页面 + 子页面 + 附件,按 Delete。
- 断言:文件树立即消失,垃圾箱 Drawer 或 `/trash` 立即出现相应条目。
- 恢复父页面,断言子页面恢复规则、位置、focus/reveal。
- 对同一批对象执行彻底删除 / 清空垃圾箱。
- 查询 Convex 或通过 API 断言 purge 后不存在。
- 双浏览器验证不刷新同步。
## 5. 不应误读的口径
- 不应说“04-tree 文件树已完全对标 VSCode Explorer”。更准确的说法是:文件树 renderer、selection、preflight、部分批量删除和垃圾桶基础已经具备;VSCode Explorer 产品闭环仍在收口。
- 不应说“Trash 已完整完成”。更准确的说法是:页面软删 / 恢复 / purge 与 Sidebar Drawer 基础已存在;Rust 主壳、`/trash` 入口、资源级命令、清空后 Convex 同步删除还需要端到端验收。
- 不应说“Resource Tree runtime 已完成”。更准确的说法是:object identity、block-asset relation、file_tree projection 合同已落地;资源树产品面与资源命令闭环待核验。
- 不应说“local folder 已满足 no-refresh”。更准确的说法是:local folder Markdown lifecycle 与非 md 文件基础可见性已有 smoke 覆盖,但当前本地 watch 仍有 `window.location.reload()`,非 md 资源 trash / restore / purge 未收口。
- 不应说“mindmap / table 已正式切到 tree.resource.*”。更准确的说法是:mindmap / table 生命周期已有 compat route 与 no-refresh 证据,正式资源命令合同仍待补。
- 不应把 07-ai 开发建立在“04-tree 删除 / 垃圾箱 / Explorer 能力已经全闭环”的假设上。后续 07-ai 若需要操作页面、附件、mindmap、table,必须优先走统一 tree command 和 projection,不要继续扩散 legacy route。