fix: align local markdown conflict and title behavior

This commit is contained in:
lix-2026
2026-05-24 18:16:05 +08:00
parent 289a44acea
commit 9c4bb58257
9 changed files with 664 additions and 17 deletions
@@ -0,0 +1,67 @@
# 5-27 本地 Markdown Working Copy 冲突合同 v1
## 背景
`bugs/0524.md` 第 2 条暴露的问题不是单个上传入口错误,而是本地文件夹 watcher、正文 session、文件树资源事件和冲突 UI 之间缺少清晰边界:
- 上传附件或向文件树拖入文件后,非 Markdown 文件变化会进入正文外部变更链路。
- 新建页面后,旧的冲突提示可能被同 root 的事件流带到新页面,刷新后消失。
- 真实冲突仍然需要保留:当前 Markdown 有未保存编辑,同时磁盘上的同一个 Markdown 文件被外部修改时,必须进入冲突处理。
## Sidex 对照
Sidex/VSCode 的核心模型是 `StoredFileWorkingCopy`
- 每个 working copy 绑定一个具体 `resource`
- 保存时以 `lastResolvedFileStat.etag/mtime` 做 dirty write prevention。
- 只有同一个 resource 的写入出现 `FILE_MODIFIED_SINCE` 时,才进入 `inConflictMode`
- 文件系统 watcher 的目录级事件不会直接把同目录其它文件变化升级成当前 working copy 冲突。
MNote 不需要照搬 VSCode 的实现,但应采用同一条合同:正文冲突只属于当前 Markdown 文件,不属于同 root 下任意附件、文件树资源或元数据文件。
## 当前根因
当前 `mnote-web` 的 document event channel 以 `rootUri` 共享:
- 前端订阅 `/api/local-folder/events?rootUri=...`,没有传 `documentId`
- 后端 `build_document_events_stream` 在没有 `documentId` 时不会按 Markdown 相对路径过滤。
- watcher 对非 Markdown 资源发出的 payload 中 `documentId` 为空。
- 前端收到空 `documentId` 后,仍会对同 root 下所有 document session 设置 `externalChangePending` 并调度正文刷新。
这会把附件上传、文件树拖入、资源创建等 root 级变化误投递到正文 session,形成文件冲突误报和跨页面状态污染。
另一个独立根因是冲突 UI 的 DOM 生命周期:
- `renderSessionConflictSurface` 会把冲突面板直接插入 `.document-pane`
- 页面切换时 `unmountEditorViewBinding` 只卸载 editor runtime 和事件监听,没有清理旧 session 的冲突面板。
- 因此旧页面已出现冲突时,新建/切换到新页面可能短暂看到旧冲突面板;刷新后整页 DOM 重建,问题消失。
## 合同
1. Markdown 正文 session 只订阅自己的 Markdown 文件事件。
2. 非 Markdown 资源事件只用于文件树/资源投影刷新,不触发正文 `externalChangePending`
3. 真实正文冲突只在“同一个 Markdown documentId + 当前 session dirty/saving/saveTimer/recent input”时出现。
4. 同 root 下不同 Markdown 页面必须有独立 document event channel。
5. 文件树 live stream 继续承担 root 级变化刷新,不依赖正文冲突链路。
## 已执行修复切片
- 前端 document event channel key 从 `rootUri` 收窄为 `rootUri#documentId`
- 前端订阅本地文件夹 document events 时携带 `documentId`
- 前端收到无 `documentId` 的 change payload 时直接忽略,不再设置正文 `externalChangePending`
- editor view unmount 时清理旧 session 的冲突面板,避免手动插入的 DOM 跨页面残留。
- 后端补充 document event filter 测试,确保资源文件路径不会匹配 Markdown 正文路径。
## 后续系统性收口
-`DocumentBufferStore` 作为正文冲突唯一状态底座,前端 session 只展示 buffer state,不自行拼第二套冲突事实。
- 为资源 tab 增加独立 resource watch 合同:按 `resourcePath` 监听资源文件,而不是复用 Markdown document event。
- 为新建页面增加创建者写入抑制或 bootstrap generation,避免未来新建空文件 watcher 与首次打开时序竞争。
-`task451` 的真实冲突 smoke 与上传/新建页面无冲突 smoke 合并成一组 local Markdown conflict regression。
## 验收
- 上传第一个/第二个附件后,主编辑区不显示 `mnote-editor-conflict-panel`
- 新建页面后,旧页面冲突 UI 不带入新页面。
- 新建页面后直接向文件树拖入文件,不显示文件冲突。
- dirty 当前 Markdown 后由外部修改同一个 `.md` 文件,仍显示冲突并保留 accept disk / keep current / diff 流程。