桌面应用开发工具【免费下载链接】MooToolHandy tool set for developers项目地址https://gitcode.com/gh_mirrors/mo/MooTool点击查看免费下载本文以next-compose仓库中的 DIFF-248 规格248-vault-conflict-session.md为核心讲解 JSON 工具与随手记在遇到 Vault 外部文件冲突时为什么 Compose 的remember状态不够用、冲突状态如何迁移到JsonSession/QuickNoteSession中持久持有以及「重新加载 / 另存副本 / 保留编辑」三个处理动作在源码里的完整实现。读完本文你将掌握这套冲突会话态的设计动机、状态生命周期边界、对话框交互规则与对应的验证手段。背景Vault 外部冲突是怎么被判定出来的DIFF-248 讨论的对象是「Vault 外部冲突对话框」要理解会话态改造的价值先要看清冲突判定本身。在 next-compose 中JSON 片段库jsonVault与随手记笔记库NoteVault都支持把文件直接放在本地目录里Git 快照、MCP、外部编辑器都可能改动这些文件因此必须处理「用户正在编辑时磁盘上的文件被别人改了」的场景。冲突判定引擎 VaultConflictEngine判定逻辑集中在 VaultConflictEngine.kt 中这是一个纯 Kotlin 对象不依赖 Compose便于在无窗口单测中复现其设计背景见 DIFF-026 规格VaultChangeKind枚举定义了外部变化的五种分类Ignored忽略、Reload静默重载、Conflict冲突、Deleted文件被外部删除、TreeChanged仅树变化VaultConflictState是冲突发生时携带的快照数据relativePath相对 Vault 路径、editorText编辑器当前内容、diskText磁盘最新内容删除时为null、deleted是否外部删除判定以 UTF-8 内容的 SHA-256 为同一性依据sha256Text而不是 mtime因为 mtime 无法唯一判断内容是否相同MAX_HASH_BYTES 8 * 1024 * 1024超过 8 MiB 的文件只比较 size 指纹避免对超大文件做全量哈希。核心的decide方法VaultConflictEngine.kt L41-L61决策顺序如下变化路径不是当前打开的文件 →TreeChanged只刷树不碰编辑器磁盘文件已不存在 → 编辑器内容与上次保存一致则Deleted否则Conflict本地有未保存编辑但文件被删了磁盘哈希等于expectedOwnHash本次是自己写盘产生的回显→Ignored磁盘内容 编辑器内容 →Ignored编辑器内容 上次保存内容本地没改→Reload直接用磁盘版本静默重载不打扰用户磁盘内容 上次保存内容外部没改→Ignored以上都不满足本地脏且磁盘也偏离了保存基线→Conflict。保存方向另有护栏canOverwriteL63-L66要求磁盘内容必须等于上次保存基线或等于编辑器内容否则拒绝写盘——即「保存前再次读盘外部版本与基准和编辑器都不同则拒绝覆盖」。副本命名规则在conflictCopyNameL68-L77中实现把原文件名改成*.local-{epochMs}.*例如a/b.json→a/b.local-1757900000000.json。路径过滤由shouldIgnore负责L30-L39忽略点文件、.tmp、.git/随手记库额外忽略attachments/。冲突进入会话的两个入口以 JSON 工具为例外部变化最终汇聚到 JsonVaultSession.kt 的两个函数jsonVaultApplyExternalChangeL70-L111磁盘监视回调时调用。编辑器干净时走jsonVaultReloadOpenFileIfClean直接重载脏时调用VaultConflictEngine.decide返回Conflict就通过onConflict回调产出一个VaultConflictStatesaveJsonVaultL303-L327用户保存时先readOrNull再canOverwrite不通过则同样回调onConflict并返回失败杜绝后写覆盖。随手记侧的quickNoteApplyExternalChangeQuickNoteVaultContext.kt L244-L287逻辑对称只是比较前会用NoteFrontmatter.parse剥离 frontmatter、只比较正文。监视器VaultRevisionMonitor以 400ms 间隔做内容哈希轮询VaultRevisionMonitor.kt L15-L18检测到的变化经SwingUtilities.invokeLater切到 EDT 后调用上述入口。问题remember 状态的组成生命周期陷阱DIFF-248 的规格原文非常简短它只声明了三条行为契约248-vault-conflict-session.mdJsonSession.vaultConflict、QuickNoteSession.vaultConflict持有VaultConflictState替代 Composeremember分离工具窗口时冲突对话框与状态栏横幅不随主窗占位销毁而丢失主窗切页不自动清空冲突与 Electron 会话语义一致用户须在对话框内处理或关闭。这三条背后是 Compose 桌面端一个具体的状态管理问题在改造前「是否存在待处理冲突」如果存放在JsonScreen/QuickNoteScreen组合体内部的remember中其生命周期绑定的是该 composable 的 composition。而 next-compose 的工具体系存在两类场景会摧毁或重建 composition分离工具窗口detach工具面板可以从主窗拖出成为独立窗口。此时主窗中该工具的占位组合会被销毁再合并回来时重新组合remember的值随之蒸发——用户正面对着一个未解决的磁盘冲突对话框和状态栏横幅却凭空消失composition 反复重建主题切换、布局重组等都可能触发局部重组把本应跨页面保留的业务状态当成「UI 临时状态」丢掉。而「有没有冲突」并不是 UI 临时状态它对应磁盘上真实存在的双版本分叉必须由用户显式裁决。把它放在remember里语义上就错了。DIFF-248冲突状态上移到 Session 会话对象会话字段与声明位置改造后VaultConflictState的两个宿主在 SessionManager.kt 中声明JsonSessionL154 声明var vaultConflict: VaultConflictState? null与gitDialogOpen、vaultDeleteConfirmPath、vaultContextMenuPath、vaultTreeExpanded、vaultTreeScrollOffset等字段并列同属「Vault 作用域」的状态族QuickNoteSessionL1739 声明同样的字段。JsonSession与QuickNoteSession实例由SessionManager以缓存对象形式持有生命周期与 App 会话一致远长于任何一次 composable 组合。因此当JsonScreen在主窗被拆掉、在分离窗口重建、或用户切到别的工具页再切回来时session.vaultConflict始终保留原值VaultConflictDialog会依据它再次渲染出来——这正是规格中「分离窗口不丢失」「主窗切页不自动清空」两条契约的实现基础。清空边界什么时候才允许清掉冲突规格第三条「用户须在对话框内处理或关闭」意味着清空只允许发生在两类场合源码里对应两个明确的位置用户在对话框内完成处理三个动作的收尾都会把session.vaultConflict null见后文「三个处理动作」一节会话整体被外部重置两个 Session 都提供了resetVaultScopedOverlays()JsonSession在 SessionManager.kt L159-L170QuickNoteSession在 L1757-L1765一次性清掉 Git 对话框、冲突状态、删除确认、右键菜单、路径选择器等全部 Vault 作用域弹层。它的调用方是reloadJsonSessionInPlace/reloadQuickNoteSessionInPlaceL2511-L2525即备份恢复或 SQLite 会话库被外部替换后、在既有会话实例上从tool_sessions重新装载的路径——此时整个工作区已被换掉旧冲突自然作废。Vault 根目录变更的场景则由 VaultRootChange.kt 中的dismissVaultScopedOverlays()扩展函数走同一清理逻辑。需要注意的细节是vaultConflict故意没有纳入snapshot()/restore()的持久化快照。JsonSessionSnapshot、QuickNoteSessionSnapshot只保存编辑内容、查找替换、树展开选择等常规字段冲突状态是「运行期待裁决」状态不落库。这与会话重装载时先resetVaultScopedOverlays()再restore(snapshot)的顺序相配合避免把一个基于旧磁盘版本的冲突快照恢复到新的数据环境里。冲突对话框显隐规则与三个处理动作对话框 UI 与显示规则会话态通过 VaultConflictOverlay.kt 的两个入口接入两个工具JsonVaultConflictOverlayL12-L43与QuickNoteVaultConflictOverlayL45-L71都是session.vaultConflict?.let { ... }的声明式渲染会话字段非空即显示为 null 即消失不再有独立于会话的本地开关。对话框本体 VaultConflictDialog.kt 为 640x420 的遮罩弹层从上到下依次是标题、冲突文件相对路径warning 色、说明行、unified diff 预览区等宽字体可滚动、底部按钮行。显隐与权重规则统一收敛在 VaultConflictPresentation.kt外部删除deleted true时不显示「重新加载」按钮showReloadAction返回 false主操作是「另存副本」saveCopyProminent为 truediff 预览区不显示只展示删除提示文案hintDeleted外部改写时主操作是「重新加载」reloadProminent为 true预览区展示DiffEngine.compare(editorText, diskText)生成的 unified diff截断到 4000 字符buildUnifiedPreview的maxChars参数diff 为空时显示「无差异」占位文案「保留编辑」按钮恒在并且整个遮罩的onDismiss也映射到onKeep即点击空白处等同于保留编辑。三个动作的会话副作用动作实现集中在 VaultConflictActions.ktJSON 与随手记各一套模式一致都遵循「先落盘/读盘 → 更新会话基线 → 清vaultConflict→ 写 notice/error」的顺序重新加载ReloadapplyJsonVaultConflictReloadL15-L33。非删除场景下在 EDT 上用container.jsonVault.read(relativePath)把磁盘内容写回编辑器recordUndo false然后同步savedText让编辑器进入「干净」状态最后清冲突、提示「已重载」。删除场景下则把currentFile清空、把当前编辑器内容提升为新的savedText基线避免用户编辑内容在基线上变成「脏」。另存副本Save CopyapplyJsonVaultConflictSaveCopyL35-L62。用VaultConflictEngine.conflictCopyName生成*.local-{epoch}.*副本名并写入编辑内容成功后调用monitor?.rebaselineAfterLocalCrud()让监视器把自己这次写盘当作「自写」忽略避免副本文件触发下一轮监视误报删除场景下副本直接成为新打开的文件改写场景下原路径恢复为磁盘版本。失败时只写session.notice不清冲突状态——用户还可以重试。注意nowMillis是显式参数默认System.currentTimeMillis()这让单测可以注入确定性时间戳断言副本文件名。保留编辑KeepapplyJsonVaultConflictKeep/applyQuickNoteVaultConflictKeepL64-L66、L111-L113只清session.vaultConflict不触碰编辑器与磁盘。此后若用户直接 CtrlSsaveJsonVault会再次canOverwrite失败并重新弹出对话框——「保留」只是暂时关闭提示不解除保存护栏。会话态如何改变屏幕行为autosave 守卫与状态栏横幅冲突从remember上移到会话字段后JsonScreen的若干组合逻辑改为直接读取session.vaultConflictJsonScreen.kt250ms 空闲自动保存守卫L228-L238LaunchedEffect的 key 里包含session.vaultConflict且首行if (session.vaultConflict ! null) returnLaunchedEffect——存在未裁决冲突时禁止空闲自动保存把本地脏内容压到分叉的磁盘上同时该 key 的参与保证冲突状态一变出现或清除自动保存逻辑立即重新求值状态栏横幅L466session.vaultConflict ! null时在状态栏追加vault.conflict.banner文案warning 色。横幅与对话框读同一个会话字段所以对话框即使因组合抖动未渲染横幅也明确告诉用户「有未处理冲突」两者不会互相矛盾冲突回调的注入方式JsonScreen中所有可能产出冲突的调用点监视器回调、保存、Git flush、打开文件、导入、自动保存等都统一传{ session.vaultConflict it }作为onConflict与前述 JsonVaultConflictSessionTest.kt 中直接对jsonVaultApplyExternalChange/saveJsonVault的断言形成「入口产出 → 会话持有 → UI 渲染」的完整链条。验证会话级单测与产品证据DIFF-248 规定的验证命令是./gradlew :composeApp:desktopTest --offline。围绕该改造仓库中的关键测试资产包括JsonVaultConflictSessionTest.kt在临时目录里构造open.json先用SwingUtilities.invokeAndWait把编辑器改成脏内容再用container.jsonVault.write模拟外部改写断言jsonVaultApplyExternalChange回调出的VaultConflictState字段完整relativePath、editorText、diskText、deleted以及saveJsonVault在磁盘偏离基线时保存失败且回调冲突QuickNoteVaultConflictSessionTest.kt随手记的对称路径覆盖 frontmatter/正文变更触发ConflictJsonVaultMonitorConflictTest.kt、QuickNoteVaultMonitorConflictTest.kt验证真实VaultRevisionMonitor轮询路径也能经invokeLater走到同一回调wiring 层证据见 DIFF-439保存副本/保留编辑的具体流转由 JsonVaultConflictSaveCopyFlowTest.kt、JsonVaultConflictReloadKeepFlowTest.kt 等覆盖产品窗口级的手工步骤记录在 docs/evidence/2026-09-17-vault-conflict-product-window/results.md。从源码结构看这套测试之所以可行正是因为判定引擎VaultConflictEngine、会话JsonSession与动作VaultConflictActions都不绑定 Compose 组合环境仅编辑器文本设置需要切到 EDT——这正是 DIFF-248 把状态上移为普通会话字段带来的附带收益冲突链路可以在无窗口单测里被逐段钉死。小结DIFF-248 的改动量很小两个会话字段 一处 overlay 渲染条件但它修正的是桌面 Compose 应用里一类常见的状态归属错误把「必须跨组合存活、且对应真实磁盘事实」的业务状态放在remember里。改造后的语义边界清晰——冲突状态随会话生、随用户裁决灭、随整个工作区重置而失效对话框、状态栏横幅、自动保存守卫三者读同一个session.vaultConflict字段主窗切页、窗口分离都不会丢失未裁决的冲突同时saveJsonVault的canOverwrite护栏保证即使用户点了「保留编辑」磁盘分叉也不会被静默覆盖。这一设计与 Electron 版本的会话语义对齐参见 DIFF-026 中「与 Electron 比dirty 时不再静默忽略外部更改保存会被拦住直到用户选择」的行为差异说明是 next-compose 逐条对齐并改进旧版行为的典型样例。赞分享桌面应用开发工具【免费下载链接】MooToolHandy tool set for developers项目地址https://gitcode.com/gh_mirrors/mo/MooTool点击查看免费下载相关推荐MooTool Next-Compose 中的 Vault 树滚动位置会话态DIFF-251 设计与实现解析MooTool Next Compose 中的 Vault 树滚动位置会话态DIFF 251 设计与实现解析 导读 本篇文章围绕 MooTool 下一代桌面实桌面应用开发工具Learn Harness Engineering 第三讲让代码仓库成为 AI Agent 的唯一事实来源System of RecordLearn Harness Engineering 第三讲让代码仓库成为 AI Agent 的唯一事实来源System of Record 导读 本讲解桌面应用开发工具MooTool next-composeVault Git 面板的会话级持久化设计与实现DIFF-246MooTool next composeVault Git 面板的会话级持久化设计与实现DIFF 246 本文基于 MooTool 的 next comp桌面应用开发工具上一篇炉石传说插件 HsMod 值得一试从 8 倍速开包到酒馆开图的完整上手手册下一篇京东秒杀抢不到用JDspyder开源脚本3步实现毫秒级自动预约与抢购创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考