Files
mnote/design/04-tree-domain/done/4-27-resource-lifecycle-command-cutover-v1.md
T
lix-2026 47e224d419 chore: align sqlite control plane architecture
- replace default Convex control-plane wording with Rust SQLite control-plane across architecture, AGENTS, Reasonix, and design docs

- retire root Convex functions source and deploy script into recycle while keeping explicit cloud/compat/sync-replica boundaries

- add control-plane migration guard/docs and keep CodeGraph refreshed after the SQLite control-plane cutover
2026-05-22 17:45:22 +08:00

13 KiB
Raw Blame History

4-27 Resource Lifecycle Command Cutover v1

状态:done 归档说明(2026-05-21):核心 tree.resource.* 生命周期命令 cutover 已完成;双浏览器 no-refresh、restore reveal/focus 等尾项已由 4-344-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 为上述命令提供历史 / cloud / compat source 的 Convex 映射;默认 local-first 资源生命周期不再依赖 Convex functions。
  • 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.emptyTrashByWorkspacemindmaps.emptyTrashByWorkspacetables.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/purgeresourceKind=mindmap
  • Table 通过 /api/tables/{tableId} DELETE、/api/tables/restore/api/tables/purge 操作时,响应必须携带 canonicalCommand=tree.resource.archive/restore/purgeresourceKind=table
  • Rust runtime plan 必须包含 domainEventPlanstreamDeltaHint
  • Rust 3000 兼容 route 不再直接写 mediaAssets:patchById / purgeById,而是先进入 runtime command。
  • 现有垃圾箱 route 回归保持通过。

5. 验证命令

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
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-15mnote-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/purgetable 映射到 tables:remove/restore/purge,并携带 resourceLifecyclePlandomainEventHint 与 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_fileresourceScope=local_folder
  • 资源定位:命令 payload 必须携带 workspaceIdrootIdfilePathresourceKindfilePath 使用相对 local_folder 根目录的规范化 UTF-8 路径,不允许绝对路径、空路径、.. 逃逸或平台分隔符混用。
  • md 排除:扩展名判定为 Markdown 的文件继续走页面/树节点生命周期,不进入 local_file 资源生命周期。
  • deletetree.resource.archive 不直接物理删除文件,先把源文件移动到本工作区受控 trash 存储区,并写入 trash index。
  • restoretree.resource.restore 只从 trash index 中恢复已归档资源,默认恢复到原 filePath;若原路径冲突,按冲突策略处理。
  • purgetree.resource.purge 只允许删除 trash index 中已归档资源对应的 trash 文件和索引记录;不允许对当前活跃文件路径直接 purge。
  • 事件:三类命令都产出 domainEventPlanstreamDeltaHint,至少能通知资源列表、附件引用状态、垃圾箱视图刷新。

8.3 命令 identity

local_folder 文件的稳定命令 identity 分两层:

  • 用户输入 identityworkspaceId + rootId + normalizedFilePath。这是兼容 UI、外部变更监听和命令 payload 的最小输入。
  • 归档后 identitytrashEntryId。第一次 archive 成功后生成并写入 trash indexrestore / purge 必须优先使用 trashEntryId,同时校验其记录的原始 filePathworkspaceIdrootId 与命令 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_fileresourceScope=local_folderfilePathtrashEntryIdarchive 成功后)和 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-15mnote-web local folder executor 已把 delete / restore / purge 分流到 entry 级处理;local:asset:* / local:node:* raw file 通过 tree.resource.archive/restore/purge 进入 .mnote/trashtrash-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 验证命令

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