fix(pairing): avoid stale full config overwrite during origin patch

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>
This commit is contained in:
Cursor Agent
2026-06-09 11:13:58 +00:00
parent 5aa09f4bb7
commit 8230066c2b
4 changed files with 102 additions and 33 deletions

View File

@@ -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"));

View File

@@ -297,7 +297,7 @@ pub fn auto_pair_device() -> Result<String, String> {
/// 将 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<String> = 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<String> = 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]