- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
本文基于 next-compose 项目 DIFF-506(代码运行
runtime.options长度规范化)变更记录展开,剖析其背景成因、与 ElectronnormalizeSettings的对齐逻辑、Compose 侧CodeRunRuntimeOptionsNormalize实现、会话持久化集成点与验证方式,帮助读者理解跨产品(Electron / Compose)运行时草稿与运行参数字段长度一致性保障的完整链路。
背景:为什么运行参数字段需要长度规范化
在 MooTool 的跨产品架构中,Electron 版与 Compose 版(next-compose)共享同一套产品语义,其中「代码运行」工具(F05)在 Electron 端的设置存储mootool-next.json中保存了四语言(Java / Groovy / Python / Node)的运行草稿与运行选项。
Electron 端normalizeSettings对以下字段执行截断规范化(见 settings.ts):
| 字段 | Electron 截断上限 | 说明 |
|---|---|---|
runtime.drafts.{java, groovy, python, node} | 1 MiB(1024 * 1024) | 四语言源码草稿 |
runtime.options.*.arguments | 2000 字符 | 运行参数(arguments) |
runtime.options.*.workingDirectory | 1000 字符 | 工作目录 |
具体实现位于 settings.ts 的normalizeRuntimeDrafts与normalizeRuntimeOptions:
function normalizeRuntimeDrafts(value: Record<RuntimeSettingsId, string> | undefined): Record<RuntimeSettingsId, string> { return Object.fromEntries((['java', 'groovy', 'python', 'node'] as RuntimeSettingsId[]).map((id) => [ id, typeof value?.[id] === 'string' ? value[id].slice(0, 1024 * 1024) : '' ])) as Record<RuntimeSettingsId, string> } function normalizeRuntimeOptions(value: Record<RuntimeSettingsId, RuntimeRunOption> | undefined): Record<RuntimeSettingsId, RuntimeRunOption> { return Object.fromEntries((['java', 'groovy', 'python', 'node'] as RuntimeSettingsId[]).map((id) => [id, { arguments: typeof value?.[id]?.arguments === 'string' ? value[id].arguments.slice(0, 2000) : '', workingDirectory: typeof value?.[id]?.workingDirectory === 'string' ? value[id].workingDirectory.slice(0, 1000) : '' }])) as Record<RuntimeSettingsId, RuntimeRunOption> }问题成因:此前 DIFF-151 将 Electron 的runtime.drafts/runtime.options迁入 Compose 代码运行会话时,仅对源码应用了CodeRunEngine.MAX_CODE_BYTES截断,而运行参数字符串(arguments)与从磁盘恢复的会话快照(session snapshot)可能仍超出长度上限。这会带来两类直接后果:
CodeRunEngine校验阶段拒绝运行:Compose 侧运行引擎在校验参数时对单个参数长度、工作目录长度有硬性约束(详见下文),超长字段会导致运行请求直接失败。- 持久化不可预期的大 JSON:会话快照落盘时若写入超长字段,会导致存储文件无谓膨胀。
parity-gapF05 代码运行引擎链即由此闭环,同时刻意避开 DIFF-501~505 的控件半径外观链。
核心实现:CodeRunRuntimeOptionsNormalize规范化单元
DIFF-506 的核心产物是 Compose 侧的规范化对象 CodeRunRuntimeOptionsNormalize.kt,其设计与 Electron 侧逐一对齐:
object CodeRunRuntimeOptionsNormalize { const val MAX_ARGUMENTS_CHARS = 2_000 const val MAX_WORKING_DIRECTORY_CHARS = 1_000 fun normalizeArguments(value: String): String = if (value.length <= MAX_ARGUMENTS_CHARS) value else value.take(MAX_ARGUMENTS_CHARS) fun normalizeWorkingDirectory(value: String): String = if (value.length <= MAX_WORKING_DIRECTORY_CHARS) value else value.take(MAX_WORKING_DIRECTORY_CHARS) fun normalizeSnapshot(snapshot: CodeRunSessionSnapshot): CodeRunSessionSnapshot { val limit = CodeRunEngine.MAX_CODE_BYTES return snapshot.copy( javaCode = snapshot.javaCode.take(limit), groovyCode = snapshot.groovyCode.take(limit), pythonCode = snapshot.pythonCode.take(limit), nodeCode = snapshot.nodeCode.take(limit), javaArguments = normalizeArguments(snapshot.javaArguments), groovyArguments = normalizeArguments(snapshot.groovyArguments), pythonArguments = normalizeArguments(snapshot.pythonArguments), nodeArguments = normalizeArguments(snapshot.nodeArguments), javaWorkingDirectory = normalizeWorkingDirectory(snapshot.javaWorkingDirectory), groovyWorkingDirectory = normalizeWorkingDirectory(snapshot.groovyWorkingDirectory), pythonWorkingDirectory = normalizeWorkingDirectory(snapshot.pythonWorkingDirectory), nodeWorkingDirectory = normalizeWorkingDirectory(snapshot.nodeWorkingDirectory), ) } }要点拆解:
- 两个常量与 Electron 完全一致:
MAX_ARGUMENTS_CHARS = 2000、MAX_WORKING_DIRECTORY_CHARS = 1000,源码注释明确声明"Aligns with ElectronnormalizeRuntimeOptions/normalizeRuntimeDraftsinsettings.ts"。 - 四语言同步截断:
normalizeSnapshot一次性覆盖 4 种语言 × 3 类字段(源码、arguments、workingDirectory)共 12 个字段,其中源码统一采用CodeRunEngine.MAX_CODE_BYTES(1 MiB)。 - 幂等安全:当值未超过上限时原样返回,不会破坏既有合法快照。
CodeRunEngine.MAX_CODE_BYTES定义在 CodeRunEngine.kt:
object CodeRunEngine { const val MAX_CODE_BYTES = 1024 * 1024 const val HISTORY_LIMIT_HINT = 2 * 1024 * 1024 ... }运行引擎的校验约束:超长字段为何会"拒绝运行"
理解 DIFF-506 的必要性,需要先看清 CodeRunEngine.kt 的validate入口:
private fun validate(input: CodeRunInput) { if (!requestIdPattern.matches(input.requestId)) { throw CodeRunException(CodeRunErrorCode.INVALID_REQUEST, "Invalid runtime request id") } val bytes = input.code.toByteArray(StandardCharsets.UTF_8) if (bytes.size > MAX_CODE_BYTES) { throw CodeRunException(CodeRunErrorCode.INVALID_REQUEST, "Code exceeds 1 MB limit") } if (input.arguments.size > 40 || input.arguments.any { it.length > 1000 || it.contains('\u0000') }) { throw CodeRunException(CodeRunErrorCode.INVALID_REQUEST, "Invalid runtime arguments") } if (input.workingDirectory.length > 1000 || input.workingDirectory.contains('\u0000')) { throw CodeRunException(CodeRunErrorCode.INVALID_REQUEST, "Invalid runtime working directory") } ... }可见运行引擎对参数有比存储更严格、维度更多的约束:
- 参数条目数上限 40 个;
- 单个参数长度上限 1000 字符;
- 参数与工作目录均不允许包含
\u0000(NUL 字符)。
参数解析还来自parseArguments(CodeRunEngine.kt),它实现了一个小型 shell 风格解析器:支持双引号/单引号配对、反斜杠转义(单引号内不转义)、空白分隔,并在解析后校验result.size > 40 || result.any { it.length > 1000 }。
因此,若从 Electron 迁入或从磁盘恢复的参数快照未经规范化就进入run(input, ...),极可能在validate阶段返回INVALID_REQUEST。DIFF-506 将规范化前置到导入与持久化边界,从源头杜绝超长字段进入引擎。
集成点一:Electron 设置导入链路规范化
DIFF-506 将规范化接入 ElectronNextSettingsImport.kt 的两个入口:
1.parseCodeRunPatch:从 Electron 商店 JSON 解析runtime.drafts与runtime.options时即调用normalizeSnapshot对输出规范化:
private fun parseCodeRunPatch(runtime: JsonObject): CodeRunSessionSnapshot { val drafts = runtime["drafts"]?.jsonObject val options = runtime["options"]?.jsonObject fun draft(key: String): String = drafts?.get(key)?.jsonPrimitive?.content?.trim() ?: "" fun option(key: String): Pair<String, String> { val node = options?.get(key)?.jsonObject val arguments = node?.get("arguments")?.jsonPrimitive?.content?.trim() ?: "" val workingDirectory = node?.get("workingDirectory")?.jsonPrimitive?.content?.trim() ?: "" return arguments to workingDirectory } ... return CodeRunRuntimeOptionsNormalize.normalizeSnapshot( CodeRunSessionSnapshot( javaCode = draft("java"), groovyCode = draft("groovy"), pythonCode = draft("python"), nodeCode = draft("node"), javaArguments = javaOpt.first, ... ), ) }2.mergeCodeRunSnapshots:合并当前会话与导入补丁时,先对 patch 规范化,再对合并结果二次规范化:
fun mergeCodeRunSnapshots(current: CodeRunSessionSnapshot, patch: CodeRunSessionSnapshot): CodeRunSessionSnapshot { val normalizedPatch = CodeRunRuntimeOptionsNormalize.normalizeSnapshot(patch) fun pick(patchValue: String, currentValue: String): String = patchValue.takeIf { it.isNotEmpty() } ?: currentValue return CodeRunRuntimeOptionsNormalize.normalizeSnapshot( current.copy( javaCode = pick(normalizedPatch.javaCode, current.javaCode), ... nodeWorkingDirectory = pick(normalizedPatch.nodeWorkingDirectory, current.nodeWorkingDirectory), ), ) }合并语义为"导入侧非空字段覆盖当前会话对应字段"(pick逻辑),最终输出再次经normalizeSnapshot兜底,确保即使当前会话已存在超长字段也会被收敛。这与 DIFF-151 描述的mergeCodeRunSnapshots行为一脉相承,并在其基础上补齐了参数/工作目录的规范化。
集成点二:会话快照写盘与恢复双向规范化
DIFF-506 的第三个落点是 SessionManager.kt 中的CodeRunSession(会话存储键为ToolId.Java):
snapshotState()(写盘):
fun snapshotState(): CodeRunSessionSnapshot = CodeRunRuntimeOptionsNormalize.normalizeSnapshot( CodeRunSessionSnapshot( tab = tab, javaMode = javaMode, javaCode = javaEditor.text, groovyCode = groovyEditor.text, pythonCode = pythonEditor.text, ... nodeWorkingDirectory = nodeWorkingDirectory, ), )restore()(读盘):
fun restore(snapshot: CodeRunSessionSnapshot) { val normalized = CodeRunRuntimeOptionsNormalize.normalizeSnapshot(snapshot) tab = normalized.tab javaMode = normalized.javaMode if (normalized.javaCode.isNotEmpty()) javaEditor.setText(normalized.javaCode, recordUndo = false) ... }两个方向都经过normalizeSnapshot,因此:
- 新写入的快照必然合规,持久化 JSON 体积可控;
- 磁盘上遗留的旧会话快照(含超长字段)在
restore时也会被收敛,兼容历史数据——这正是 DIFF 文档中"兼容旧会话中的超长字段"的含义。
会话生命周期由 SessionManager.kt 统一管理:codeRunSession()通过store.load(ToolId.Java.id)读取快照并调用session.restore,persistCodeRun()则调用store.save(ToolId.Java.id, ...)写入snapshotState()结果。
验证:测试用例与运行命令
DIFF-506 的验证分为两层:
1. 单元测试 CodeRunRuntimeOptionsNormalizeTest.kt
@Test fun truncatesArgumentsAndWorkingDirectoryLikeElectronNormalizeRuntimeOptions() { val longArgs = "a".repeat(CodeRunRuntimeOptionsNormalize.MAX_ARGUMENTS_CHARS + 50) val longDir = "/tmp/" + "b".repeat(CodeRunRuntimeOptionsNormalize.MAX_WORKING_DIRECTORY_CHARS) val normalized = CodeRunRuntimeOptionsNormalize.normalizeSnapshot( CodeRunSessionSnapshot( javaArguments = longArgs, javaWorkingDirectory = longDir, ), ) assertEquals(CodeRunRuntimeOptionsNormalize.MAX_ARGUMENTS_CHARS, normalized.javaArguments.length) assertEquals(longArgs.take(CodeRunRuntimeOptionsNormalize.MAX_ARGUMENTS_CHARS), normalized.javaArguments) assertEquals(CodeRunRuntimeOptionsNormalize.MAX_WORKING_DIRECTORY_CHARS, normalized.javaWorkingDirectory.length) assertEquals(longDir.take(CodeRunRuntimeOptionsNormalize.MAX_WORKING_DIRECTORY_CHARS), normalized.javaWorkingDirectory) } @Test fun truncatesDraftCodeToMaxCodeBytes() { val huge = "x".repeat(CodeRunEngine.MAX_CODE_BYTES + 100) val normalized = CodeRunRuntimeOptionsNormalize.normalizeSnapshot( CodeRunSessionSnapshot(pythonCode = huge), ) assertEquals(CodeRunEngine.MAX_CODE_BYTES, normalized.pythonCode.length) }测试断言了两件事:超长 arguments 与 workingDirectory 分别收敛到 2000 / 1000 字符,且截断结果等于take语义(即保留前 N 个字符);超长源码收敛到MAX_CODE_BYTES。
2. 导入链路集成测试 ElectronNextSettingsImportTest.kt
DIFF 文档点名的loadCodeRunPatchTruncatesOversizedOptionsLikeElectronNormalize用例,模拟 Electron 商店 JSON 中写入超长arguments与workingDirectory:
val longArgs = "x".repeat(CodeRunRuntimeOptionsNormalize.MAX_ARGUMENTS_CHARS + 10) val longDir = "/w/" + "y".repeat(CodeRunRuntimeOptionsNormalize.MAX_WORKING_DIRECTORY_CHARS) ... val patch = ElectronNextSettingsImport.loadCodeRunPatchFromStore(store)!! assertEquals(CodeRunRuntimeOptionsNormalize.MAX_ARGUMENTS_CHARS, patch.javaArguments.length) assertEquals(CodeRunRuntimeOptionsNormalize.MAX_WORKING_DIRECTORY_CHARS, patch.javaWorkingDirectory.length)同文件的loadsRuntimeDraftsAndOptionsIntoCodeRunPatch用例则验证了正常导入路径(DIFF-151 产出):runtime.drafts与runtime.options被正确解析为补丁并合并进会话。
3. 全量回归命令
./gradlew :composeApp:desktopTest --offline需要 JDK 21 环境,--offline使用本地 Gradle 依赖缓存。
变更边界:本次未做
DIFF-506 明确划定了本次变更的边界,避免范围蔓延:
- 产品窗 F05 运行参数走查帧(UI 走查)未做;
- 六套 CSS 逐选择器皮肤(控件外观链,DIFF-501~505 范畴)未做;
- 设置未实现分类面板未做;
- Git/Vault 新切片未做;
- P7 三平台安装验收未做;
- hero/claude 内容区 shell 走查未做。
这些内容属于后续 DIFF(如 DIFF-627、DIFF-629 等 F05 接线切片)的覆盖范围,与本次纯数据规范化主题解耦。
小结:一条完整的长度一致性保障链路
DIFF-506 用一个小而完整的变更补齐了 Compose 代码运行会话在长度约束上与 Electron 的一致性,其设计可归纳为**"一处定义、三处接入、双向兜底"**:
- 一处定义:
CodeRunRuntimeOptionsNormalize集中定义 2000 / 1000 / 1 MiB 三档上限,与 Electronsettings.ts常量语义一致; - 三处接入:Electron 设置导入(
parseCodeRunPatch/mergeCodeRunSnapshots)、会话快照写盘(snapshotState)、会话读盘恢复(restore); - 双向兜底:导入与合并输出均规范化,旧会话超长字段在恢复时也被收敛,保证无论数据来源(Electron 旧存储、本地快照、历史遗留)都不会触发
CodeRunEngine.validate的INVALID_REQUEST,也避免了不可预期的大 JSON 落盘。
从源码结构看,这套模式(独立的*Normalize对象 + 在会话边界统一调用)在 settings.ts 的normalizeSettings及其他会话(如 JSON 的normalizeInspectorIndent、翻译的MAX_TEXT_UNITS)中均有同构体现,可作为后续跨产品数据一致性变更的参考范式。
- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
相关推荐
MooTool Next Compose DIFF-515 深度解析:Vault Git merge/rebase 状态 UI、push 引擎守卫与 HTTP/翻译代理运行时规范化
MooTool Next Compose DIFF 515 深度解析:Vault Git merge/rebase 状态 UI、push 引擎守卫与 HTTP/
桌面应用开发工具MooTool Next Compose 跨产品迁移:Electron runtime.drafts / runtime.options 导入代码运行会话的实现
MooTool Next Compose 跨产品迁移:Electron runtime.drafts / runtime.options 导入代码运行会话的实现
桌面应用开发工具MooTool 设置页运行环境检测与可执行文件选择(DIFF-145)实现解析
MooTool 设置页运行环境检测与可执行文件选择(DIFF 145)实现解析 本文聚焦 MooTool next compose (Compose Multi
桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考