news 2026/10/15 3:47:12

MooTool Compose 客户端 JSON Vault 空闲自动保存失败反馈补全:`jsonVaultIdleAutosaveAttempt` 设计与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MooTool Compose 客户端 JSON Vault 空闲自动保存失败反馈补全:`jsonVaultIdleAutosaveAttempt` 设计与实践
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

本篇技术指南围绕 MooTool Compose(Kotlin/Compose Desktop)客户端 JSON 工具的DIFF-370 变更展开,讲解 JSON Vault 250ms 空闲自动保存在失败时如何从"静默失败"改为写入状态栏notice的完整实现链路。读者将掌握:该变更的动机(与随手记、Vault 保存反馈的一致性对齐)、jsonVaultIdleAutosaveAttempt辅助函数的源码级实现、JsonScreen中LaunchedEffect空闲保存协程的接线方式、底层冲突判定逻辑(VaultConflictEngine.canOverwrite),以及对应的桌面测试用例与离线验收命令。

背景:空闲自动保存的"静默失败"问题

MooTool 的 Compose Desktop 客户端(位于仓库next-compose/目录,独立 Gradle Wrapper 工程)中,JSON 工具(内部编号F04)与随手记(F01)都实现了"编辑停顿 250ms 后自动落盘"的能力,对齐 Electron 版本JsonVaultPanel.persistSelectedOnIdle/QuickNoteTool.persistSnapshotOnIdle的行为。

在 DIFF-370 之前,两条空闲自动保存链路存在一处明显不对称:

  • 随手记:250ms idle 自动保存失败时,quickNoteSaveCurrent会把失败原因写入session.error(见 DIFF-199:随手记 250ms 空闲自动保存),状态栏可感知失败;
  • JSON:JsonScreen中的同类型LaunchedEffect虽然同样调用saveJsonVault(..., showToast = false),但失败时完全静默——既没有 toast,也不写notice。

这与 DIFF-363:Vault prepare / saveIfNeeded 失败统一状态栏反馈 确立的"保存失败必须反映到状态栏"原则不一致。DIFF-363 已经让prepareJsonVaultContext在saveJsonVault失败时写入session.notice,而空闲自动保存这个高频后台路径还遗留了静默缺口。

从源码结构看,该缺陷的影响面主要在两类场景:一是编辑内容与磁盘版本产生外部冲突时自动保存被VaultConflictEngine拦截;二是写盘 IO 异常(如文件被占用、权限不足)。无论哪种,用户都可能以为内容已保存,实则失败被无声吞掉。

行为变更:新增jsonVaultIdleAutosaveAttempt辅助函数

DIFF-370 的核心变更非常聚焦:在 JSON Vault 会话模块中新增内部辅助函数jsonVaultIdleAutosaveAttempt,行为契约如下:

  • 成功:返回true;
  • 失败:向session.notice写入失败文案,并返回false。

实现位于 JsonVaultSession.kt:

/** 250ms 空闲自动保存;失败写入 `session.notice`(对齐随手记 idle `error` 与 DIFF-363)。 */ internal fun jsonVaultIdleAutosaveAttempt( container: AppContainer, session: JsonSession, monitor: VaultRevisionMonitor?, onConflict: (VaultConflictState) -> Unit, ): Boolean = saveJsonVault(container, session, monitor, showToast = false, onConflict = onConflict) .onFailure { session.notice = it.message ?: container.t("json.notice.failed") } .isSuccess

可以对照随手记侧新增的对称实现quickNoteIdleAutosaveAttempt(位于 QuickNoteVaultContext.kt),两条路径现在保持了行为契约上的对齐——差异仅在于 JSON 侧写入session.notice、随手记侧写入session.error。

几个值得注意的实现细节:

  1. showToast = false:空闲自动保存是高频静默动作,成功时不应弹出 toast 打断用户,只在失败时通过状态栏notice反馈;
  2. onFailure文案优先级:优先取异常自身的message(更具体),为空时才回退到 i18n 键json.notice.failed(简体中文文案为"处理失败",见 Translator.kt);
  3. 返回布尔值:调用方可以根据返回值决定是否需要额外刷新 UI。

接线:JsonScreen空闲保存协程的改动

JsonScreen中原有的空闲保存LaunchedEffect改用上述辅助函数。改动后的完整协程逻辑位于 JsonScreen.kt:

LaunchedEffect(session.editor.revision, session.currentFile, session.vaultConflict) { if (session.vaultConflict != null) return@LaunchedEffect val file = session.currentFile if (file.isBlank()) return@LaunchedEffect if (session.editor.text == session.savedText) return@LaunchedEffect delay(250) if (file != session.currentFile) return@LaunchedEffect if (session.editor.text == session.savedText) return@LaunchedEffect jsonVaultIdleAutosaveAttempt(container, session, monitor) { session.vaultConflict = it } refresh() }

这个协程的守卫条件与行为可以拆解为五层:

阶段守卫 / 动作作用
入口session.vaultConflict != null直接返回冲突对话框打开期间不自动保存
前置检查currentFile为空则返回无打开文件(Vault 未选中)时不触发
前置检查editor.text == savedText则返回编辑器干净(无脏数据)时不触发
防抖delay(250)等待 250ms 编辑停顿,天然防抖
二次校验文件已切换 / 已变干净则返回防抖期间状态变化则放弃本次保存
执行调用jsonVaultIdleAutosaveAttempt成功或失败均写session.notice/ 触发onConflict

与 DIFF-370 同时落地的关键差异点在于协程末尾的refresh():无论jsonVaultIdleAutosaveAttempt返回成功还是失败,都会无条件调用refresh()触发 UI 重绘(sessionManager.bump()+persistJson()),因此失败写入的session.notice能立即在状态栏呈现。这一"始终刷新"的设计后来被随手记侧借鉴,见 DIFF-373:随手记空闲自动保存失败刷新 UI(随手记失败路径原本只bump不刷新,DIFF-373 补上了该缺口)。

顺带一提,JSON 侧该机制最早成型于 DIFF-197:JSON Vault 空闲自动保存与树选中,当时的约束就是"冲突态不自动保存、saveJsonVault(..., showToast = false)",DIFF-370 是在此基础上补齐失败反馈。

底层原理:saveJsonVault与冲突判定

理解jsonVaultIdleAutosaveAttempt的失败语义,需要先看它包装的saveJsonVault(同样位于 JsonVaultSession.kt)。其核心流程为:

internal fun saveJsonVault( container: AppContainer, session: JsonSession, monitor: VaultRevisionMonitor?, showToast: Boolean = true, onConflict: (VaultConflictState) -> Unit, ): Result<Unit> { val name = session.currentFile if (name.isBlank()) { return Result.failure(IllegalStateException(container.t("json.vault.saveNoSelection"))) } val disk = runCatching { container.jsonVault.readOrNull(name) }.getOrNull() if (!VaultConflictEngine.canOverwrite(session.savedText, disk, session.editor.text)) { onConflict(VaultConflictState(name, session.editor.text, disk, disk == null)) return Result.failure(IllegalStateException(container.t("vault.conflict.blocked"))) } return runCatching { container.jsonVault.write(name, session.editor.text) monitor.noteOwnWriteJsonVaultFile(container.jsonVault, name) session.currentFile = name session.savedText = session.editor.text container.recordVaultActivity(VaultGitCheckpointMessages.UPDATE_JSON_SNIPPET, json = true) if (showToast) container.toastSuccess(container.t("json.vault.saved")) } }

三个Result.failure出口对应三类失败原因:

  1. 未选择文件:currentFile为空 → 文案json.vault.saveNoSelection;
  2. 外部冲突:VaultConflictEngine.canOverwrite判定不允许覆盖 → 同时回调onConflict(...)打开冲突状态、文案vault.conflict.blocked(简体中文:"磁盘文件已从外部更改,已阻止覆盖。",见 Translator.kt);
  3. 写盘异常:jsonVault.write抛出的 IO 异常,由runCatching捕获。

其中冲突判定逻辑来自 VaultConflictEngine.kt:

fun canOverwrite(savedText: String, diskText: String?, editorText: String): Boolean { if (diskText == null) return false return diskText == savedText || diskText == editorText }

即:仅当磁盘文件存在,且磁盘内容等于"上次保存的快照"(说明外部未改动)或等于"当前编辑器内容"(说明自身刚写盘)时才允许覆盖。任何第三方外部改写(磁盘内容与 savedText、editorText 都不同)都会拦截自动保存——这正是空闲自动保存最容易触发的失败场景。

值得注意的是,saveJsonVault失败路径不会把失败信息写进session.notice,只是返回Result.failure。这正是 DIFF-370 引入jsonVaultIdleAutosaveAttempt的意义:由该辅助函数统一承担"失败 →session.notice"的转换,而saveJsonVault本身保持通用性(手动保存路径由 jsonVaultSaveFromUserAction 自行处理notice与成功 toast)。

测试验证:JsonVaultIdleAutosaveTest

DIFF-370 配套的桌面测试位于 JsonVaultIdleAutosaveTest.kt,共两个用例,分别覆盖"冲突 notice"与"成功落盘"两条路径:

用例 1:idleAutosaveConflictWritesNotice——验证冲突时写入 notice 并返回 false

@Test fun idleAutosaveConflictWritesNotice() { // 构造 AppContainer、JsonSession,Vault 中创建 a.json container.jsonVault.createFile("a.json", """{"saved":true}""") val session = JsonSession().apply { currentFile = "a.json" savedText = """{"saved":true}""" editor.setText("""{"dirty":true}""", recordUndo = false) } container.jsonVault.write("a.json", """{"disk":true}""") // 磁盘被外部改写 assertFalse(jsonVaultIdleAutosaveAttempt(container, session, monitor = null) { }) assertTrue(session.notice.isNotBlank()) }

测试构造了三方数据不一致的状态:savedText是{"saved":true}、编辑器文本是{"dirty":true}、磁盘内容被外部改写为{"disk":true}。此时canOverwrite判定三向都不匹配 → 返回false,并断言session.notice非空。测试全程传入monitor = null,说明冲突判定不依赖 Git 修订监控。

用例 2:idleAutosaveSucceedsWhenCleanConflictCheckPasses——验证无冲突时成功落盘并返回 true

@Test fun idleAutosaveSucceedsWhenCleanConflictCheckPasses() { container.jsonVault.createFile("a.json", "{}") val session = JsonSession().apply { currentFile = "a.json" savedText = "{}" editor.setText("""{"a":1}""", recordUndo = false) } assertTrue(jsonVaultIdleAutosaveAttempt(container, session, monitor = null) { }) assertEquals("""{"a":1}""", session.savedText) }

该用例覆盖正常的 250ms 防抖落盘:磁盘内容等于savedText(外部未改动),编辑器有脏内容{"a":1}→ 允许覆盖、写盘成功,返回true,且session.savedText更新为磁盘内容。同时验证了冲突回调onConflict在成功路径不会被触发。

验收方式与运行限制

按原文档约定,DIFF-370 的验收对象是F04 JSON Vault 自动保存反馈,执行方式为 Compose 工程的离线桌面测试:

cd next-compose ./gradlew :composeApp:desktopTest --offline

--offline保证测试不依赖网络拉取依赖,是仓库next-compose/docs/acceptance.md中反复使用的标准验收形式。从仓库验收记录看,本机使用 JDK 21 运行该工程桌面测试的规模在 1340+ 用例量级,DIFF-370 的两个新增用例随JsonVaultIdleAutosaveTest纳入这一测试集。

需要说明的前提:Compose Desktop 测试针对 JVM 桌面运行时,其行为验证以单元测试形式断言(状态栏 UI 的最终呈现依赖session.notice被JsonScreen状态栏读取,见 JsonScreen.kt 的MooStatusMeta(session.notice)渲染分支),手工 UI 验收不在本次变更范围内。

小结:一条贯穿"随手记 ↔ JSON ↔ Vault"的反馈一致性链路

DIFF-370 表面上只是新增一个 8 行的辅助函数并改一处调用点,但它在 MooTool Compose 客户端的保存反馈体系里补齐了关键一环:

  • 横向:JSON 空闲自动保存的失败反馈对齐了随手记(DIFF-199 的session.error与 DIFF-373 的失败刷新);
  • 纵向:空闲自动保存路径对齐了 Vault 其他保存路径(DIFF-363 确立的prepareJsonVaultContext/saveIfNeeded失败写状态栏原则);
  • 上游参照:行为基准始终对齐 Electron 版JsonVaultPanel.persistSelectedOnIdle的静默自动保存语义(成功不打扰、失败必须有信号)。

这一变更使"外部冲突被拦截""写盘失败"等高频场景不再无声发生,为用户提供了可感知、可追溯的保存状态。对于需要为 Vault 类编辑器实现空闲自动保存的开发者,jsonVaultIdleAutosaveAttempt的"辅助函数封装showToast=false的保存 + 失败落notice+ 返回布尔值 + 调用方无条件刷新"组合,是一套可复用的成熟模式。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载
上一篇:DeepSeek Harness 工具输出 spill 机制:超大工具结果的有界预览与可检索落盘策略
下一篇:Perso-Arabic 脚本归一化与乌尔都语 NMT 训练实战:深入解析 NMTMediumV1 OpenNMT-tf Recipe

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 3:47:12

如何做到 Godot 场景切换不卡顿:Godot 场景管理实战指南

如何做到 Godot 场景切换不卡顿&#xff1a;Godot 场景管理实战指南 【免费下载链接】godot-docs Godot Engine official documentation 项目地址: https://gitcode.com/GitHub_Trending/go/godot-docs 点"开始游戏"卡住 1 秒、每过一关内存往上爬、切回主界面…

作者头像 李华
网站建设 2026/10/15 3:46:24

一文读懂ResNet:残差块、网络搭建与训练优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 3:44:06

为什么外贸企业需要AIGEO优化?AI搜索时代下海外获客的新机会

为什么外贸企业需要AIGEO优化&#xff1f;AI搜索时代下海外获客的新机会核心摘要海外采购者的搜索行为正在发生根本性变化&#xff1a;越来越多人通过ChatGPT、Perplexity、Copilot等AI工具获取供应商信息&#xff0c;而非仅仅依赖传统搜索结果页。AIGEO&#xff08;AI生成引擎…

作者头像 李华
网站建设 2026/10/15 3:44:05

庆祝冬余数据联合广东省商业地产租赁协会举办“AI 赋能商业企业创新实践案例”活动取得圆满成功!

庆祝冬余数据联合广东省商业地产租赁协会举办“AI 赋能商业企业创新实践案例”活动取得圆满成功&#xff01;近日&#xff0c;由冬余数据携手广东省商业地产租赁协会及数智经济专委会共同主办的“AI赋能商业企业创新实践案例”主题活动&#xff0c;在各方与会嘉宾的热烈交流中圆…

作者头像 李华
网站建设 2026/10/15 3:43:36

C/C++避坑指南:内存管理、指针与多线程实战经验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 3:42:43

基于 Spring Boot 的公考线上组卷系统的设计与实现

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 1. 项目背景与意义 随着公务员考试报考人数的持续增长&#xff0c;传统线下组卷方式存在出题效率低、试卷复用难、知识点覆盖不均衡等问题。教师或培训机构需要花费大量…

作者头像 李华