diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index 530f6a79..d53e7f38 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -15,6 +15,8 @@ pub struct McpApps { pub gemini: bool, #[serde(default)] pub opencode: bool, + #[serde(default)] + pub openclaw: bool, } impl McpApps { @@ -25,6 +27,7 @@ impl McpApps { AppType::Codex => self.codex, AppType::Gemini => self.gemini, AppType::OpenCode => self.opencode, + AppType::OpenClaw => self.openclaw, } } @@ -35,6 +38,7 @@ impl McpApps { AppType::Codex => self.codex = enabled, AppType::Gemini => self.gemini = enabled, AppType::OpenCode => self.opencode = enabled, + AppType::OpenClaw => self.openclaw = enabled, } } @@ -53,12 +57,15 @@ impl McpApps { if self.opencode { apps.push(AppType::OpenCode); } + if self.openclaw { + apps.push(AppType::OpenClaw); + } apps } /// 检查是否所有应用都未启用 pub fn is_empty(&self) -> bool { - !self.claude && !self.codex && !self.gemini && !self.opencode + !self.claude && !self.codex && !self.gemini && !self.opencode && !self.openclaw } } @@ -73,6 +80,8 @@ pub struct SkillApps { pub gemini: bool, #[serde(default)] pub opencode: bool, + #[serde(default)] + pub openclaw: bool, } impl SkillApps { @@ -83,6 +92,7 @@ impl SkillApps { AppType::Codex => self.codex, AppType::Gemini => self.gemini, AppType::OpenCode => self.opencode, + AppType::OpenClaw => self.openclaw, } } @@ -93,6 +103,7 @@ impl SkillApps { AppType::Codex => self.codex = enabled, AppType::Gemini => self.gemini = enabled, AppType::OpenCode => self.opencode = enabled, + AppType::OpenClaw => self.openclaw = enabled, } } @@ -111,12 +122,15 @@ impl SkillApps { if self.opencode { apps.push(AppType::OpenCode); } + if self.openclaw { + apps.push(AppType::OpenClaw); + } apps } /// 检查是否所有应用都未启用 pub fn is_empty(&self) -> bool { - !self.claude && !self.codex && !self.gemini && !self.opencode + !self.claude && !self.codex && !self.gemini && !self.opencode && !self.openclaw } /// 仅启用指定应用(其他应用设为禁用) @@ -222,6 +236,9 @@ pub struct McpRoot { /// OpenCode MCP 配置(v4.0.0+,实际使用 opencode.json) #[serde(default, skip_serializing_if = "McpConfig::is_empty")] pub opencode: McpConfig, + /// OpenClaw MCP 配置(v4.1.0+,实际使用 openclaw.json) + #[serde(default, skip_serializing_if = "McpConfig::is_empty")] + pub openclaw: McpConfig, } impl Default for McpRoot { @@ -234,6 +251,7 @@ impl Default for McpRoot { codex: McpConfig::default(), gemini: McpConfig::default(), opencode: McpConfig::default(), + openclaw: McpConfig::default(), } } } @@ -256,6 +274,8 @@ pub struct PromptRoot { pub gemini: PromptConfig, #[serde(default)] pub opencode: PromptConfig, + #[serde(default)] + pub openclaw: PromptConfig, } use crate::config::{copy_file, get_app_config_dir, get_app_config_path, write_json_file}; @@ -271,6 +291,7 @@ pub enum AppType { Codex, Gemini, OpenCode, + OpenClaw, } impl AppType { @@ -280,15 +301,16 @@ impl AppType { AppType::Codex => "codex", AppType::Gemini => "gemini", AppType::OpenCode => "opencode", + AppType::OpenClaw => "openclaw", } } /// Check if this app uses additive mode /// /// - Switch mode (false): Only the current provider is written to live config (Claude, Codex, Gemini) - /// - Additive mode (true): All providers are written to live config (OpenCode) + /// - Additive mode (true): All providers are written to live config (OpenCode, OpenClaw) pub fn is_additive_mode(&self) -> bool { - matches!(self, AppType::OpenCode) + matches!(self, AppType::OpenCode | AppType::OpenClaw) } /// Return an iterator over all app types @@ -298,6 +320,7 @@ impl AppType { AppType::Codex, AppType::Gemini, AppType::OpenCode, + AppType::OpenClaw, ] .into_iter() } @@ -313,10 +336,11 @@ impl FromStr for AppType { "codex" => Ok(AppType::Codex), "gemini" => Ok(AppType::Gemini), "opencode" => Ok(AppType::OpenCode), + "openclaw" => Ok(AppType::OpenClaw), other => Err(AppError::localized( "unsupported_app", - format!("不支持的应用标识: '{other}'。可选值: claude, codex, gemini, opencode。"), - format!("Unsupported app id: '{other}'. Allowed: claude, codex, gemini, opencode."), + format!("不支持的应用标识: '{other}'。可选值: claude, codex, gemini, opencode, openclaw。"), + format!("Unsupported app id: '{other}'. Allowed: claude, codex, gemini, opencode, openclaw."), )), } } @@ -336,6 +360,9 @@ pub struct CommonConfigSnippets { #[serde(default, skip_serializing_if = "Option::is_none")] pub opencode: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub openclaw: Option, } impl CommonConfigSnippets { @@ -346,6 +373,7 @@ impl CommonConfigSnippets { AppType::Codex => self.codex.as_ref(), AppType::Gemini => self.gemini.as_ref(), AppType::OpenCode => self.opencode.as_ref(), + AppType::OpenClaw => self.openclaw.as_ref(), } } @@ -356,6 +384,7 @@ impl CommonConfigSnippets { AppType::Codex => self.codex = snippet, AppType::Gemini => self.gemini = snippet, AppType::OpenCode => self.opencode = snippet, + AppType::OpenClaw => self.openclaw = snippet, } } } @@ -396,6 +425,7 @@ impl Default for MultiAppConfig { apps.insert("codex".to_string(), ProviderManager::default()); apps.insert("gemini".to_string(), ProviderManager::default()); apps.insert("opencode".to_string(), ProviderManager::default()); + apps.insert("openclaw".to_string(), ProviderManager::default()); Self { version: 2, @@ -555,6 +585,7 @@ impl MultiAppConfig { AppType::Codex => &self.mcp.codex, AppType::Gemini => &self.mcp.gemini, AppType::OpenCode => &self.mcp.opencode, + AppType::OpenClaw => &self.mcp.openclaw, } } @@ -565,6 +596,7 @@ impl MultiAppConfig { AppType::Codex => &mut self.mcp.codex, AppType::Gemini => &mut self.mcp.gemini, AppType::OpenCode => &mut self.mcp.opencode, + AppType::OpenClaw => &mut self.mcp.openclaw, } } @@ -579,6 +611,7 @@ impl MultiAppConfig { Self::auto_import_prompt_if_exists(&mut config, AppType::Codex)?; Self::auto_import_prompt_if_exists(&mut config, AppType::Gemini)?; Self::auto_import_prompt_if_exists(&mut config, AppType::OpenCode)?; + Self::auto_import_prompt_if_exists(&mut config, AppType::OpenClaw)?; Ok(config) } @@ -599,6 +632,7 @@ impl MultiAppConfig { || !self.prompts.codex.prompts.is_empty() || !self.prompts.gemini.prompts.is_empty() || !self.prompts.opencode.prompts.is_empty() + || !self.prompts.openclaw.prompts.is_empty() { return Ok(false); } @@ -611,6 +645,7 @@ impl MultiAppConfig { AppType::Codex, AppType::Gemini, AppType::OpenCode, + AppType::OpenClaw, ] { // 复用已有的单应用导入逻辑 if Self::auto_import_prompt_if_exists(self, app)? { @@ -681,6 +716,7 @@ impl MultiAppConfig { AppType::Codex => &mut config.prompts.codex.prompts, AppType::Gemini => &mut config.prompts.gemini.prompts, AppType::OpenCode => &mut config.prompts.opencode.prompts, + AppType::OpenClaw => &mut config.prompts.openclaw.prompts, }; prompts.insert(id, prompt); @@ -709,12 +745,13 @@ impl MultiAppConfig { let mut conflicts = Vec::new(); // 收集所有应用的 MCP - for app in [AppType::Claude, AppType::Codex, AppType::Gemini] { + for app in [AppType::Claude, AppType::Codex, AppType::Gemini, AppType::OpenCode] { let old_servers = match app { AppType::Claude => &self.mcp.claude.servers, AppType::Codex => &self.mcp.codex.servers, AppType::Gemini => &self.mcp.gemini.servers, AppType::OpenCode => &self.mcp.opencode.servers, + AppType::OpenClaw => continue, // OpenClaw MCP is still in development, skip }; for (id, entry) in old_servers { diff --git a/src-tauri/src/database/dao/mcp.rs b/src-tauri/src/database/dao/mcp.rs index d5c60163..52d73ef8 100644 --- a/src-tauri/src/database/dao/mcp.rs +++ b/src-tauri/src/database/dao/mcp.rs @@ -13,7 +13,7 @@ impl Database { pub fn get_all_mcp_servers(&self) -> Result, AppError> { let conn = lock_conn!(self.conn); let mut stmt = conn.prepare( - "SELECT id, name, server_config, description, homepage, docs, tags, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode + "SELECT id, name, server_config, description, homepage, docs, tags, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw FROM mcp_servers ORDER BY name ASC, id ASC" ).map_err(|e| AppError::Database(e.to_string()))?; @@ -31,6 +31,7 @@ impl Database { let enabled_codex: bool = row.get(8)?; let enabled_gemini: bool = row.get(9)?; let enabled_opencode: bool = row.get(10)?; + let enabled_openclaw: bool = row.get(11)?; let server = serde_json::from_str(&server_config_str).unwrap_or_default(); let tags = serde_json::from_str(&tags_str).unwrap_or_default(); @@ -46,6 +47,7 @@ impl Database { codex: enabled_codex, gemini: enabled_gemini, opencode: enabled_opencode, + openclaw: enabled_openclaw, }, description, homepage, @@ -70,8 +72,8 @@ impl Database { conn.execute( "INSERT OR REPLACE INTO mcp_servers ( id, name, server_config, description, homepage, docs, tags, - enabled_claude, enabled_codex, enabled_gemini, enabled_opencode - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", params![ server.id, server.name, @@ -87,6 +89,7 @@ impl Database { server.apps.codex, server.apps.gemini, server.apps.opencode, + server.apps.openclaw, ], ) .map_err(|e| AppError::Database(e.to_string()))?; diff --git a/src-tauri/src/database/dao/skills.rs b/src-tauri/src/database/dao/skills.rs index 2254cb06..f29e89a6 100644 --- a/src-tauri/src/database/dao/skills.rs +++ b/src-tauri/src/database/dao/skills.rs @@ -22,7 +22,7 @@ impl Database { let mut stmt = conn .prepare( "SELECT id, name, description, directory, repo_owner, repo_name, repo_branch, - readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at + readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw, installed_at FROM skills ORDER BY name ASC", ) .map_err(|e| AppError::Database(e.to_string()))?; @@ -43,8 +43,9 @@ impl Database { codex: row.get(9)?, gemini: row.get(10)?, opencode: row.get(11)?, + openclaw: row.get(12)?, }, - installed_at: row.get(12)?, + installed_at: row.get(13)?, }) }) .map_err(|e| AppError::Database(e.to_string()))?; @@ -63,7 +64,7 @@ impl Database { let mut stmt = conn .prepare( "SELECT id, name, description, directory, repo_owner, repo_name, repo_branch, - readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at + readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw, installed_at FROM skills WHERE id = ?1", ) .map_err(|e| AppError::Database(e.to_string()))?; @@ -83,8 +84,9 @@ impl Database { codex: row.get(9)?, gemini: row.get(10)?, opencode: row.get(11)?, + openclaw: row.get(12)?, }, - installed_at: row.get(12)?, + installed_at: row.get(13)?, }) }); @@ -101,8 +103,8 @@ impl Database { conn.execute( "INSERT OR REPLACE INTO skills (id, name, description, directory, repo_owner, repo_name, repo_branch, - readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw, installed_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", params![ skill.id, skill.name, @@ -116,6 +118,7 @@ impl Database { skill.apps.codex, skill.apps.gemini, skill.apps.opencode, + skill.apps.openclaw, skill.installed_at, ], ) @@ -145,8 +148,8 @@ impl Database { let conn = lock_conn!(self.conn); let affected = conn .execute( - "UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4 WHERE id = ?5", - params![apps.claude, apps.codex, apps.gemini, apps.opencode, id], + "UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4, enabled_openclaw = ?5 WHERE id = ?6", + params![apps.claude, apps.codex, apps.gemini, apps.opencode, apps.openclaw, id], ) .map_err(|e| AppError::Database(e.to_string()))?; Ok(affected > 0) diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 41c26e97..352593fd 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -48,7 +48,7 @@ const DB_BACKUP_RETAIN: usize = 10; /// 当前 Schema 版本号 /// 每次修改表结构时递增,并在 schema.rs 中添加相应的迁移逻辑 -pub(crate) const SCHEMA_VERSION: i32 = 5; +pub(crate) const SCHEMA_VERSION: i32 = 6; /// 安全地序列化 JSON,避免 unwrap panic pub(crate) fn to_json_string(value: &T) -> Result { diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index bca3c2de..92493912 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -58,7 +58,8 @@ impl Database { id TEXT PRIMARY KEY, name TEXT NOT NULL, server_config TEXT NOT NULL, description TEXT, homepage TEXT, docs TEXT, tags TEXT NOT NULL DEFAULT '[]', enabled_claude BOOLEAN NOT NULL DEFAULT 0, enabled_codex BOOLEAN NOT NULL DEFAULT 0, - enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0 + enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0, + enabled_openclaw BOOLEAN NOT NULL DEFAULT 0 )", [], ) @@ -86,6 +87,7 @@ impl Database { enabled_codex BOOLEAN NOT NULL DEFAULT 0, enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0, + enabled_openclaw BOOLEAN NOT NULL DEFAULT 0, installed_at INTEGER NOT NULL DEFAULT 0 )", [], @@ -360,6 +362,11 @@ impl Database { Self::migrate_v4_to_v5(conn)?; Self::set_user_version(conn, 5)?; } + 5 => { + log::info!("迁移数据库从 v5 到 v6(OpenClaw 支持)"); + Self::migrate_v5_to_v6(conn)?; + Self::set_user_version(conn, 6)?; + } _ => { return Err(AppError::Database(format!( "未知的数据库版本 {version},无法迁移到 {SCHEMA_VERSION}" @@ -852,6 +859,7 @@ impl Database { enabled_claude BOOLEAN NOT NULL DEFAULT 0, enabled_codex BOOLEAN NOT NULL DEFAULT 0, enabled_gemini BOOLEAN NOT NULL DEFAULT 0, + enabled_openclaw BOOLEAN NOT NULL DEFAULT 0, installed_at INTEGER NOT NULL DEFAULT 0 )", [], @@ -914,6 +922,30 @@ impl Database { Ok(()) } + /// v5 -> v6 迁移:添加 OpenClaw 支持 + /// + /// 为 mcp_servers 和 skills 表添加 enabled_openclaw 列。 + fn migrate_v5_to_v6(conn: &Connection) -> Result<(), AppError> { + // 为 mcp_servers 表添加 enabled_openclaw 列 + Self::add_column_if_missing( + conn, + "mcp_servers", + "enabled_openclaw", + "BOOLEAN NOT NULL DEFAULT 0", + )?; + + // 为 skills 表添加 enabled_openclaw 列 + Self::add_column_if_missing( + conn, + "skills", + "enabled_openclaw", + "BOOLEAN NOT NULL DEFAULT 0", + )?; + + log::info!("v5 -> v6 迁移完成:已添加 OpenClaw 支持"); + Ok(()) + } + /// 插入默认模型定价数据 /// 格式: (model_id, display_name, input, output, cache_read, cache_creation) /// 注意: model_id 使用短横线格式(如 claude-haiku-4-5),与 API 返回的模型名称标准化后一致