From 8230066c2b280ee94a05df742f34b596937d0666 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 9 Jun 2026 11:13:58 +0000 Subject: [PATCH] fix(pairing): avoid stale full config overwrite during origin patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit auto_pair_device always patches gateway.controlUi.allowedOrigins. The previous implementation saved the entire config snapshot captured at read time, which could clobber concurrent user edits to gateway auth, models, or channels when WebSocket auto-pair ran during reconnect. Write only the allowedOrigins delta through the existing merge path so other fields on disk are preserved. Co-authored-by: 晴天 <1186258278@users.noreply.github.com> --- scripts/dev-api.js | 15 ++++++-- src-tauri/src/commands/config.rs | 27 ++++++++++++++ src-tauri/src/commands/pairing.rs | 57 ++++++++++++++--------------- tests/patch-gateway-origins.test.js | 36 ++++++++++++++++++ 4 files changed, 102 insertions(+), 33 deletions(-) create mode 100644 tests/patch-gateway-origins.test.js 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 增量', + ) +})