feat(config): unify common config snippets persistence across all apps

- Add unified `common_config_snippets` structure to MultiAppConfig
- Implement `get_common_config_snippet` and `set_common_config_snippet` commands
- Replace localStorage with config.json persistence for Codex and Gemini
- Auto-migrate legacy `claude_common_config_snippet` to new unified structure
- Deprecate individual API methods in favor of unified interface
- Add automatic migration from localStorage on first load

BREAKING CHANGE: Common config snippets now stored in unified `common_config_snippets` object instead of separate fields
This commit is contained in:
Jason
2025-11-15 19:52:49 +08:00
parent 2540f6ba08
commit 154ff4c819
7 changed files with 300 additions and 90 deletions

View File

@@ -174,6 +174,39 @@ impl FromStr for AppType {
}
}
/// 通用配置片段(按应用分治)
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CommonConfigSnippets {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claude: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub codex: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gemini: Option<String>,
}
impl CommonConfigSnippets {
/// 获取指定应用的通用配置片段
pub fn get(&self, app: &AppType) -> Option<&String> {
match app {
AppType::Claude => self.claude.as_ref(),
AppType::Codex => self.codex.as_ref(),
AppType::Gemini => self.gemini.as_ref(),
}
}
/// 设置指定应用的通用配置片段
pub fn set(&mut self, app: &AppType, snippet: Option<String>) {
match app {
AppType::Claude => self.claude = snippet,
AppType::Codex => self.codex = snippet,
AppType::Gemini => self.gemini = snippet,
}
}
}
/// 多应用配置结构(向后兼容)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiAppConfig {
@@ -188,7 +221,10 @@ pub struct MultiAppConfig {
/// Prompt 配置(按客户端分治)
#[serde(default)]
pub prompts: PromptRoot,
/// Claude 通用配置片段JSON 字符串,用于跨供应商共享配置
/// 通用配置片段(按应用分治
#[serde(default)]
pub common_config_snippets: CommonConfigSnippets,
/// Claude 通用配置片段(旧字段,用于向后兼容迁移)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claude_common_config_snippet: Option<String>,
}
@@ -209,6 +245,7 @@ impl Default for MultiAppConfig {
apps,
mcp: McpRoot::default(),
prompts: PromptRoot::default(),
common_config_snippets: CommonConfigSnippets::default(),
claude_common_config_snippet: None,
}
}
@@ -278,6 +315,13 @@ impl MultiAppConfig {
updated = true;
}
// 迁移通用配置片段claude_common_config_snippet → common_config_snippets.claude
if let Some(old_claude_snippet) = config.claude_common_config_snippet.take() {
log::info!("迁移通用配置claude_common_config_snippet → common_config_snippets.claude");
config.common_config_snippets.claude = Some(old_claude_snippet);
updated = true;
}
if updated {
log::info!("配置结构已更新(包括 MCP 迁移或 Prompt 自动导入),保存配置...");
config.save()?;

View File

@@ -136,7 +136,7 @@ pub async fn open_app_config_folder(handle: AppHandle) -> Result<bool, String> {
Ok(true)
}
/// 获取 Claude 通用配置片段
/// 获取 Claude 通用配置片段(已废弃,使用 get_common_config_snippet
#[tauri::command]
pub async fn get_claude_common_config_snippet(
state: tauri::State<'_, crate::store::AppState>,
@@ -145,10 +145,10 @@ pub async fn get_claude_common_config_snippet(
.config
.read()
.map_err(|e| format!("读取配置锁失败: {e}"))?;
Ok(guard.claude_common_config_snippet.clone())
Ok(guard.common_config_snippets.claude.clone())
}
/// 设置 Claude 通用配置片段
/// 设置 Claude 通用配置片段(已废弃,使用 set_common_config_snippet
#[tauri::command]
pub async fn set_claude_common_config_snippet(
snippet: String,
@@ -165,7 +165,7 @@ pub async fn set_claude_common_config_snippet(
.map_err(|e| format!("无效的 JSON 格式: {e}"))?;
}
guard.claude_common_config_snippet = if snippet.trim().is_empty() {
guard.common_config_snippets.claude = if snippet.trim().is_empty() {
None
} else {
Some(snippet)
@@ -174,3 +174,69 @@ pub async fn set_claude_common_config_snippet(
guard.save().map_err(|e| e.to_string())?;
Ok(())
}
/// 获取通用配置片段(统一接口)
#[tauri::command]
pub async fn get_common_config_snippet(
app_type: String,
state: tauri::State<'_, crate::store::AppState>,
) -> Result<Option<String>, String> {
use crate::app_config::AppType;
use std::str::FromStr;
let app = AppType::from_str(&app_type)
.map_err(|e| format!("无效的应用类型: {}", e))?;
let guard = state
.config
.read()
.map_err(|e| format!("读取配置锁失败: {}", e))?;
Ok(guard.common_config_snippets.get(&app).cloned())
}
/// 设置通用配置片段(统一接口)
#[tauri::command]
pub async fn set_common_config_snippet(
app_type: String,
snippet: String,
state: tauri::State<'_, crate::store::AppState>,
) -> Result<(), String> {
use crate::app_config::AppType;
use std::str::FromStr;
let app = AppType::from_str(&app_type)
.map_err(|e| format!("无效的应用类型: {}", e))?;
let mut guard = state
.config
.write()
.map_err(|e| format!("写入配置锁失败: {}", e))?;
// 验证格式(根据应用类型)
if !snippet.trim().is_empty() {
match app {
AppType::Claude | AppType::Gemini => {
// 验证 JSON 格式
serde_json::from_str::<serde_json::Value>(&snippet)
.map_err(|e| format!("无效的 JSON 格式: {}", e))?;
}
AppType::Codex => {
// TOML 格式暂不验证(或可使用 toml crate
// 注意TOML 验证较为复杂,暂时跳过
}
}
}
guard.common_config_snippets.set(
&app,
if snippet.trim().is_empty() {
None
} else {
Some(snippet)
},
);
guard.save().map_err(|e| e.to_string())?;
Ok(())
}

View File

@@ -517,6 +517,8 @@ pub fn run() {
commands::open_app_config_folder,
commands::get_claude_common_config_snippet,
commands::set_claude_common_config_snippet,
commands::get_common_config_snippet,
commands::set_common_config_snippet,
commands::read_live_provider_settings,
commands::get_settings,
commands::save_settings,