fix(config): resolve common config sync issues across frontend and backend

- Fix backfill pollution: extract custom config from live when common config enabled
- Fix proxy backup: merge common config before backup to preserve full config
- Unify null override semantics: null no longer overrides in both frontend and backend
- Add missing i18n keys for common config UI
- Hide format button in readonly JsonEditor
- Add mapGeminiWarningToI18n for user-friendly warning messages
This commit is contained in:
YoVinchen
2026-01-27 12:28:46 +08:00
parent 733605ae5c
commit 9e25ecf475
11 changed files with 200 additions and 26 deletions
+1 -1
View File
@@ -705,7 +705,7 @@ fn merge_gemini_config(custom_config: &JsonValue, common_snippet: &str) -> Merge
/// - ENV format: KEY=VALUE lines
/// - Flat JSON: {"KEY": "VALUE", ...}
/// - Wrapped JSON: {"env": {"KEY": "VALUE", ...}}
fn parse_gemini_common_snippet(snippet: &str) -> serde_json::Map<String, JsonValue> {
pub(crate) fn parse_gemini_common_snippet(snippet: &str) -> serde_json::Map<String, JsonValue> {
let trimmed = snippet.trim();
if trimmed.is_empty() {
return serde_json::Map::new();
+94 -1
View File
@@ -13,6 +13,7 @@ use serde::Deserialize;
use serde_json::Value;
use crate::app_config::AppType;
use crate::config_merge::{extract_json_difference, extract_toml_difference_str, is_common_config_enabled};
use crate::error::AppError;
use crate::provider::{Provider, UsageResult};
use crate::services::mcp::McpService;
@@ -386,7 +387,18 @@ impl ProviderService {
// Only backfill when switching to a different provider
if let Ok(live_config) = read_live_settings(app_type.clone()) {
if let Some(mut current_provider) = providers.get(&current_id).cloned() {
current_provider.settings_config = live_config;
// Check if common config is enabled for this provider
let common_enabled = is_common_config_enabled(current_provider.meta.as_ref(), &app_type);
let config_to_save = if common_enabled {
// Extract custom config from live (remove common config parts)
Self::extract_custom_from_live(state, &app_type, &live_config)?
} else {
// Common config not enabled, use live config directly
live_config
};
current_provider.settings_config = config_to_save;
// Ignore backfill failure, don't affect switch flow
let _ = state.db.save_provider(app_type.as_str(), &current_provider);
}
@@ -459,6 +471,87 @@ impl ProviderService {
}
}
/// Extract custom config from live config (remove common config parts).
///
/// This is used during backfill to avoid polluting the provider's settings_config
/// with common config values that should remain in the common config snippet.
fn extract_custom_from_live(
state: &AppState,
app_type: &AppType,
live_config: &Value,
) -> Result<Value, AppError> {
// Get common config snippet from database
let common_snippet = state
.db
.get_config_snippet(app_type.as_str())?
.unwrap_or_default();
if common_snippet.trim().is_empty() {
// No common config, return live config as-is
return Ok(live_config.clone());
}
match app_type {
AppType::Claude => {
// Parse common config as JSON
let common_config: Value = serde_json::from_str(&common_snippet).map_err(|e| {
AppError::Config(format!("Failed to parse common config snippet: {e}"))
})?;
// Extract difference (custom = live - common)
let (custom_config, _) = extract_json_difference(live_config, &common_config);
Ok(custom_config)
}
AppType::Codex => {
// Codex: Extract TOML config field difference
let mut result = live_config.clone();
if let Some(config_str) = live_config.get("config").and_then(|v| v.as_str()) {
// Extract TOML difference for config field
// Returns (custom_toml, has_common_keys, error)
let (custom_toml, _, _) = extract_toml_difference_str(config_str, &common_snippet);
if let Some(obj) = result.as_object_mut() {
obj.insert("config".to_string(), Value::String(custom_toml));
}
}
Ok(result)
}
AppType::Gemini => {
// Gemini: Extract env field difference
// Parse common config (supports ENV format and JSON format)
let common_env = crate::config_merge::parse_gemini_common_snippet(&common_snippet);
if common_env.is_empty() {
return Ok(live_config.clone());
}
let mut result = live_config.clone();
if let Some(live_env) = live_config.get("env").and_then(|v| v.as_object()) {
// Extract difference: custom = live_env - common_env
let mut custom_env = serde_json::Map::new();
for (key, value) in live_env {
if common_env.get(key) != Some(value) {
// Key doesn't exist in common or value is different
custom_env.insert(key.clone(), value.clone());
}
}
if let Some(obj) = result.as_object_mut() {
obj.insert("env".to_string(), Value::Object(custom_env));
}
}
Ok(result)
}
AppType::OpenCode => {
// OpenCode doesn't support common config
Ok(live_config.clone())
}
}
}
/// Extract common config for Claude (JSON format)
fn extract_claude_common_config(settings: &Value) -> Result<String, AppError> {
let mut config = settings.clone();
+38 -7
View File
@@ -1506,26 +1506,57 @@ impl ProxyService {
///
/// 与 backup_live_configs() 不同,此方法从供应商的 settings_config 生成备份,
/// 而不是从 Live 文件读取(因为 Live 文件已被代理接管)。
///
/// **重要**: 新架构下 settings_config 存储的是自定义配置(custom diff),
/// 备份时需要先与 common config 合并,生成完整的 finalConfig。
pub async fn update_live_backup_from_provider(
&self,
app_type: &str,
provider: &Provider,
) -> Result<(), String> {
let app_type_enum =
AppType::from_str(app_type).map_err(|_| format!("无效的应用类型: {app_type}"))?;
// Get common config snippet for merge
let common_snippet = self
.db
.get_config_snippet(app_type)
.ok()
.flatten();
// Merge custom config with common config to get final config
let merge_result = merge_config_for_live(
&app_type_enum,
provider,
common_snippet.as_deref(),
);
// Log warning if any
if let Some(warning) = &merge_result.warning {
log::warn!(
"Common config merge warning for {} provider '{}': {}",
app_type,
provider.id,
warning
);
}
let final_config = merge_result.config;
let backup_json = match app_type {
"claude" => {
// Claude: settings_config 直接作为备份
serde_json::to_string(&provider.settings_config)
// Claude: 使用合并后的 final config 作为备份
serde_json::to_string(&final_config)
.map_err(|e| format!("序列化 Claude 配置失败: {e}"))?
}
"codex" => {
// Codex: settings_config 包含 {"auth": ..., "config": ...},直接使用
serde_json::to_string(&provider.settings_config)
// Codex: 使用合并后的 final config
serde_json::to_string(&final_config)
.map_err(|e| format!("序列化 Codex 配置失败: {e}"))?
}
"gemini" => {
// Gemini: 只提取 env 字段(与原始备份格式一致)
// proxy.rs 的 read_gemini_live() 返回 {"env": {...}}
let env_backup = if let Some(env) = provider.settings_config.get("env") {
let env_backup = if let Some(env) = final_config.get("env") {
json!({ "env": env })
} else {
json!({ "env": {} })
@@ -1541,7 +1572,7 @@ impl ProxyService {
.await
.map_err(|e| format!("更新 {app_type} 备份失败: {e}"))?;
log::info!("已更新 {app_type} Live 备份(热切换)");
log::info!("已更新 {app_type} Live 备份(热切换,含 common config 合并");
Ok(())
}