diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 62bea01..7db82c5 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -2687,10 +2687,17 @@ function patchGatewayOrigins() { const merged = [...new Set([...existing, ...origins])] // 幂等:已包含所有需要的 origin 时跳过写入 if (origins.every(o => existing.includes(o))) return false - if (!config.gateway) config.gateway = {} - if (!config.gateway.controlUi) config.gateway.controlUi = {} - config.gateway.controlUi.allowedOrigins = merged - writeOpenclawConfigFile(config) + // 只写入 allowedOrigins 增量,避免用陈旧全量快照覆盖并发保存的其它配置字段。 + const existingOnDisk = fs.existsSync(CONFIG_PATH) ? JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) : null + const partial = { + gateway: { + controlUi: { + allowedOrigins: merged, + }, + }, + } + const mergedConfig = existingOnDisk ? mergeConfigsPreservingFields(existingOnDisk, partial) : partial + writeOpenclawConfigFile(mergedConfig) return true } diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 1fe31b0..86c3c19 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -7613,6 +7613,33 @@ mod write_openclaw_config_merge_tests { assert_eq!(resolved, Some(cmd)); } + #[test] + fn partial_gateway_patch_preserves_auth_token() { + let existing = json!({ + "gateway": { + "auth": { "token": "secret-new" }, + "controlUi": { "allowedOrigins": ["http://localhost:3000"] } + } + }); + let patch = json!({ + "gateway": { + "controlUi": { + "allowedOrigins": ["http://localhost:3000", "tauri://localhost"] + } + } + }); + let merged = merge_configs_preserving_fields(&existing, &patch); + assert_eq!( + merged.pointer("/gateway/auth/token"), + Some(&json!("secret-new")) + ); + let origins = merged + .pointer("/gateway/controlUi/allowedOrigins") + .and_then(|v| v.as_array()) + .expect("allowedOrigins"); + assert!(origins.iter().any(|v| v == "tauri://localhost")); + } + #[test] fn node_requirement_rejects_versions_below_minimum() { assert!(!node_version_satisfies_requirement("v22.17.0", ">=22.19.0")); diff --git a/src-tauri/src/commands/pairing.rs b/src-tauri/src/commands/pairing.rs index 8c9e933..88298b6 100644 --- a/src-tauri/src/commands/pairing.rs +++ b/src-tauri/src/commands/pairing.rs @@ -297,7 +297,7 @@ pub fn auto_pair_device() -> Result { /// 将 Tauri 应用的 origin 写入 gateway.controlUi.allowedOrigins /// 避免 Gateway 因 origin not allowed 拒绝 WebSocket 握手 fn patch_gateway_origins() { - let Ok(mut config) = super::config::load_openclaw_json() else { + let Ok(config) = super::config::load_openclaw_json() else { return; }; @@ -310,37 +310,36 @@ fn patch_gateway_origins() { "http://127.0.0.1:1420".into(), ]; - if let Some(obj) = config.as_object_mut() { - let gateway = obj - .entry("gateway") - .or_insert_with(|| serde_json::json!({})); - if let Some(gw) = gateway.as_object_mut() { - let control_ui = gw - .entry("controlUi") - .or_insert_with(|| serde_json::json!({})); - if let Some(cui) = control_ui.as_object_mut() { - // 合并:保留用户已有的 origin,追加缺失的 Tauri origin - let existing: Vec = cui - .get("allowedOrigins") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|s| s.as_str().map(String::from)) - .collect() - }) - .unwrap_or_default(); - let mut merged = existing; - for r in &required { - if !merged.iter().any(|e| e == r) { - merged.push(r.clone()); - } - } - cui.insert("allowedOrigins".to_string(), serde_json::json!(merged)); - } + let existing: Vec = config + .pointer("/gateway/controlUi/allowedOrigins") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|s| s.as_str().map(String::from)) + .collect() + }) + .unwrap_or_default(); + + if required.iter().all(|origin| existing.iter().any(|e| e == origin)) { + return; + } + + let mut merged = existing; + for origin in &required { + if !merged.iter().any(|existing_origin| existing_origin == origin) { + merged.push(origin.clone()); } } - let _ = super::config::save_openclaw_json(&config); + // 只写入 allowedOrigins 增量,避免用陈旧全量快照覆盖并发保存的其它配置字段。 + let patch = serde_json::json!({ + "gateway": { + "controlUi": { + "allowedOrigins": merged + } + } + }); + let _ = super::config::save_openclaw_json(&patch); } #[tauri::command] diff --git a/tests/patch-gateway-origins.test.js b/tests/patch-gateway-origins.test.js new file mode 100644 index 0000000..ea283db --- /dev/null +++ b/tests/patch-gateway-origins.test.js @@ -0,0 +1,36 @@ +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 via 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 增量/, + 'dev-api 自动配对 origin 修补必须只更新 allowedOrigins', + ) + assert.match( + fn, + /mergeConfigsPreservingFields\(existingOnDisk, partial\)/, + 'dev-api 自动配对 origin 修补必须走局部 merge 写入', + ) + assert.doesNotMatch( + fn, + /writeOpenclawConfigFile\(config\)/, + 'dev-api 不能再把陈旧全量 config 直接写回磁盘', + ) +}) + +test('patch_gateway_origins writes only allowedOrigins patch in Rust', () => { + assert.match( + pairing, + /只写入 allowedOrigins 增量[\s\S]*save_openclaw_json\(&patch\)/, + 'Rust 自动配对 origin 修补必须只提交 gateway.controlUi.allowedOrigins 增量', + ) +})