Files
mnote/design/04-tree-domain/done/4-27-resource-lifecycle-command-cutover-v1.md
T
lix-2026 1956a8a21a chore: align mvp design governance
- 统一 local-first MVP 后阶段架构口径,补充 process 执行总序和 Reasonix 协作记录

- 归档已完成的 design checklist,标注参考型 process,更新 AGENTS/REASONIX/架构文档

- 补充文件树/主编辑器下载与上下文菜单相关实现、bug 记录和 smoke 脚本

验证:git diff --check;codegraph sync .;cargo test -p mnote-web;node --check scripts/task476-filetree-editor-context-menu-download-smoke.js
2026-05-21 09:04:13 +08:00

163 lines
13 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.
# 4-27 Resource Lifecycle Command Cutover v1
> 状态:done
> 归档说明(2026-05-21):核心 `tree.resource.*` 生命周期命令 cutover 已完成;双浏览器 no-refresh、restore reveal/focus 等尾项已由 `4-34`、`4-28` 继续跟踪。
> 更新时间:2026-05-15
> 范围:`tree.resource.archive / restore / purge / rename` 正式命令面,以及 `/api/media/*`、mindmap、table 资源垃圾箱兼容入口的收口边界。
## 1. 本阶段目标
- 普通附件 file asset 的删除、恢复、永久删除、重命名必须具备正式 `tree.resource.*` 命令。
- `/api/media/batch``/api/media/purge` 可以继续作为历史 URL,但只能作为兼容 alias,内部必须生成正式 command plan 并记录 command artifact。
- mindmap / table 旧 URL 可以保留为兼容 route,但 delete / restore / purge 必须迁移到同一 `tree.resource.*` 命令 payload,不再扩写分散 route 语义。
## 2. 已落地范围
- `bridge-runtime` 新增 file asset 生命周期命令计划:
- `tree.resource.archive -> mediaAssets:patchById`
- `tree.resource.restore -> mediaAssets:patchById`
- `tree.resource.purge -> mediaAssets:purgeById`
- `tree.resource.rename -> mediaAssets:patchById`
- `storage-convex-bridge` 为上述命令提供默认 Convex 映射。
- `wolai-frontend``/api/media/batch`
- `delete` 改为 `tree.resource.archive`
- `restore` 改为 `tree.resource.restore`
- `rename` 改为 `tree.resource.rename`
- `wolai-frontend``/api/media/purge` 改为 `tree.resource.purge`
- `mnote-web` 的 Rust 3000 `/api/media/batch``/api/media/purge` 改为通过 runtime command 执行,旧 URL 仅作为兼容 alias。
- `mnote-web` 的 Rust 3000 `/api/mindmap/{docId}/{mindmapId}` DELETE / PATCH restore / PATCH purge 改为通过 `tree.resource.archive / restore / purge` 执行,旧 URL 仅作为兼容 alias。
- `mnote-web` 的 Rust 3000 `/api/tables/{tableId}` DELETE、`/api/tables/restore``/api/tables/purge` 改为通过 `tree.resource.archive / restore / purge` 执行,旧 URL 仅作为兼容 alias。
- tree shell command event schema 已补资源生命周期事件。
## 3. 未完成边界
- mindmap 与 table 的 DELETE / restore / purge 已进入 `tree.resource.*` runtime command;但旧 URL 仍保留为兼容入口,前端 Sidebar 仍按资源类型分流到这些旧 URL。
- `tree.resource.rename` 当前只覆盖普通附件 file assetmindmap rename 暂不支持,table rename 需要结合 `tables:update` 与 UI 标题语义另行收口。
- `empty-trash` 仍是 workspace 级批量兼容合同:`mediaAssets.emptyTrashByWorkspace``mindmaps.emptyTrashByWorkspace``tables.emptyTrashByWorkspace`,暂不展开为逐资源 `tree.resource.purge`
- 双浏览器 no-refresh、垃圾箱恢复后的 reveal/focus、跨资源 projection delta 细粒度更新仍由后续阶段覆盖。
## 4. 验收
- 普通附件通过 `/api/media/batch delete/restore/rename``/api/media/purge` 操作时,测试中应观察到正式命令名 `tree.resource.archive/restore/rename/purge`
- Mindmap 通过 `/api/mindmap/{docId}/{mindmapId}` DELETE / PATCH restore / PATCH purge 操作时,响应必须携带 `canonicalCommand=tree.resource.archive/restore/purge``resourceKind=mindmap`
- Table 通过 `/api/tables/{tableId}` DELETE、`/api/tables/restore``/api/tables/purge` 操作时,响应必须携带 `canonicalCommand=tree.resource.archive/restore/purge``resourceKind=table`
- Rust runtime plan 必须包含 `domainEventPlan``streamDeltaHint`
- Rust 3000 兼容 route 不再直接写 `mediaAssets:patchById / purgeById`,而是先进入 runtime command。
- 现有垃圾箱 route 回归保持通过。
## 5. 验证命令
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p bridge-runtime tree_resource_lifecycle_plans_cover_file_asset_commands
cargo test -p bridge-runtime tree_resource_lifecycle_plans_cover_mindmap_and_table_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
```
```bash
cd /mnt/Data1T/mnote/wolai-frontend
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
```
## 6. 执行记录
- 2026-05-15:先补 `mnote-web` 路由测试断言 mindmap / table DELETE、restore、purge 响应必须携带 `canonicalCommand=tree.resource.archive/restore/purge` 与对应 `resourceKind`,测试红灯确认旧路由仍只返回 legacy mutation 结果。
- 2026-05-15`mnote-web` mindmap / table 兼容 route 已改为构造 `tree.resource.archive/restore/purge` runtime command,并通过 `execute_runtime_command_via_convex_with_artifacts` 执行;旧 URL 保留。
- 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 项通过。
- 2026-05-15 验证:`cargo test --manifest-path rust/Cargo.toml -p mnote-web trash_routes -- --nocapture --test-threads=1` 通过,3 项通过。
- 2026-05-15 验证:`cargo test --manifest-path rust/Cargo.toml -p storage-convex-bridge tree_resource_lifecycle_commands_have_convex_mapping -- --nocapture` 通过,1 项通过。
## 7. 剩余边界
- `tree.resource.rename` 仍只覆盖普通附件 file assetmindmap rename 继续明确不支持,table rename 待结合 `/api/tables/{tableId}` PATCH 与表格标题语义单独收口。
- `empty-trash` 仍是 workspace 级批量合同,不把它伪装成逐资源 `tree.resource.purge`
- 前端 Sidebar 仍按类型调用旧 URL;这是兼容 alias,不是新增长期业务语义。后续如要统一客户端命令面,应在 `resource-command-client` 引入 mindmap/table payload,并保留旧 URL 适配层。
## 8. 子阶段:local_folder file_path resource lifecycle
### 8.1 目标与范围
本子阶段只覆盖 `local_folder` workspace 中由本地文件系统提供的非 md 资源文件,典型对象包括图片、PDF、音视频、Office 文件、压缩包和其他附件型二进制文件。Markdown 页面本体、目录节点、远端 media asset、mindmap、table 不在本子阶段内。
最小目标是把本地非 md 资源文件的 delete / restore / purge 纳入 `tree.resource.*` 正式命令面,同时保留本地文件路径作为用户可理解的 identity 输入。实现不要求立即接入云端文件删除,也不要求新增复杂版本库;只要求命令可审计、垃圾箱可索引、冲突可判定、恢复行为可预测。
### 8.2 最小设计
- 资源归属:`resourceKind=local_file``resourceScope=local_folder`
- 资源定位:命令 payload 必须携带 `workspaceId``rootId``filePath``resourceKind``filePath` 使用相对 `local_folder` 根目录的规范化 UTF-8 路径,不允许绝对路径、空路径、`..` 逃逸或平台分隔符混用。
- md 排除:扩展名判定为 Markdown 的文件继续走页面/树节点生命周期,不进入 `local_file` 资源生命周期。
- delete`tree.resource.archive` 不直接物理删除文件,先把源文件移动到本工作区受控 trash 存储区,并写入 trash index。
- restore`tree.resource.restore` 只从 trash index 中恢复已归档资源,默认恢复到原 `filePath`;若原路径冲突,按冲突策略处理。
- purge`tree.resource.purge` 只允许删除 trash index 中已归档资源对应的 trash 文件和索引记录;不允许对当前活跃文件路径直接 purge。
- 事件:三类命令都产出 `domainEventPlan``streamDeltaHint`,至少能通知资源列表、附件引用状态、垃圾箱视图刷新。
### 8.3 命令 identity
`local_folder` 文件的稳定命令 identity 分两层:
- 用户输入 identity`workspaceId + rootId + normalizedFilePath`。这是兼容 UI、外部变更监听和命令 payload 的最小输入。
- 归档后 identity`trashEntryId`。第一次 archive 成功后生成并写入 trash indexrestore / purge 必须优先使用 `trashEntryId`,同时校验其记录的原始 `filePath``workspaceId``rootId` 与命令 payload 一致。
`resourceId` 不直接等同于裸 `filePath`。对活跃文件可派生为 `local_file:{workspaceId}:{rootId}:{pathHash}`,但该值只作为事件和 projection 的稳定 key;实际文件操作仍以规范化路径和 trash index 记录为准,避免路径重命名、大小写差异或 Unicode 归一化导致误删。
### 8.4 Trash index
每个 `local_folder` root 维护一个受控 trash index,最小字段如下:
- `trashEntryId`
- `workspaceId`
- `rootId`
- `resourceKind=local_file`
- `originalFilePath`
- `trashedFilePath`
- `fileSize`
- `contentHash`,可延后异步补齐,但 purge 前若存在则用于防误删校验
- `archivedAt`
- `archivedByCommandId`
- `restoreStatus`
- `purgedAt`
- `purgedByCommandId`
trash index 是 restore / purge 的唯一可信入口。目录扫描只能用于发现活跃文件或 orphan trash 文件,不能绕过 index 直接恢复或永久删除。
### 8.5 冲突策略
- archive 时源文件不存在:命令返回 `not_found`,不生成新的 trash entry;若 index 已存在同一路径的活跃归档记录,返回该 `trashEntryId` 并标记为幂等归档结果。
- archive 时目标 trash 路径已存在:生成新的 `trashEntryId` 与唯一 `trashedFilePath`,不覆盖旧 trash 文件。
- restore 时原路径不存在:直接恢复到 `originalFilePath`,并把 index 状态改为 restored。
- restore 时原路径已存在且 contentHash 相同:视为幂等恢复,index 状态改为 restored,不覆盖活跃文件。
- restore 时原路径已存在且 contentHash 不同或无法判定:不得覆盖活跃文件;返回 `conflict=file_path_occupied`,UI 可选择恢复为带后缀副本或让用户改名,但默认命令不自动改名。
- purge 时 trash 文件不存在但 index 仍为 archived:允许把 index 标记为 purged,并记录 `missing_trash_file`,用于容忍外部手动清理。
- purge 时 index 状态不是 archived:返回幂等结果,不删除任何活跃文件。
- 任一命令发现 `filePath` 逃逸 root、大小写归一化后指向不同文件、或 symlink 指向 root 外部:返回 `invalid_path`,不移动、不删除。
### 8.6 验收标准
- 本地非 md 资源文件 delete 只能通过 `tree.resource.archive` 进入 trash,不允许直接 unlink 活跃文件。
- restore / purge 必须优先基于 `trashEntryId` 与 trash index 执行;仅传 `filePath` 时只能定位唯一 archived entry,不能模糊匹配多条记录。
- trash index 中能追踪原路径、trash 路径、归档命令、恢复/永久删除命令和当前状态。
- restore 冲突时默认不覆盖用户已有文件,并返回可被 UI 展示的结构化冲突原因。
- purge 只能作用于 archived trash entry,不能对 active `filePath` 执行永久删除。
- Markdown 文件不进入本子阶段;测试中应验证 `.md` / `.markdown` 路径被拒绝或转交页面生命周期。
- 命令事件必须包含 `resourceKind=local_file``resourceScope=local_folder``filePath``trashEntryId`archive 成功后)和 `canonicalCommand=tree.resource.archive/restore/purge`
- 外部变更监听与本命令并发时,以文件系统当前状态和 trash index 状态共同判定;不得因 stale projection 覆盖真实文件系统变化。
### 8.7 执行记录
- 2026-05-15:先补 `tree_command_local_folder_asset_trash_restore_and_purge_use_trash_index` 红灯,确认 `local:asset:docs/photo.png` delete 原本掉到 Markdown 删除路径。
- 2026-05-15`mnote-web` local folder executor 已把 `delete / restore / purge` 分流到 entry 级处理;`local:asset:*` / `local:node:*` raw file 通过 `tree.resource.archive/restore/purge` 进入 `.mnote/trash``trash-index.json`,Markdown 页面继续走原页面生命周期。
- 2026-05-15Rust 3000 主壳 filetree delete plan 已对 `sourceKind=local_folder && rowKind=asset` 使用 rowId 作为本地资源 identityDelete / Backspace 走 `/api/tree/commands`,不走 `/api/media/batch`
- 2026-05-15:新增 `scripts/task437-local-folder-asset-trash-lifecycle-smoke.js`,真实浏览器覆盖 local asset Delete 进入本地回收站、restore 回原路径、purge 清理 trash 文件与索引;结果文件 `tmp/task437-local-folder-asset-trash-lifecycle-smoke/result.json` 记录 `navigationEvents=[]`
### 8.8 验证命令
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder_asset_trash_restore_and_purge_use_trash_index -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder_create_rename_copy_trash_restore_and_purge_use_same_endpoint -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_delete_keys_support_mixed_doc_and_asset_selection -- --nocapture
MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3427 node scripts/task437-local-folder-asset-trash-lifecycle-smoke.js
```