Files
mnote/design/04-tree-domain/done/4-49-local-file-operation-event-contract-v1.md
T
lix-2026 1882db7681 收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
2026-06-01 09:29:12 +08:00

239 lines
12 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-49 Local File Operation 与 Watch Event 合同 v1
> 状态:done
> Owner04-tree-domain / 03-rust-web
> 背景:Sidex / VSCode Explorer 对照显示,MNote local_folder 的 tree command、watch event、refresh/reveal 和 opened resource 生命周期之间仍缺少稳定合同。当前前端常靠 `relativePath` / `previousRelativePath` 反推刷新父级,watch event 又容易退回全量 resync,导致大目录、批量操作和已打开资源场景下出现重复刷新、状态漂移和错误恢复困难。
## 1. 结论
local_folder 命令结果必须从“执行成功 + 少量路径字段”升级为“文件操作事件合同”:
1. 后端声明 `affectedParents`,前端不再猜 old/new/current parent。
2. 命令结果携带 `revealTarget` / `selectTarget`,刷新后可稳定落焦。
3. watch event 按 root 合并批次,输出 `changedPaths``affectedParents``eventKinds`
4. opened editor/resource 通过 participant 参与 rename/move/delete,处理 buffer rekey、stale、close 或 redirect。
5. 批量 paste/delete/move 用 batch id 聚合执行结果和刷新点。
## 2. Sidex / VSCode 对照
### 2.1 可借鉴的行为
- ExplorerService 对 create/copy/move/delete 明确计算 old parent、new parent、nested parent,并刷新对应 item。
- file changes 先进入延迟队列,批量判断是否影响已解析模型,再决定是否 refresh。
- WorkingCopyFileService 在 move/delete 前后处理 dirty working copy,失败时发结构化 fail event。
- BulkFileEdits 按批次执行,收集 per-item result 和 undo 操作。
### 2.2 不照搬的内容
- 不引入 VSCode undo/redo service 全模型。
- 不引入完整 working copy service container。
- 不改变 MNote 当前 local-first 文件系统事实源。
- 不把 Convex command 分支重新变成本地默认路径。
## 3. 当前缺口
### 3.1 command result 刷新边界不足
当前 `local_folder` command 走 `execute_local_tree_command_with_sort` 后返回 `execution`,前端再用 `relativePath``previousRelativePath``parentRelativePath` 和 request body 推断刷新父级。这个推断对 rename、move、trash、restore、bulk paste 都容易遗漏 old parent 或 new parent。
### 3.2 批量操作没有一致提交点
paste / bulk delete 当前逐项 dispatch command,单项成功后可能立即 patch DOM。多个结果交错时,UI 会经历多次 refresh,失败项也缺少结构化状态。
### 3.3 watch event 粒度偏粗
watcher 可以观察 path 事件,但 live event route 仍容易 rebuild full snapshot / resync。前端 fallback 有 debounce,服务端 SSE/resync 路径缺同等批处理语义。
### 3.4 opened resource 生命周期缺 participant
rename/move/delete 对已打开 Markdown buffer、mindmap、office resource tab 的影响仍分散:
- Markdown buffer key 绑定 relative path。
- resource tab identity 由前端字符串拼接。
- delete/archive 后是否 close、mark stale、redirect,没有统一参与层。
## 4. 目标
- 所有 local_folder tree command 成功结果都能说明影响哪些 parent。
- 前端 refresh scheduler 只消费 `affectedParents`,不解析 action-specific 路径字段。
- 批量操作只在 batch 完成后统一 refresh/apply。
- watcher event 按 root 合并,优先刷新 visible/loaded affected parents。
- opened resource 在文件操作前后收到 participant 通知,能处理 dirty、rekey、stale、close。
- 所有失败路径发结构化 error,包含 phase/rootUri/action/path/batchId。
## 5. 非目标
- 不实现全局 undo/redo。
- 不重写 trash 物理存储。
- 不改变现有 tree command URL。
- 不把 resource tab group persistence 放进本设计;那属于 editor-mainline 后续。
## 6. 协议草案
### 6.1 Command result
建议 local command result 至少包含:
```json
{
"schema": "mnote.local_file_operation_result.v1",
"operationId": "uuid-or-short-id",
"batchId": "",
"action": "rename",
"rootUri": "file:///mnt/Data1T/mnote",
"resource": {
"documentId": "local-md:design/foo.md",
"relativePath": "design/foo.md",
"objectIdentity": "resource:file:file:///mnt/Data1T/mnote:design/foo.md"
},
"previousResource": {
"documentId": "local-md:design/old.md",
"relativePath": "design/old.md",
"objectIdentity": "resource:file:file:///mnt/Data1T/mnote:design/old.md"
},
"affectedParents": [
{ "relativePath": "design", "reason": "old-parent" },
{ "relativePath": "design/new", "reason": "new-parent" }
],
"revealTarget": {
"relativePath": "design/foo.md",
"rowId": "local:file:design/foo.md"
},
"selectTarget": {
"relativePath": "design/foo.md",
"rowId": "local:file:design/foo.md"
}
}
```
约束:
- rename same-parent 也必须返回 affected parent。
- move cross-parent 必须返回 old parent 和 new parent。
- delete/archive 必须返回 old parent 和 next focus hint。
- restore 必须返回 restored parent 和 reveal target。
- `objectIdentity` 必须来自后端 canonical identity,不由前端拼接。
### 6.2 Batch result
批量操作返回:
```json
{
"schema": "mnote.local_file_operation_batch_result.v1",
"batchId": "batch-20260527-001",
"action": "paste",
"succeeded": 3,
"failed": 1,
"results": [],
"affectedParents": [],
"primaryRevealTarget": {}
}
```
前端规则:
- batch 未完成前不做全量 refresh。
- per-item 可做乐观占位,但最终以 batch result 的 affected parents 为准。
- 部分失败时展示结构化失败列表,不用 `alert` 拼字符串作为唯一反馈。
### 6.3 Watch event batch
服务端按 root 聚合短窗口:
```json
{
"schema": "mnote.local_folder_watch_batch.v1",
"rootUri": "file:///mnt/Data1T/mnote",
"watchRevision": {},
"changedPaths": [
{ "relativePath": "design/foo.md", "kind": "modified" }
],
"affectedParents": [
{ "relativePath": "design", "reason": "child-modified" }
],
"eventKinds": ["modified"],
"fallbackResync": false
}
```
前端规则:
- visible/loaded affected parent:局部 refresh。
- collapsed/loaded parent:标记 stale。
- 未 loaded parent:只记录 dirty,不加载。
- `fallbackResync=true`:刷新当前 scope parent,保留 expanded cache。
### 6.4 FileOperationParticipant
后端或 runtime 层新增参与点:
```text
beforeLocalFileOperation
afterLocalFileOperation
onLocalFileOperationFailed
```
参与者:
- `DocumentBufferStore`rename/move 后 rekeydelete/archive 后 mark stale 或 close。
- `ResourceTabRuntime`resource identity 改变时更新 tab key,目标被删除时显示 stale/closed state。
- `SearchIndex`:按 changed path 增量刷新,避免 query 时 rebuild。
- `FileTreeRefreshScheduler`:收集 affected parents。
## 7. Checklist
- [x] 审查 `execute_local_tree_command_with_sort` 每个 action 当前返回字段。
- [x] 定义 Rust `LocalFileOperationResult` / `AffectedParent` / `RevealTarget` 类型。
- [x] 让 create/rename/move/archive/restore/purge 返回统一 `affectedParents`
- [x] 增加过渡期 `affectedParents` 合成器,根据后端已有 `previousRelativePath` / `relativePath` / `parentRelativePath` 声明 old/new/current parent。
- [x] 修改前端 `fileTreeRefreshParentsForCommand`,优先消费 `result.affectedParents`
- [x] 为 legacy result 保留兼容 fallback,但加日志标记。
- [x] 给 bulk paste/delete 增加 batch id,完成后统一 refresh。
- [x] watch registry 或 event route 增加 root 级 debounce batch。
- [x] `tree:resync` payload 缺失或 rebuild 失败时发结构化 error event。
- [x]`DocumentBufferStore` participant 设计切片:rename/move rekeydelete/archive mark stale。
- [x] 补 smoke:批量删除、跨父级移动、rename opened markdown、watch batch 不全量折叠。
## 8. 验收
- rename same-parent 只刷新该 parent,目标 row 保持 selected/focused。
- move cross-parent 刷新 old parent 和 new parent,不刷新 workspace root。
- bulk delete 10 个文件只触发一次 batch refresh。
- 外部连续创建/删除多个文件时,watch event 合并成一个 batch。
- 已打开 Markdown rename 后,buffer key 和 tab identity 更新,不出现旧路径保存覆盖。
- 已打开 resource 被 archive/delete 后,tab 进入 stale/closed state,不继续写旧路径。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_tree_command -- --test-threads=1` 通过。
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js` 通过。
- 新增批量/watch smoke 通过。
## 9. 风险
- 如果 command result 同时保留新旧字段,前端可能继续依赖旧推断。迁移期需要明确优先级:`affectedParents` 优先,旧字段只作 fallback。
- participant 不能阻塞文件系统操作太久;dirty/conflict 处理需要有明确 timeout 或失败策略。
- watch batch 不能吞掉不可恢复错误;需要把 watcher unhealthy / fallback polling 状态暴露给前端。
## 10. 执行记录
- 2026-05-27:在 `execute_local_tree_command_with_sort(...)` 出口新增 `add_local_tree_command_affected_parents(...)`,基于现有 command result 字段生成 `affectedParents`
- 2026-05-27:新增 `local_tree_command_move_returns_affected_parents`,验证跨父级 move 返回旧父级 root 和新父级 `docs`
- 2026-05-27:前端 `fileTreeRefreshParentsForCommand(...)` 新增 `addAffectedParentsFromCommandResult(...)`,优先消费 `result.affectedParents`;旧字段推断保留为 fallback。
- 2026-05-27:新增 `sidebar_filetree_runtime_prefers_command_affected_parents` 契约测试。
- 2026-05-27:完成 `execute_local_tree_command_with_sort(...)` action 返回字段审查;发现 delete/archive/purge、目录 move、legacy wrapper 是主要缺口。
- 2026-05-27:新增 Rust `LocalFileOperationResult` / `AffectedParent` / `RevealTarget` / `LocalFileOperationResource` 类型,并在 local_folder 命令出口统一补 `schema= mnote.local_file_operation_result.v1``operationId``rootUri``resource/previousResource``affectedParents``revealTarget/selectTarget`
- 2026-05-27:补齐 delete/archive/purge 的 `originalRelativePath`、目录 move 的 `previousRelativePath`;前端 affectedParents fallback 支持 `result.execution.affectedParents`legacy fallback 标记 `data-mnote-filetree-command-refresh-fallback`
- 2026-05-27bulk paste/delete 增加 `batchId``tree:local-command` 按 batch 暂存 affected parents`tree:local-command-batch-complete` 后统一 refresh;修复 bulk delete local asset 缺少 `removeFileTreeAssetRow` 依赖导致命令成功但 UI 记录失败的问题。
- 2026-05-27local folder tree live SSE 从逐事件 full resync 改为 120ms root 级 `watch_batch`payload 输出 `changedPaths/affectedParents/eventKinds/fallbackResync=false`;重建失败/缺 payload 输出 `mnote.tree_live_error.v1` 结构化 error。
- 2026-05-27`DocumentBufferStore` 增加 local file operation participantrename/move rekey opened Markdown bufferdelete/archive/purge mark deletedtree command local_folder 分支执行后调用 participant。
- 2026-05-27:扩展 smoke`task494` 覆盖 watch batch 不折叠、opened Markdown rename buffer rekey、未加载深层 reveal`task471` 覆盖 bulk delete batchId 与 batch 完成后统一 refresh。
- 验证:
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_tree_command_move_returns_affected_parents -- --nocapture`,通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_runtime -- --nocapture`5 passed。
- `node scripts/task494-filetree-lazy-loading-dedup-smoke.js`,通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_tree_command_ -- --nocapture --test-threads=1`11 passed。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_events -- --nocapture --test-threads=1`7 passed。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web document_buffer_ -- --nocapture --test-threads=1`13 passed。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_runtime -- --nocapture --test-threads=1`10 passed。
- `node scripts/task494-filetree-lazy-loading-dedup-smoke.js`,通过,覆盖 lazy dedup、scope generation、watch batch、reveal、opened rename buffer rekey。
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:38291 node scripts/task471-local-folder-bulk-resource-trash-smoke.js`,通过,覆盖 bulk delete batchId 与 batch refresh。