- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
本篇技术指南围绕 MooTool 多端重写项目next-compose中编号为 DIFF-559 的开发切片展开,详细拆解该切片在 JSON Vault 搜索、Host 方案侧栏搜索、工作台导航分组标签、F12/F19/F23 工具引擎按钮守卫、host/json相关 CSS 组件批次、F20 翻译离线边界单测、Vault Git merge hint 焦点帧捕获以及 P7 打包 offline gate 上的全部落地内容。读完本文,你将理解 MooTool 的 Compose 桌面端如何通过"可单测的 Presentation 纯函数 + 屏幕接线"模式逐批收敛与 Electron 版本的 parity 差距,并能直接复用同一套接线模式与本地 HttpServer 离线测试方法。
切片背景与边界:DIFF-559 在 parity 迁移中的定位
DIFF-559 是next-compose系列切片中的一环,其前序切片 DIFF-558 已完成设置关于/更新 UI、Vault 外部冲突 UI、MCPprotobuf_wire接线与 Compose 帧152。按照切片文档的明确约定,本条不重复558 中关于/更新、Vault 冲突 savecopy、MCP wire 链的内容,只聚焦本切片自有的六个行为面。
同时,该切片揭示了整体 parity 迁移的剩余缺口(parity-gap)状态:
- 六套 CSS 逐选择器皮肤尚未完成;
- 产品主窗 Tab/IME/TCC PNG 证据尚未补齐;
- P7 三平台安装尚未验收;
- 其余 substantial 引擎/UI 缺口仍在清单中,目标未达成。
这意味着本切片只是"逐批收敛"过程中的一个里程碑,其验收口径以离线可跑的单元/桌面测试(desktopTest)+ 证据帧为准,三平台安装/公证不在本切片范围内(详见后文 P7 offline gate 章节)。
JSON Vault 搜索的 Presenter 化:query 规范化与三路匹配
本切片的第一项核心行为是JsonVaultSearchPresentation,它位于next-compose/composeApp/src/commonMain/kotlin/com/rememberber/mootool/next/compose/domain/JsonVaultSearchPresentation.kt。该对象将 JSON Vault 搜索的 query 处理收敛为四个可单测的纯函数:
| 函数 | 语义 |
|---|---|
normalizeQuery(raw) | 对用户输入做trim().lowercase(),得到统一的小写 needle |
isFiltering(raw) | normalizeQuery(raw)非空即判定为"正在过滤",用于驱动空态文案切换 |
pathSegmentMatches(relativePath, name, needle) | needle 为空直接放行;否则分别对相对路径与文件名做小写contains匹配 |
titleMatches(title, needle) | needle 非空且标题小写包含 needle |
contentMatches(content, needle, includeContent) | 仅在开启"包含内容"时,对内容做小写包含匹配 |
VaultSearchIndex(next-compose/composeApp/src/desktopMain/kotlin/com/rememberber/mootool/next/compose/domain/VaultSearchIndex.kt)复用了这套规范化逻辑,其filter(records, query, includeContent)的树形过滤策略很有参考价值:
- 目录命中:当某条目录记录满足
pathMatches时,保留该目录本身以及以其relativePath为前缀的全部后代(startsWith("${relativePath}/")),实现"目录命中即整棵子树可见"; - 文件命中:当文件记录满足
pathMatches/titleMatches/contentMatches(后两者受includeContent约束)时,保留该文件并沿VaultMove.parentDirectory逐级向上保留所有父目录,保证选中文件在树中总有可展示的路径骨架; - needle 为空时直接返回全量记录,不产生过滤开销。
在 UI 侧,JsonScreen.kt(next-compose/composeApp/src/desktopMain/kotlin/com/rememberber/mootool/next/compose/features/json/JsonScreen.kt)的VaultPane中通过VaultSearchIndex.filter(snapshot, session.vaultQuery, session.includeContent)(第 277 行)驱动列表,搜索框使用mooJsonVaultSearch()修饰符(第 931 行)。对这套逻辑的验证由JsonVaultSearchPresentationTest承担,确保不同 query 形态下三路匹配与树形保留行为符合预期。
Host 方案侧栏接线:内容搜索守卫与无匹配文案
第二个行为面是HostWiringPresentation(next-compose/composeApp/src/desktopMain/kotlin/com/rememberber/mootool/next/compose/domain/HostWiringPresentation.kt)。该切片在其中新增了两个与搜索 UI 直接相关的函数:
canToggleContentSearch(applying):!applying——应用/备份恢复进行中时禁止切换"搜内容"开关,避免在 busy 状态下改动过滤条件;showFilteredEmpty(profileCount, query):profileCount == 0 && JsonVaultSearchPresentation.isFiltering(query)——复用 JSON Vault 的isFiltering判定,区分"本来为空"与"过滤后无匹配"两种空态。
在HostScreen.kt(next-compose/composeApp/src/desktopMain/kotlin/com/rememberber/mootool/next/compose/features/host/HostScreen.kt)的方案侧栏中:
- 侧栏整体使用
mooHostProfilesPane()修饰符(第 748 行); - "搜内容"开关的点击目标用
HostWiringPresentation.canToggleContentSearch(session.applying)作为enabled守卫(第 760 行),并实时切换session.includeContent; - 列表为空时通过
HostWiringPresentation.showFilteredEmpty(profiles.size, session.query)决定文案:过滤中显示json.notice.noMatches(复用 JSON 工具的无匹配文案),否则显示host.empty(第 775-782 行)。
值得注意的是,HostWiringPresentation是一个相当完整的"动作守卫 + 引擎路径"合集:除上述两个函数外,还包括canConfirmApply、canOpenApplyConfirm、canRestoreBackup、saveProfileActionEnabled(对齐工具栏disabled={!dirty})、exportProfileActionEnabled、copyProfileActionEnabled、deleteProfileActionEnabled、findQueryActionEnabled(对齐EditorFindBarPresentation)、renameProfileActionEnabled(对齐重命名对话框disabled={!renameValue.trim()})、copySystemHostsActionEnabled(读到系统 hosts 路径即可复制,含空文件),以及runReadSystem/runBuildApplyPreview/runApply/runRestoreBackup/runReadImportProfile/runWriteExportProfile六个runCatching包裹的引擎调用路径,错误统一折叠为Success/Failure密封结果以便 UI 层 toast。
工作台导航分组标签:LayoutPolicy 集中决策
第三个行为面是WorkbenchNavPresentation.showNavigationGroupLabel。它本身只做一层薄封装,把决策委托给LayoutPolicy.showNavigationGroupLabel(next-compose/composeApp/src/desktopMain/kotlin/com/rememberber/mootool/next/compose/ui/workbench/LayoutPolicy.kt第 55-59 行):
fun showNavigationGroupLabel(navigationStyle: String, showSeparators: Boolean, customGroup: Boolean): Boolean { if (!showSeparators) return false if (customGroup) return true return navigationStyle != "classic" }决策规则可归纳为三条:
- 全局关闭分隔线(
showSeparators == false)时一律不显示分组标题; - 自定义分组(
customGroup == true)即使开启也始终保留标题——对应 Electron.app-shell--nav-classic .tool-group h2 { display: none }但自定义分组保留标题的语义; - 默认分组仅在导航样式非
classic时显示标题(classic 模式隐藏 h2)。
该函数在Sidebar.kt(next-compose/composeApp/src/desktopMain/kotlin/com/rememberber/mootool/next/compose/ui/workbench/Sidebar.kt)中被三处接线:最近工具组(customGroup = false,第 132 行)、自定义分组(customGroup = true,第 146 行)、以及默认工具分组(customGroup = false,第 159/172/182 行)。由此,导航分组标题的显隐完全由LayoutPolicy单一决策源控制,避免 UI 层散落判断。
F 工具引擎 UI 守卫:F12/F19/F23 可执行性接线
本切片还为核心引擎之外的"元数据"做了F 工具引擎 UI(非 metadata)接线,即三个工具的按钮enabled状态由可单测的 Presentation 守卫驱动:
F12 UA 解析(UaWiringPresentation,.../domain/UaWiringPresentation.kt)
canParse(source)/parseActionEnabled(source):源文本非空才允许解析;canCopyResult(result)/copyResultActionEnabled(result):结果非空才允许复制;runParse(source)将UaEngine.parse包进runCatching,返回ParseOutcome.Success/Failure;shouldToastParseFailure恒为 true。
在UaParseScreen.kt中,解析按钮enabled = UaWiringPresentation.parseActionEnabled(session.source)(第 187 行),复制按钮enabled = UaWiringPresentation.copyResultActionEnabled(...)(第 220 行)。UaRegexTimeWiringPresentationTest验证了canCopyResult("") == false、canCopyResult("{}") == true等边界。
F19 留言板(MessageBoardWiringPresentation,.../domain/MessageBoardWiringPresentation.kt)
clippedMessage(raw)委托MessageBoardEngine.clip裁剪文案;wakeErrorIfNeeded(displayAwake):显示器未唤醒时提示唤醒错误;canEnterPresentation(message, displayAwake):message.isNotBlank() && displayAwake——只有内容非空且显示器处于唤醒态才允许进入演示;runFitFontSize(availableWidth, availableHeight, sizePercent, fits)委托引擎按可用尺寸与字号百分比二分拟合。
MessageBoardScreen.kt第 330 行将其接为"演示"按钮的enabled。MessageBoardWiringPresentationTest覆盖了"空内容 + 已唤醒"和"有内容 + 未唤醒"两种拒绝路径。
F23 SVG 批量(ImageSvgWiringPresentation,.../domain/ImageSvgWiringPresentation.kt)
coerceColors(value):coerceIn(2, 64),颜色数输入边界 2~64;coerceSpeckle(value):coerceIn(0, 128),斑点数输入边界 0~128;parseColorsField/parseSpeckleField:解析字符串字段,失败时回退默认值再 clamp;canStartSvgBatch(selectedCount, busy):selectedCount > 0 && !busy——至少选中一张图且当前不忙才允许启动批量;runWriteSvgFile用Files.writeString(..., StandardCharsets.UTF_8)落盘。
ImageScreen.kt第 766 行将其接为批量生成按钮的enabled。ImageSvgWiringPresentationTest验证了(0, false)与(2, true)均拒绝、(2, false)放行的组合守卫。
CSS 组件批次:mooJsonVaultSearch 与 mooHostProfilesPane
样式(CSS 组件批次)部分在next-compose/composeApp/src/desktopMain/kotlin/com/rememberber/mootool/next/compose/ui/components/Controls.kt中新增了两个与切片主题强相关的 Modifier:
/** Electron JSON Vault 搜索行。 */ fun Modifier.mooJsonVaultSearch(): Modifier = fillMaxWidth().heightIn(min = 33.dp) /** Electron `.host-profiles` 方案列表侧栏(对齐 `.http-collection` 分隔)。 */ fun Modifier.mooHostProfilesPane(): Modifier = mooHttpCollection()mooJsonVaultSearch为 JSON Vault 搜索行设定最小高度 33dp 并占满横向宽度,对齐 Electron 侧搜索输入框的行高感知;mooHostProfilesPane直接复用mooHttpCollection()(Controls.kt第 1430 行起),使 Host 方案侧栏在分隔、列表项间距上与 HTTP 工具已落地的.http-collection组件保持一致——这正是文档中"对齐.host-profiles/.http-collection"的落地方式。
这两个 Modifier 分别被JsonScreen.kt的VaultPane(第 931 行)与HostScreen.kt的方案侧栏(第 748 行)引用,属于"组件批次"式的最小粒度样式迁移。
F20 翻译离线边界:本机 HttpServer 无公网测试
F20 翻译模块的离线 mock 是本切片中技术含量最高的一块:TranslationEngineTest(next-compose/composeApp/src/desktopTest/kotlin/com/rememberber/mootool/next/compose/domain/TranslationEngineTest.kt)用 JDK 自带com.sun.net.httpserver.HttpServer在127.0.0.1随机端口起本地服务,通过TranslationEndpoints把引擎的三个端点(Google 翻译端点、Bing/translator页面、Bing/ttranslatev3API)全部覆盖,实现完全无公网的端到端测试。
本切片文档点名的三个边界场景:
- 空文本:
emptyTextReturnsOkWithoutNetwork——输入空串时translate直接返回ok = true、text = "",不发任何网络请求; - Google 畸形 JSON:
malformedGooglePayloadFallsBackOrFailsOffline——/google返回"not-json",Bing 页面与 v3 端点返回 500,结果errorCode == NETWORK; - Bing 缺 token 页:
bingPageMissingTokenReturnsNetwork——/translator返回不含params_AbusePreventionHelper的裸 HTML,引擎无法提取 token,errorCode == NETWORK。
除这三个边界外,同一测试类还系统性覆盖了翻译引擎的其它关键路径,可作为"离线 HTTP 单测"的完整范例:
- 分块与代理项安全:
splitsLongTextWithoutLosingContentOrSurrogates验证超长文本分块后拼接还原、每块不超限、句尾空白保留,且 9 字符 + emoji + 9 字符的文本不会被从 emoji 的 UTF-16 surrogate 中间切开; - legacy 语言码映射:
convertsLegacyLanguageCodesAndNormalizesNames验证wyw→lzh、jp→ja、kor→ko、fra→fr、spa→es等旧码映射,以及cht→zh-TW(Google)与cht→zh-Hant(Bing)的差异、auto→auto-detect、中英文语言名规范化("英语"/"英語"→en); - chunk 并发与 fallback:
localServerCoversChunksFallbackCancelAndWords覆盖 1800+200 字符长文本的多块并发(maxConcurrent观测)、Google 500 时自动 fallback 到 Bing(fallbackUsed == true)、providerCooldownMs冷却期内直接跳过失败 provider、MAX_TEXT_UNITS + 1返回INVALID_REQUEST、TranslationEngine.cancel(id)返回ABORTED、1s 超时返回TIMEOUT,以及TranslationStore的单词/历史增删查; - provider 顺序:
translationProviderOrder(Google, false) == [Google, Bing]、(Google, true) == [Bing, Google],Bing 偏好开关直接翻转降级顺序; - 请求参数映射:
googleWiringSendsMappedSlTlAndClient用jp/kor输入,断言实际发送的sl=ja、tl=ko、client=gtx——证明语言码映射发生在网络请求之前; - Bing 首选零 Google 调用:
bingPreferredWiringUsesBingWithoutGoogleCall断言选择 Bing 时googleHits == 0。
该测试通过endpointsOverride参数把引擎的线上域名替换为本地地址,这正是"本机 HttpServer、无公网"离线测试得以成立的关键设计。
Vault Git merge hint 叠层:Compose 焦点帧捕获
Git merge 流程的 UI 证据由GitMergeFlowOverlayCaptureTest(next-compose/composeApp/src/desktopTest/kotlin/com/rememberber/mootool/next/compose/features/git/GitMergeFlowOverlayCaptureTest.kt)产出。它在640x160的 Compose Desktop 测试窗口内渲染:
- 状态胶囊:
MooStatusPill(zh.t("git.operationMerge"), kind = MooStatusKind.Error)+ 冲突计数文案git.counts(danger 色,11sp); - 冲突提示行:
GitMergeConflictPresentation.unresolvedHintKey(merging, conflicts, "merge")对应的 i18n 文案; - 产品流程提示行:
GitMergeProductFlowPresentation.productFlowHintKey(merging, conflicts, selectedConflict)对应文案,并施加mooGitMergeFlowHint()修饰符; - 刷新按钮
MooButton(zh.t("git.refresh"), p5Toolbar = true, ...),通过FocusRequester在 idle 时请求焦点。
随后captureToImage().toAwtImage()落盘为docs/evidence/2026-09-15-inspector-screencapture/windows/153-compose-git-merge-flow-hint-tab-focus.png,并用像素计数断言校验焦点环:统计0x316DC0色值像素数 ≥ 8,从而证明"刷新按钮获得焦点环"这一视觉状态确实生效。该帧属于非产品主窗的组件级证据,与本切片"状态胶囊 + 冲突/产品 hint + 刷新钮焦点"的描述完全对应。
命令盘关键词与 P7 offline gate
命令盘(Command Palette)关键词
CommandSearchCatalogTest为本切片新增的三组关键词做了回归验证,全部落在next-compose/composeApp/src/commonMain/kotlin/com/rememberber/mootool/next/compose/app/CommandSearchCatalog.kt的CommandSettingsTarget.keywords中:
| 关键词 | 期望落点 | 测试 |
|---|---|---|
vaultsearch | vault设置分类 | vaultSearchKeywordOpensVaultSettings |
profilesearch | network设置分类 | hostProfileSearchKeywordOpensNetworkSettings |
grouplabel | layout设置分类 | groupLabelKeywordOpensLayoutSettings |
vaultsearch与jsonvault、includecontent、随手记等同属 Vault 分类关键词;profilesearch与hosts、hostfile、profile、searchcontent等同属 network 分类;grouplabel与navitem、sidebaritem、紧凑、经典等同属 layout 分类。测试还通过catalogCoversEverySettingsNavCategory保证命令盘目录覆盖全部设置导航分类,防止新设置页漏挂关键词。
P7 offline gate:本切片在打包流水线中的验收口径
next-compose/scripts/prepare-p7-package-smoke.sh在注释中明确记录了 DIFF-559 的 offline gate 范围:
DIFF-559: JSON/Host 搜索 Presentation + 导航分组 + F12/F19/F23 引擎守卫 + host/json CSS + 翻译离线边界单测 + Git merge hint 帧 153 仍属 offline gate;三平台安装/公证未验收。
该脚本的流水线顺序为:
scripts/check-toolchain.sh检查工具链;- 要求
JAVA_HOME指向JDK 21(/usr/libexec/java_home -v 21,未设置即报错退出); ./gradlew :composeApp:printTooling --offline与:composeApp:verifyNativePackageMetadata --offline;./gradlew :composeApp:desktopTest --offline——本切片文档"验证"一节所指的核心测试入口;- 仅在
MOOTOOL_P7_BUILD_DIST=1时执行:composeApp:packageDistributionForCurrentOS --offline构建当前 OS 安装包,且明确 Windows MSI / Linux DEB·RPM 必须在对应操作系统上构建; - 可选
MOOTOOL_HTTP_PUBLIC_SMOKE=1触发HttpEngineTest.optionalHttpBinPublicGetSmoke,默认不属于 offline gate。
也就是说,本切片的全部功能验收都在离线桌面测试 + 证据帧层面完成,P7 的"三平台安装/公证"仍保留为后续手工步骤。这与文档"未做"一节互相印证。
另外,next-compose/scripts/prepare-tray-screencapture-evidence.sh负责从docs/evidence/2026-09-15-inspector-screencapture/windows/57-color.png恢复托盘/TCC 场景的基线证据帧(57-color-baseline.png),属于本切片验证流程中"恢复证据 PNG"的配套脚本。
验证矩阵与未做项:切片的验收边界
本切片的完整验证矩阵(9 个测试 + 2 个脚本入口):
JsonVaultSearchPresentationTest、HostWiringPresentationTest——JSON/Host 搜索接线纯函数;WorkbenchNavPresentationTest——导航分组标签决策;UaRegexTimeWiringPresentationTest、MessageBoardWiringPresentationTest、ImageSvgWiringPresentationTest——F12/F19/F23 引擎守卫;TranslationEngineTest——F20 离线翻译边界(本机 HttpServer);CommandSearchCatalogTest——vaultsearch/profilesearch/grouplabel等命令盘关键词;GitMergeFlowOverlayCaptureTest——153-compose-git-merge-flow-hint-tab-focus.png焦点帧像素断言;- 运行方式:
./scripts/prepare-tray-screencapture-evidence.sh恢复证据 PNG,./gradlew :composeApp:desktopTest --offline(JDK 21)。
明确的未做项(该切片有意留白,避免越界):六套 CSS 逐选择器皮肤、产品主窗 Vault Git merge/IME 手工 PNG、P7 三平台安装/公证、其余 substantial 引擎/UI。这些正是后续切片(DIFF-560/564/565 等)继续收敛的目标,也解释了为何切片文档在末尾反复强调"目标未达成"。
小结:从 DIFF-559 可以复用的工程模式
DIFF-559 提供了一套可复用的"切片收敛"范式,值得在同类跨端重写项目中借鉴:
- 可单测的 Presentation 纯函数层:搜索、导航、按钮守卫全部收敛为无状态函数(
JsonVaultSearchPresentation、HostWiringPresentation、WorkbenchNavPresentation、UaWiringPresentation等),Screen 只做参数透传,行为正确性由 desktopTest 保障; - 本地 HttpServer 离线端到端:用
TranslationEndpoints覆盖在线端点,让翻译引擎的分块、降级、冷却、取消、超时、语言码映射全部在无公网环境得到验证; - 像素级证据帧:通过 Compose
captureToImage+ 焦点环色值计数,把 UI 状态(如刷新按钮焦点)固化为可断言、可入库的 PNG 证据; - 命令盘关键词 + 设置分类的闭环回归:每个新功能同时挂上命令盘关键词,并以单测保证关键词与设置分类映射不漂移;
- offline gate 分层:把"功能正确性(desktopTest --offline)"与"分发验收(三平台安装/公证)"严格分层,避免单切片无限膨胀。
通过本切片,MooTool 的 Compose 桌面端在 JSON/Host 搜索、导航分组、F 工具按钮守卫与翻译离线可靠性上向 Electron 端又迈进了完整一步,而剩余的 CSS 皮肤与三平台安装缺口则留给后续切片继续推进。
- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
相关推荐
MooTool(Compose 桌面版)F 工具引擎接线:WiringPresentation 守卫模式、随手记 Vault 底栏与命令盘深链导航实现解析
MooTool(Compose 桌面版)F 工具引擎接线:WiringPresentation 守卫模式、随手记 Vault 底栏与命令盘深链导航实现解析 导读
桌面应用开发工具MooTool Next Compose 开发切片解析:F17/F03 引擎守卫、Vault Git remote、MCP stdio 编码与设置导航切片(DIFF-560)
MooTool Next Compose 开发切片解析:F17/F03 引擎守卫、Vault Git remote、MCP stdio 编码与设置导航切片(DI
桌面应用开发工具MooTool next-compose 开发切片解读:F 工具 run* 引擎接线、Vault 冲突/MCP 分页与 CSS 组件批次
MooTool next compose 开发切片解读:F 工具 run 引擎接线、Vault 冲突/MCP 分页与 CSS 组件批次 导读 DIFF 563
桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考