fix(openclaw): 兼容新版配置与 Node 门槛

This commit is contained in:
晴天
2026-06-11 15:24:01 +08:00
parent 5aa09f4bb7
commit 675ad1628b
33 changed files with 717 additions and 130 deletions

View File

@@ -71,6 +71,7 @@ test('Hermes 配置页会暴露记忆结构化配置字段', () => {
'hm-memory-user-char-limit',
'hm-memory-nudge-interval',
'hm-memory-flush-min-turns',
'hm-memory-qmd-rerank',
]) {
assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`)
}
@@ -243,6 +244,7 @@ test('Hermes 配置页会暴露 Tirith 安全扫描结构化配置字段', () =>
'hm-security-tirith-path',
'hm-security-tirith-timeout',
'hm-security-tirith-fail-open',
'hm-security-install-policy-json',
]) {
assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`)
}

View File

@@ -16,6 +16,7 @@ test('Hermes 记忆配置读取会提供上游默认值', () => {
userCharLimit: 1375,
nudgeInterval: 10,
flushMinTurns: 6,
qmdRerank: true,
})
})
@@ -28,6 +29,9 @@ test('Hermes 记忆配置读取会回显 YAML 中的记忆字段', () => {
user_char_limit: 1800,
nudge_interval: 12,
flush_min_turns: 8,
qmd: {
rerank: false,
},
},
})
@@ -37,6 +41,7 @@ test('Hermes 记忆配置读取会回显 YAML 中的记忆字段', () => {
assert.equal(values.userCharLimit, 1800)
assert.equal(values.nudgeInterval, 12)
assert.equal(values.flushMinTurns, 8)
assert.equal(values.qmdRerank, false)
})
test('Hermes 记忆配置保存会保留无关 YAML 并写入 snake_case 字段', () => {
@@ -47,6 +52,10 @@ test('Hermes 记忆配置保存会保留无关 YAML 并写入 snake_case 字段'
provider: 'honcho',
custom_flag: 'keep-me',
flush_min_turns: 9,
qmd: {
provider: 'qmd',
rerank: true,
},
},
streaming: { enabled: true },
}, {
@@ -56,6 +65,7 @@ test('Hermes 记忆配置保存会保留无关 YAML 并写入 snake_case 字段'
userCharLimit: '1500',
nudgeInterval: '0',
flushMinTurns: '7',
qmdRerank: false,
})
assert.deepEqual(next.model, { provider: 'anthropic' })
@@ -66,6 +76,8 @@ test('Hermes 记忆配置保存会保留无关 YAML 并写入 snake_case 字段'
assert.equal(next.memory.user_char_limit, 1500)
assert.equal(next.memory.nudge_interval, 0)
assert.equal(next.memory.flush_min_turns, 7)
assert.equal(next.memory.qmd.rerank, false)
assert.equal(next.memory.qmd.provider, 'qmd')
assert.equal(next.memory.provider, 'honcho')
assert.equal(next.memory.custom_flag, 'keep-me')
})

View File

@@ -14,6 +14,7 @@ test('Hermes 安全扫描配置读取会提供 Tirith 默认值', () => {
tirithPath: 'tirith',
tirithTimeout: 5,
tirithFailOpen: true,
installPolicyJson: '',
})
})
@@ -24,6 +25,10 @@ test('Hermes 安全扫描配置读取会规范化已有值', () => {
tirith_path: 'C:/tools/tirith.exe',
tirith_timeout: 12,
tirith_fail_open: false,
installPolicy: {
enabled: true,
targets: ['skill', 'plugin'],
},
},
})
@@ -31,6 +36,10 @@ test('Hermes 安全扫描配置读取会规范化已有值', () => {
assert.equal(values.tirithPath, 'C:/tools/tirith.exe')
assert.equal(values.tirithTimeout, 12)
assert.equal(values.tirithFailOpen, false)
assert.deepEqual(JSON.parse(values.installPolicyJson), {
enabled: true,
targets: ['skill', 'plugin'],
})
})
test('Hermes 安全扫描配置保存会保留未知字段并写入 security.tirith', () => {
@@ -39,6 +48,10 @@ test('Hermes 安全扫描配置保存会保留未知字段并写入 security.tir
security: {
allow_private_urls: false,
website_blocklist: { enabled: true, domains: ['example.com'] },
installPolicy: {
enabled: false,
targets: ['skill'],
},
custom_flag: 'keep-security',
},
terminal: { backend: 'docker' },
@@ -47,6 +60,15 @@ test('Hermes 安全扫描配置保存会保留未知字段并写入 security.tir
tirithPath: '~/bin/tirith',
tirithTimeout: '9',
tirithFailOpen: false,
installPolicyJson: JSON.stringify({
enabled: true,
targets: ['skill', 'plugin'],
exec: {
source: 'exec',
command: 'tirith',
args: ['scan'],
},
}),
})
assert.deepEqual(next.model, { provider: 'anthropic' })
@@ -54,6 +76,15 @@ test('Hermes 安全扫描配置保存会保留未知字段并写入 security.tir
assert.equal(next.security.allow_private_urls, false)
assert.deepEqual(next.security.website_blocklist, { enabled: true, domains: ['example.com'] })
assert.equal(next.security.custom_flag, 'keep-security')
assert.deepEqual(next.security.installPolicy, {
enabled: true,
targets: ['skill', 'plugin'],
exec: {
source: 'exec',
command: 'tirith',
args: ['scan'],
},
})
assert.equal(next.security.tirith_enabled, false)
assert.equal(next.security.tirith_path, '~/bin/tirith')
assert.equal(next.security.tirith_timeout, 9)
@@ -69,4 +100,8 @@ test('Hermes 安全扫描配置保存会拒绝非法超时和空路径', () => {
() => mergeHermesSecurityConfig({}, { tirithPath: '' }),
/security\.tirith_path/,
)
assert.throws(
() => mergeHermesSecurityConfig({}, { installPolicyJson: '[]' }),
/security\.installPolicy/,
)
})

View File

@@ -20,13 +20,13 @@ test('Hermes Web 工具配置读取会回显 YAML 字段', () => {
const values = buildHermesWebConfigValues({
web: {
backend: 'tavily',
search_backend: 'searxng',
search_backend: 'parallel-free',
extract_backend: 'firecrawl',
},
})
assert.equal(values.webBackend, 'tavily')
assert.equal(values.webSearchBackend, 'searxng')
assert.equal(values.webSearchBackend, 'parallel-free')
assert.equal(values.webExtractBackend, 'firecrawl')
})
@@ -41,14 +41,14 @@ test('Hermes Web 工具配置保存会保留未知字段并写入上游结构',
},
streaming: { enabled: true },
}, {
webBackend: 'parallel',
webBackend: 'parallel-free',
webSearchBackend: 'exa',
webExtractBackend: 'native',
})
assert.deepEqual(next.model, { provider: 'anthropic' })
assert.deepEqual(next.streaming, { enabled: true })
assert.equal(next.web.backend, 'parallel')
assert.equal(next.web.backend, 'parallel-free')
assert.equal(next.web.search_backend, 'exa')
assert.equal(next.web.extract_backend, 'native')
assert.equal(next.web.custom_flag, 'keep-web')

View File

@@ -0,0 +1,39 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
const devApi = readFileSync(new URL('../scripts/dev-api.js', import.meta.url), 'utf8')
const rustConfig = readFileSync(new URL('../src-tauri/src/commands/config.rs', import.meta.url), 'utf8')
test('web check_node prefers standalone bundled Node when available', () => {
const start = devApi.indexOf('check_node() {')
const end = devApi.indexOf('get_status_summary()', start)
const fn = start >= 0 && end > start ? devApi.slice(start, end) : ''
assert.ok(fn, 'check_node handler must exist')
assert.match(fn, /classifyCliSource\(cliPath\) === 'standalone'/)
assert.match(fn, /standaloneBundledNodePath\(cliPath\)/)
assert.match(fn, /detectedFrom: 'standalone-bundled'/)
})
test('desktop check_node prefers standalone bundled Node before PATH lookup', () => {
const start = rustConfig.indexOf('pub fn check_node()')
const pathLookup = rustConfig.indexOf('let node_path = find_node_path', start)
const bundledLookup = rustConfig.indexOf('standalone_bundled_node_bin(&cli_path)', start)
assert.ok(start >= 0, 'check_node must exist')
assert.ok(bundledLookup > start, 'standalone bundled Node lookup must exist')
assert.ok(pathLookup > bundledLookup, 'bundled Node lookup must run before PATH lookup')
assert.match(rustConfig, /"standalone-bundled"/)
})
test('Node 22.19 fallback is gated by OpenClaw 2026.6.5 or newer', () => {
assert.match(devApi, /OPENCLAW_NODE_REQUIREMENT_VERSION_FLOOR = '2026\.6\.5'/)
assert.match(devApi, /OPENCLAW_NODE_REQUIREMENT_FOR_NEWER_RUNTIME = '>=22\.19\.0'/)
assert.match(devApi, /versionGe\(baseVersion\(installedVersion\), OPENCLAW_NODE_REQUIREMENT_VERSION_FLOOR\)/)
assert.doesNotMatch(devApi, /DEFAULT_OPENCLAW_NODE_REQUIREMENT/)
assert.match(rustConfig, /OPENCLAW_NODE_REQUIREMENT_VERSION_FLOOR: &str = "2026\.6\.5"/)
assert.match(rustConfig, /OPENCLAW_NODE_REQUIREMENT_FOR_NEWER_RUNTIME: &str = ">=22\.19\.0"/)
assert.match(rustConfig, /openclaw_version_requires_node_22_19/)
})

View File

@@ -0,0 +1,29 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
const devApi = readFileSync(new URL('../scripts/dev-api.js', import.meta.url), 'utf8')
const pairing = readFileSync(new URL('../src-tauri/src/commands/pairing.rs', import.meta.url), 'utf8')
test('patchGatewayOrigins writes only allowedOrigins through merge path', () => {
const start = devApi.indexOf('function patchGatewayOrigins()')
const end = devApi.indexOf('function readOpenclawConfigOptional()', start)
const fn = start >= 0 && end > start ? devApi.slice(start, end) : ''
assert.ok(fn, 'patchGatewayOrigins must exist')
assert.match(fn, /只写入 allowedOrigins 增量/)
assert.match(fn, /const partial = \{\s*gateway: \{\s*controlUi: \{\s*allowedOrigins: merged,/s)
assert.match(fn, /mergeConfigsPreservingFields\(latest, partial\)/)
assert.doesNotMatch(fn, /writeOpenclawConfigFile\(config\)/)
})
test('patch_gateway_origins writes only allowedOrigins patch in Rust', () => {
const start = pairing.indexOf('fn patch_gateway_origins()')
const end = pairing.indexOf('#[tauri::command]', start)
const fn = start >= 0 && end > start ? pairing.slice(start, end) : ''
assert.ok(fn, 'patch_gateway_origins must exist')
assert.match(fn, /只写入 allowedOrigins 增量/)
assert.match(fn, /let patch = serde_json::json!\(\{/)
assert.match(fn, /save_openclaw_json\(&patch\)/)
})