2026-05-16 07:38:45 +08:00
|
|
|
|
# 4-27 Resource Lifecycle Command Cutover v1
|
|
|
|
|
|
|
2026-05-21 09:04:13 +08:00
|
|
|
|
> 状态:done
|
|
|
|
|
|
> 归档说明(2026-05-21):核心 `tree.resource.*` 生命周期命令 cutover 已完成;双浏览器 no-refresh、restore reveal/focus 等尾项已由 `4-34`、`4-28` 继续跟踪。
|
2026-05-16 07:38:45 +08:00
|
|
|
|
> 更新时间: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 asset;mindmap 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 asset;mindmap 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 index,restore / 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-15:Rust 3000 主壳 filetree delete plan 已对 `sourceKind=local_folder && rowKind=asset` 使用 rowId 作为本地资源 identity,Delete / 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
|
|
|
|
|
|
```
|