# 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 流程。