chore: checkpoint turso and ai runtime work
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
# 3-26 Local Folder FileChangeService 统一文件变化事件层 v1
|
||||
|
||||
> 创建时间:2026-06-29
|
||||
> 状态:`done`
|
||||
> Owner:`03-rust-web` 主控,协同 `05-editor-mainline`、`07-ai`
|
||||
> 来源:当前 Page AI / OpenHub / opencode 写入本地 `.md` 后,左侧当前文档页不会自动更新,整页刷新后才读取到新内容。
|
||||
> 参考:`reference-code/sidex-main` 的 watcher / `EventThrottler` / reaction 模型;`reference-code/vscode` 的 `FileService.onDidFilesChange`、Explorer 延迟刷新、TextFile editor reload/focus 补偿模型。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
不要新增 OpenHub / opencode 专属“写完刷新当前页”链路,也不要完整重建 VSCode FileService。
|
||||
|
||||
本轮应把 MNote 现有 local-folder watcher / browser event bus 收口成一个轻量 `FileChangeService`:
|
||||
|
||||
- Rust watcher、OpenHub/opencode changedFiles、legacy Page AI tool receipt、MNote 自身保存回声都只能作为统一文件变化事件的输入源。
|
||||
- 当前文档、filetree、page tree、knowledge source status 只消费统一 FileChange reaction,不直接解析 provider 事件。
|
||||
- 当前打开文档只有在非 dirty / 非 saving / 非 recent input 时自动拉取最新 Page Aggregate;dirty 时进入外部冲突状态,不覆盖编辑器内容。
|
||||
- filetree/page tree 继续走现有 projection / watch_batch / sidebar refresh 链路,但由统一事件层负责去抖、合并、self-write 抑制和最小刷新范围。
|
||||
|
||||
## 2. 背景与现状
|
||||
|
||||
当前已经存在文件变化监控链路,不应另起一套:
|
||||
|
||||
1. `rust/crates/mnote-web/src/local_folder_watcher_registry.rs`
|
||||
- 使用 `notify` 监听 local folder root。
|
||||
- 生成 `relativePath`、`documentId`、`eventKind`、`revision`、`observedFileVersion`、`bufferFileVersion`、`selfWriteEcho`。
|
||||
- 调用 `BufferStore.mark_external_modified_if_version_changed_with_outcome` 区分自写回声与外部修改。
|
||||
- 同步触发本地搜索索引刷新;该职责保持在 Rust 侧,不迁到浏览器 runtime。
|
||||
|
||||
2. `rust/crates/mnote-web/src/routes/local_folder_events.rs`
|
||||
- `/api/local-folder/events?treeLive=true` 复用同一个 watcher。
|
||||
- 聚合 watcher payload,输出 `watch_batch`。
|
||||
|
||||
3. `rust/crates/mnote-web/browser/local-folder-event-bus-runtime.js`
|
||||
- 复用单连接,转发 `watch_batch`。
|
||||
- 派发 `mnote:local-folder:watch-batch`、`tree:local-folder-watch-batch`、`mnote:local-folder:filetree-parent-changed`、`mnote:local-folder:document-changed`、`mnote:local-folder:resource-changed`、`mnote:local-folder:sidebar-refresh-requested`。
|
||||
|
||||
4. `rust/crates/mnote-web/browser/document-session-runtime.js`
|
||||
- 消费 `mnote:local-folder:document-changed`。
|
||||
- 命中当前 document session 后调用 `refreshSessionFromExternalChange`,再 fetch `/api/page-aggregate/...`。
|
||||
|
||||
问题说明:整页刷新后能看到 agent 写入内容,说明 Page Aggregate 读盘链路有效;当前缺口在“真实文件变化 / agent 写入结果 -> 当前打开 document session reaction”。
|
||||
|
||||
## 3. 参考实现判断
|
||||
|
||||
### 3.1 Sidex 可移植部分
|
||||
|
||||
从 `reference-code/sidex-main` 可迁移的是模型,不是完整代码:
|
||||
|
||||
- `crates/sidex-workspace/src/watcher.rs`
|
||||
- `notify` 事件包装。
|
||||
- path 级别 pending map 去重。
|
||||
- ignore pattern。
|
||||
|
||||
- `crates/sidex-workspace/src/file_watcher_events.rs`
|
||||
- `EventThrottler.record_own_write(path)` 抑制自身写入回声。
|
||||
- `ingest()` 合并短时间多事件。
|
||||
- `flush()` 把文件事件映射成 reaction。
|
||||
- dirty 文件 + external modified 进入冲突 reaction。
|
||||
|
||||
- `src/vs/workbench/contrib/files/browser/explorerService.ts`
|
||||
- Explorer 用延迟合并处理文件事件。
|
||||
- 编辑中不刷新树。
|
||||
- 只在展开父节点受影响时刷新。
|
||||
|
||||
### 3.2 Sidex 复核补充
|
||||
|
||||
Sidex 的关键收益不是更换 watcher,而是在 watcher 与消费者之间增加稳定事件质量层:
|
||||
|
||||
| 能力 | Sidex 模型 | MNote 当前状态 | 本设计处理 |
|
||||
|---|---|---|---|
|
||||
| 两段去重 | watcher pending map + `EventThrottler` | 只有 `local_folder_events.rs` 的 120ms 连续 payload 聚合 | 增加 per-root FileChange throttler,跨批次合并同一路径 |
|
||||
| own-write 处理 | `record_own_write` + 过期窗口 | Rust 已产出 `selfWriteEcho`,但 browser consumer 仍要各自判断 | 统一 reaction 层消费 `selfWriteEcho`,避免各消费方重复判断 |
|
||||
| Reaction | `FileWatcherReaction` | 前端各 runtime 自己解析 JSON | 增加统一 reaction schema,再兼容输出旧事件 |
|
||||
| dirty 冲突 | throttler/working copy 层决定 reload 或 conflict | `BufferStore` 有版本仲裁,前端也有 conflict panel 壳 | reaction 层统一把 dirty/saving/recent input 转成冲突 |
|
||||
| ignore | watcher 层忽略构建产物 | 主要覆盖 `.mnote` / 索引相关路径 | 后续补 `.git`、`node_modules`、`target` 等 |
|
||||
|
||||
### 3.3 不直接移植部分
|
||||
|
||||
- 不迁 Tauri `app.emit("watch-batch")`,MNote 使用 Rust Web SSE/WS/browser event bus。
|
||||
- 不迁 VSCode extension host `FileSystemWatcher`。
|
||||
- 不让 watcher 携带文件内容;MNote 正文读取仍走 Page Aggregate。
|
||||
- 不把 Sidex 的 settings/extensions reload 规则原样迁入;MNote 后续可补自己的偏好、插件、knowledge source reaction。
|
||||
- 不新增第二条 broadcast/event stream。统一层必须复用现有 `/api/local-folder/events` 与 browser event bus。
|
||||
|
||||
## 4. 目标架构
|
||||
|
||||
### 4.1 事件输入
|
||||
|
||||
统一输入源:
|
||||
|
||||
| 来源 | 入口 | 说明 |
|
||||
|---|---|---|
|
||||
| Rust local-folder watcher | `/api/local-folder/events?treeLive=true` | 主来源,真实磁盘文件变化 |
|
||||
| MNote 编辑器保存 | `BufferStore` / existing save completion event | 用于 self-write echo 抑制和保存态同步 |
|
||||
| OpenHub / opencode | changedFiles adapter | 只能转成 FileChange input,不直接刷新 UI |
|
||||
| legacy Page AI tool receipt | `agentRunReceipt.changedFiles` | 只能转成 FileChange input |
|
||||
| local upload / resource save | existing local-upload event | 资源 tab 与附件场景 |
|
||||
|
||||
### 4.2 标准事件
|
||||
|
||||
统一标准事件命名为 `mnote.file_change.v1`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "mnote.file_change.v1",
|
||||
"rootUri": "file:///...",
|
||||
"workspaceId": "local-ws:...",
|
||||
"relativePath": "新页面145827/新页面145827.md",
|
||||
"documentId": "local-md:...",
|
||||
"resourceKind": "markdown",
|
||||
"changeType": "modified",
|
||||
"fileVersion": "local-md:...",
|
||||
"source": "watcher|editor_save|openhub|opencode|agent_receipt|local_upload",
|
||||
"selfWriteEcho": false,
|
||||
"observedFileVersion": "...",
|
||||
"bufferFileVersion": "...",
|
||||
"createdAt": 1782738295000
|
||||
}
|
||||
```
|
||||
|
||||
批量事件命名为 `mnote.file_change_batch.v1`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "mnote.file_change_batch.v1",
|
||||
"rootUri": "file:///...",
|
||||
"workspaceId": "local-ws:...",
|
||||
"source": "watcher",
|
||||
"changes": [],
|
||||
"affectedParents": [],
|
||||
"revision": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Reaction
|
||||
|
||||
统一 reaction 命名为 `mnote.file_change_reaction.v1`:
|
||||
|
||||
| Reaction | 触发条件 | 消费方 |
|
||||
|---|---|---|
|
||||
| `refresh_current_document` | 当前打开文档 clean 且命中 changed `.md` | `document-session-runtime.js` |
|
||||
| `external_conflict_current_document` | 当前打开文档 dirty/saving/recent input 且同文件外部变化 | `document-conflict-panel-runtime.js` / session status |
|
||||
| `refresh_filetree_parents` | 创建/删除/重命名/父目录变化 | `sidebar-tree-live-apply-runtime.js` / filetree |
|
||||
| `refresh_page_tree_projection` | `.md` 页面新增/删除/重命名 | sidebar page tree |
|
||||
| `refresh_resource_tab` | 当前资源文件变化 | `document-resource-tab-runtime.js` |
|
||||
| `refresh_knowledge_source_status` | knowledge source 文件变化 | knowledge RAG status |
|
||||
| `ignore_self_write_echo` | `selfWriteEcho=true` 且 session 已处于 saved/clean | 不触发 UI 刷新 |
|
||||
|
||||
### 4.4 Browser 侧 owner
|
||||
|
||||
当前最小实现放在 `rust/crates/mnote-web/browser/local-folder-event-bus-runtime.js`,作为现有 event bus 的升级,不新增并行 runtime。
|
||||
|
||||
后续如果文件变大,再拆为:
|
||||
|
||||
- `filechange-event-runtime.js`:标准化、去抖、source adapter、diagnostics。
|
||||
- `filechange-reaction-runtime.js`:reaction 计算。
|
||||
- `local-folder-event-bus-runtime.js`:只负责 transport 与连接复用。
|
||||
|
||||
Rust 侧后续可增加类型化结构,但不要求首批大改广播签名:
|
||||
|
||||
- Phase A 保持 SSE JSON 兼容,补齐字段透传与 browser 标准化。
|
||||
- Phase B/C 再评估是否在 `mnote-web/src/file_change_service.rs` 引入 `FileChangeEvent` / `FileChangeReaction` Rust 类型,并由现有 `Value` 序列化出口兼容输出。
|
||||
- throttler 必须是 per-root 实例,放在 `LocalFolderWatchChannel` 或等价 root-scoped state 中,避免不同 workspace 互相去重。
|
||||
|
||||
## 5. 核心流程
|
||||
|
||||
### 5.1 外部编辑器 / agent 直接写 `.md`
|
||||
|
||||
1. 文件系统变化进入 Rust watcher。
|
||||
2. watcher 更新 BufferStore 并生成标准字段。
|
||||
3. `/api/local-folder/events?treeLive=true` 输出 `watch_batch`。
|
||||
4. browser event bus 标准化为 `mnote.file_change_batch.v1`。
|
||||
5. reaction 层判断命中当前 document session。
|
||||
6. 当前文档 clean:调用 `refreshSessionFromExternalChange` 拉 Page Aggregate 并派发 tiptap 内容。
|
||||
7. 当前文档 dirty:设置 `external-change-conflict`,不覆盖内容。
|
||||
|
||||
### 5.2 OpenHub / opencode 写当前页面
|
||||
|
||||
1. provider 事件或 session changedFiles 进入 adapter。
|
||||
2. adapter 将绝对路径解析为当前 `rootUri` 下 `relativePath`。
|
||||
3. 生成 `mnote.file_change_batch.v1`,source 为 `openhub` 或 `opencode`。
|
||||
4. 后续与 watcher 事件走同一 reaction。
|
||||
5. 如果真实 watcher 随后也到达,同一路径同版本事件被去抖合并,不重复刷新。
|
||||
|
||||
### 5.3 MNote 自己保存
|
||||
|
||||
1. 保存链记录 write intent / save operation / fileVersion。
|
||||
2. watcher 回声到达时带出 `selfWriteEcho=true`。
|
||||
3. FileChangeService 产出 `ignore_self_write_echo` 或只同步 clean metadata,不触发外部冲突。
|
||||
|
||||
## 6. 非目标
|
||||
|
||||
- 不引入周期轮询或 `setInterval` 检测文件是否变化。
|
||||
- 不把 OpenHub/opencode changedFiles 当作事实源;它只是 watcher 补偿输入。
|
||||
- 不让前端直接读取本地文件正文;正文仍走 Page Aggregate。
|
||||
- 不复活 BlockNote / Convex / Next 作为当前刷新主链。
|
||||
- 不把 filetree/page tree 的 projection 真相搬到前端。
|
||||
- 不在 AI runtime 中直接调用 document editor 私有刷新逻辑。
|
||||
|
||||
## 7. 实施 Checklist
|
||||
|
||||
### Batch A:现状断点复核
|
||||
|
||||
- [x] 复核 `LocalFolderWatcherRegistry` 产出的 `selfWriteEcho`、`observedFileVersion`、`bufferFileVersion` 是否完整进入 `watch_batch.changedPaths[]`。
|
||||
- [x] 复核 `/api/local-folder/events?treeLive=true` 是否对当前 workspace root 只有一个 watcher 连接。
|
||||
- [x] 复核 `local-folder-event-bus-runtime.js` 是否把 `documentId/eventKind/selfWriteEcho/fileVersion` 保留给 `document-session-runtime.js`。
|
||||
- [x] 复核 `document-session-runtime.js` 的命中条件:`documentId`、`relativePath`、`rootUri`、`workspaceId`。
|
||||
- [x] 复核当前截图场景中 Page AI/OpenHub 写入后是否有真实 watcher 事件、synthetic event、或两者都没有。
|
||||
- [x] 复核是否存在跨批次重复事件导致同一路径多次 refresh。
|
||||
- [x] 复核 ignore 规则是否放过 `.git`、`node_modules`、`target`、`dist` 等高频目录。
|
||||
|
||||
验收:
|
||||
|
||||
- [x] 产出一份断点日志或 smoke 证据,能指出“事件未到达 / 字段丢失 / session 未命中 / dirty 拦截 / refresh 失败”的具体层级。
|
||||
|
||||
### Batch B:标准 FileChange batch
|
||||
|
||||
- [x] 在 browser event bus 增加标准化函数:从 watcher `watch_batch`、synthetic changedFiles、legacy receipt 转成 `mnote.file_change_batch.v1`。
|
||||
- [x] 统一绝对路径到 root-relative path 的解析规则,路径不在 rootUri 下时丢弃并记录 diagnostics。
|
||||
- [x] 合并同一 `rootUri + relativePath` 的短时间重复事件。
|
||||
- [x] 保留 `source`、`eventKind`、`selfWriteEcho`、`observedFileVersion`、`bufferFileVersion`。
|
||||
- [x] 暴露 diagnostics:最近 batch source、changed count、dropped count、last reaction。
|
||||
- [x] 保持旧 `watch_batch.changedPaths/affectedParents/eventKinds` 字段兼容,新增标准 batch 不破坏旧消费者。
|
||||
- [x] 如 Rust 侧改类型,保证最终 SSE/JS JSON 格式仍兼容旧 smoke。
|
||||
|
||||
验收:
|
||||
|
||||
- [x] `task540-local-folder-event-bus-single-connection-smoke.js` 仍通过。
|
||||
- [x] 新增静态 smoke 确认 `mnote.file_change_batch.v1`、`selfWriteEcho`、`emitSyntheticWatchBatch` adapter 存在。
|
||||
|
||||
### Batch C:Reaction 分发
|
||||
|
||||
- [x] 新增 reaction 计算:当前文档 clean -> `refresh_current_document`。
|
||||
- [x] dirty/saving/recent input -> `external_conflict_current_document`。
|
||||
- [x] self-write echo -> `ignore_self_write_echo`。
|
||||
- [x] filetree affected parents -> `refresh_filetree_parents`。
|
||||
- [x] resource tab -> `refresh_resource_tab`。
|
||||
- [x] 保持旧事件兼容输出:`tree:local-folder-watch-batch`、`mnote:local-folder:document-changed`、`mnote:local-folder:resource-changed`。
|
||||
- [x] 不在 Rust watcher 回调中做新的重 I/O;reaction 计算应基于已有事件字段和 session state。
|
||||
|
||||
验收:
|
||||
|
||||
- [x] 外部写当前 `.md` 后,当前 tiptap 页面无需浏览器刷新显示新文本。
|
||||
- [x] 当前页面 dirty 时,外部写同一 `.md` 不覆盖编辑器,显示 external conflict。
|
||||
- [x] filetree/page tree 仍按局部 parent 刷新,不整页 reload。
|
||||
|
||||
### Batch D:AI source adapter 收口
|
||||
|
||||
- [x] OpenHub/opencode changedFiles 只调用统一 FileChange input,不直接调用 `refreshPrimaryDocument`。
|
||||
- [x] legacy Page AI receipt 只调用统一 FileChange input。
|
||||
- [x] 移除或降级旧的 provider 专属直接刷新路径;保留兼容时必须标注为 fallback diagnostics。
|
||||
- [x] changedFiles 为空时不伪造 diff;只依赖真实 watcher 或 explicit changedFiles。
|
||||
|
||||
验收:
|
||||
|
||||
- [x] agent/OpenHub/opencode 写当前 `.md` 后,当前页面自动更新。
|
||||
- [x] 若 provider 不报 changedFiles,真实 watcher 仍能驱动更新。
|
||||
- [x] 若 watcher 漏报但 provider 报 changedFiles,synthetic FileChange 可驱动同一 reaction。
|
||||
|
||||
### Batch E:回归与归档
|
||||
|
||||
- [x] 新增或更新 smoke:
|
||||
- [x] `task-local-folder-filechange-current-document-refresh-smoke.js`
|
||||
- [x] `task-page-ai-filechange-current-document-refresh-smoke.js`
|
||||
- [x] `task-local-folder-filechange-dirty-conflict-smoke.js`
|
||||
- [x] 运行 `cargo test -p mnote-web` 中相关单测。
|
||||
- [x] 运行 local-folder event bus、filetree lazy loading、page aggregate refresh 相关 smoke。
|
||||
- [x] `codegraph sync .` 并确认状态。
|
||||
- [x] 完成后把本设计从 `process/` 移到 `done/`,记录验证命令和剩余风险。
|
||||
|
||||
## 8. 风险与防线
|
||||
|
||||
- 事件重复:watcher 与 synthetic changedFiles 同时到达。防线:按 `rootUri + relativePath + fileVersion/source window` 去抖。
|
||||
- 自写回声误判外部冲突。防线:保留并消费 `selfWriteEcho`、write intent、save operation。
|
||||
- dirty 编辑器被覆盖。防线:dirty/saving/recent input 一律进入 conflict reaction。
|
||||
- filetree/page tree 首屏被 debug/diagnostics 挤占。防线:diagnostics 只写 DOM data attribute 或折叠开发态。
|
||||
- provider 事件格式不稳定。防线:OpenHub/opencode 只做 adapter,不作为系统事实源。
|
||||
- 前端拼第二份页面真相。防线:正文刷新只拉 Page Aggregate,不从 changedFiles 读取内容。
|
||||
- 第二条事件总线造成状态分裂。防线:只复用现有 `/api/local-folder/events` 与 browser event bus,不新增并行 broadcast。
|
||||
- throttler 误合并不同 workspace 事件。防线:per-root/per-workspace scoped state。
|
||||
|
||||
## 9. 验收定义
|
||||
|
||||
本设计完成的最低验收:
|
||||
|
||||
1. 真实外部写当前 `.md`,当前页面自动刷新正文。
|
||||
2. OpenHub/opencode/agent 写当前 `.md`,当前页面自动刷新正文。
|
||||
3. dirty 当前页遇到外部写入时不覆盖,显示冲突状态。
|
||||
4. 文件树/页面树仍使用同一 event bus 更新,没有新增并行轮询。
|
||||
5. OpenHub/opencode 没有直接持有编辑器刷新逻辑,只作为统一 FileChange input。
|
||||
|
||||
|
||||
## 10. 完成记录
|
||||
|
||||
完成时间:2026-06-29
|
||||
|
||||
实现摘要:
|
||||
|
||||
- Rust watcher batch 继续复用 `/api/local-folder/events?treeLive=true`,并在 `changedPaths[]` 保留 `documentId`、`eventKind`、`selfWriteEcho`、`observedFileVersion`、`bufferFileVersion`、`lastWriteIntentId`、`lastSaveOperationId`。
|
||||
- Browser `local-folder-event-bus-runtime.js` 升级为轻量 FileChangeService:标准化 `mnote.file_change_batch.v1`,80ms 去抖合并,输出 `mnote:file-change-batch` / `mnote:file-change-reaction`,并兼容旧 `tree:local-folder-watch-batch` / `mnote:local-folder:document-changed` / `resource-changed`。
|
||||
- `document-session-runtime.js` 统一消费 `selfWriteEcho`,clean 当前页走 Page Aggregate 自动刷新,dirty/saving/recent input 当前页进入 external conflict,不覆盖编辑器内容。
|
||||
- OpenHub/opencode/legacy receipt changedFiles 收口到 `emitChangedFiles` / FileChange input,不再直接调用 `refreshPrimaryDocument` 作为 provider 专属刷新链路。
|
||||
- watcher ignore 扩展到 `.git`、`node_modules`、`target`、`dist`、`build` 等高频目录。
|
||||
|
||||
验证命令:
|
||||
|
||||
- `node --check rust/crates/mnote-web/browser/local-folder-event-bus-runtime.js && node --check rust/crates/mnote-web/browser/document-session-runtime.js && node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js && node --check scripts/task540-local-folder-event-bus-single-connection-smoke.js && node --check scripts/task779-openhub-file-edit-document-pane-refresh-smoke.js && node --check scripts/task-local-folder-filechange-current-document-refresh-smoke.js && node --check scripts/task-local-folder-filechange-dirty-conflict-smoke.js && node --check scripts/task-page-ai-filechange-current-document-refresh-smoke.js`
|
||||
- `cargo fmt --check -p mnote-web`
|
||||
- `cargo test -p mnote-web local_folder -- --nocapture`
|
||||
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3010 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3010 node scripts/task540-local-folder-event-bus-single-connection-smoke.js`
|
||||
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3010 node scripts/task-local-folder-filechange-current-document-refresh-smoke.js`
|
||||
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3010 node scripts/task-local-folder-filechange-dirty-conflict-smoke.js`
|
||||
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3010 node scripts/task-page-ai-filechange-current-document-refresh-smoke.js`
|
||||
- `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3010 node scripts/task776-openhub-changed-files-bridge-smoke.js`(live probe 因缺少 task774 真实文件编辑结果跳过,静态检查通过)
|
||||
|
||||
剩余风险:
|
||||
|
||||
- `task-local-folder-filechange-current-document-refresh-smoke.js` 与 dirty conflict smoke 复用现有 task436 主流程,覆盖真实外部写入、AI 写入、dirty conflict、rename/delete conflict,但不是独立新实现的更细粒度脚本。
|
||||
- FileChange reaction 当前通过 document session debug snapshot 判断当前页状态,并已暴露 `data-mnote-file-change-service-session-api` diagnostics;后续可把这层升级成正式 session registry API。
|
||||
- `mnote:page-ai-tool-write-completed` 仍保留为 changedFiles 为空时的 legacy fallback,不作为 OpenHub/opencode/receipt changedFiles 主路径。
|
||||
Reference in New Issue
Block a user