☰
MooTool Compose 桌面端 DIFF-559 切片解析:JSON/Host 搜索接线、导航分组、F 工具引擎守卫与翻译离线边界
2026/10/16 2:52:58 网站建设 项目流程
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

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

本篇技术指南围绕 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" }

决策规则可归纳为三条:

  1. 全局关闭分隔线(showSeparators == false)时一律不显示分组标题;
  2. 自定义分组(customGroup == true)即使开启也始终保留标题——对应 Electron.app-shell--nav-classic .tool-group h2 { display: none }但自定义分组保留标题的语义;
  3. 默认分组仅在导航样式非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)全部覆盖,实现完全无公网的端到端测试。

本切片文档点名的三个边界场景:

  1. 空文本:emptyTextReturnsOkWithoutNetwork——输入空串时translate直接返回ok = true、text = "",不发任何网络请求;
  2. Google 畸形 JSON:malformedGooglePayloadFallsBackOrFailsOffline——/google返回"not-json",Bing 页面与 v3 端点返回 500,结果errorCode == NETWORK;
  3. 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中:

关键词期望落点测试
vaultsearchvault设置分类vaultSearchKeywordOpensVaultSettings
profilesearchnetwork设置分类hostProfileSearchKeywordOpensNetworkSettings
grouplabellayout设置分类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;三平台安装/公证未验收。

该脚本的流水线顺序为:

  1. scripts/check-toolchain.sh检查工具链;
  2. 要求JAVA_HOME指向JDK 21(/usr/libexec/java_home -v 21,未设置即报错退出);
  3. ./gradlew :composeApp:printTooling --offline与:composeApp:verifyNativePackageMetadata --offline;
  4. ./gradlew :composeApp:desktopTest --offline——本切片文档"验证"一节所指的核心测试入口;
  5. 仅在MOOTOOL_P7_BUILD_DIST=1时执行:composeApp:packageDistributionForCurrentOS --offline构建当前 OS 安装包,且明确 Windows MSI / Linux DEB·RPM 必须在对应操作系统上构建;
  6. 可选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 提供了一套可复用的"切片收敛"范式,值得在同类跨端重写项目中借鉴:

  1. 可单测的 Presentation 纯函数层:搜索、导航、按钮守卫全部收敛为无状态函数(JsonVaultSearchPresentation、HostWiringPresentation、WorkbenchNavPresentation、UaWiringPresentation等),Screen 只做参数透传,行为正确性由 desktopTest 保障;
  2. 本地 HttpServer 离线端到端:用TranslationEndpoints覆盖在线端点,让翻译引擎的分块、降级、冷却、取消、超时、语言码映射全部在无公网环境得到验证;
  3. 像素级证据帧:通过 ComposecaptureToImage+ 焦点环色值计数,把 UI 状态(如刷新按钮焦点)固化为可断言、可入库的 PNG 证据;
  4. 命令盘关键词 + 设置分类的闭环回归:每个新功能同时挂上命令盘关键词,并以单测保证关键词与设置分类映射不漂移;
  5. offline gate 分层:把"功能正确性(desktopTest --offline)"与"分发验收(三平台安装/公证)"严格分层,避免单切片无限膨胀。

通过本切片,MooTool 的 Compose 桌面端在 JSON/Host 搜索、导航分组、F 工具按钮守卫与翻译离线可靠性上向 Electron 端又迈进了完整一步,而剩余的 CSS 皮肤与三平台安装缺口则留给后续切片继续推进。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

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

相关推荐

上一篇:SpacetimeDB 模块发布完整指南:从 `spacetime build` 到 `spacetime publish` 的构建、发布与迁移实战
下一篇:MaaAssistantArknights 新手快速上手指南:环境准备、模拟器连接与首个任务配置

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询