diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 9533b34..7f756ad 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -814,6 +814,46 @@ pub fn save_openclaw_json(config: &Value) -> Result<(), String> { write_openclaw_config(config.clone()) } +/// Append required Gateway control-ui origins without clobbering other gateway fields. +/// +/// Callers must pass a **partial** delta (not a full config snapshot). `write_openclaw_config` +/// shallow-merges top-level objects; a stale full snapshot would revert concurrent edits to +/// `gateway.auth`, `gateway.port`, etc. +pub fn append_gateway_allowed_origins(required: &[&str]) -> Result<(), String> { + let config = load_openclaw_json().unwrap_or_else(|_| json!({})); + let existing: Vec = config + .pointer("/gateway/controlUi/allowedOrigins") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|value| value.as_str().map(|value| value.to_string())) + .collect() + }) + .unwrap_or_default(); + + let mut merged = existing.clone(); + let mut changed = false; + for origin in required { + let origin = (*origin).to_string(); + if !merged.iter().any(|existing| existing == &origin) { + merged.push(origin); + changed = true; + } + } + if !changed { + return Ok(()); + } + + let delta = json!({ + "gateway": { + "controlUi": { + "allowedOrigins": merged + } + } + }); + write_openclaw_config(delta) +} + fn model_env_values_for_config(config: &Value) -> HashMap { let mut values = HashMap::new(); if let Some(env) = config.get("env").and_then(|v| v.as_object()) { @@ -7215,6 +7255,61 @@ mod write_openclaw_config_merge_tests { std::env::temp_dir().join(format!("clawpanel-{name}-{}-{suffix}", std::process::id())) } + /// Regression: auto-pair must patch allowedOrigins with a partial delta, not a stale + /// full config snapshot — otherwise shallow gateway merge reverts concurrent auth/port edits. + #[test] + fn partial_gateway_patch_preserves_auth_and_port() { + let existing = json!({ + "gateway": { + "port": 9999, + "auth": { "mode": "token", "token": "user-new-token" }, + "controlUi": { "allowedOrigins": ["http://localhost"] } + } + }); + let stale_full_snapshot = json!({ + "gateway": { + "port": 18789, + "auth": { "mode": "token", "token": "old-token" }, + "controlUi": { + "allowedOrigins": [ + "http://localhost", + "tauri://localhost", + "http://localhost:1420" + ] + } + } + }); + let clobbered = merge_configs_preserving_fields(&existing, &stale_full_snapshot); + assert_eq!( + clobbered["gateway"]["auth"]["token"], + "old-token", + "full snapshot merge must reproduce the clobber bug for this test" + ); + + let delta = json!({ + "gateway": { + "controlUi": { + "allowedOrigins": [ + "http://localhost", + "tauri://localhost", + "http://localhost:1420" + ] + } + } + }); + let merged = merge_configs_preserving_fields(&existing, &delta); + assert_eq!(merged["gateway"]["auth"]["token"], "user-new-token"); + assert_eq!(merged["gateway"]["port"], 9999); + assert_eq!( + merged["gateway"]["controlUi"]["allowedOrigins"], + json!([ + "http://localhost", + "tauri://localhost", + "http://localhost:1420" + ]) + ); + } + /// Regression guard: Issue #127 merge keeps full provider map when the UI payload /// only touches one provider — `sync_providers_to_agent_models` must use the same /// merged view (see `write_openclaw_config`), not the raw `config` argument. diff --git a/src-tauri/src/commands/pairing.rs b/src-tauri/src/commands/pairing.rs index 8c9e933..702fdee 100644 --- a/src-tauri/src/commands/pairing.rs +++ b/src-tauri/src/commands/pairing.rs @@ -297,50 +297,14 @@ 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 { - return; - }; - - // Tauri 应用 + 本地开发服务器必须存在的 origin - let required: Vec = vec![ - "tauri://localhost".into(), - "https://tauri.localhost".into(), - "http://tauri.localhost".into(), - "http://localhost:1420".into(), - "http://127.0.0.1:1420".into(), + const REQUIRED: &[&str] = &[ + "tauri://localhost", + "https://tauri.localhost", + "http://tauri.localhost", + "http://localhost:1420", + "http://127.0.0.1:1420", ]; - - 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 _ = super::config::save_openclaw_json(&config); + let _ = super::config::append_gateway_allowed_origins(REQUIRED); } #[tauri::command]