- 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
13 KiB
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:patchByIdtree.resource.restore -> mediaAssets:patchByIdtree.resource.purge -> mediaAssets:purgeByIdtree.resource.rename -> mediaAssets:patchById
storage-convex-bridge为上述命令提供历史 / cloud / compat source 的 Convex 映射;默认 local-first 资源生命周期不再依赖 Convex functions。wolai-frontend的/api/media/batch:delete改为tree.resource.archiverestore改为tree.resource.restorerename改为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. 验证命令
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-15:
mnote-webmindmap / table 兼容 route 已改为构造tree.resource.archive/restore/purgeruntime 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,最小字段如下:
trashEntryIdworkspaceIdrootIdresourceKind=local_fileoriginalFilePathtrashedFilePathfileSizecontentHash,可延后异步补齐,但 purge 前若存在则用于防误删校验archivedAtarchivedByCommandIdrestoreStatuspurgedAtpurgedByCommandId
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.pngdelete 原本掉到 Markdown 删除路径。 - 2026-05-15:
mnote-weblocal 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 验证命令
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