feat(core): add OpenClaw to AppType enum and database schema

- Add OpenClaw variant to AppType enum
- Update is_additive_mode() to return true for OpenClaw
- Update McpApps, SkillApps, McpRoot, PromptRoot structs
- Add database migration v5 to v6 for enabled_openclaw columns
- Update mcp_servers and skills table definitions
This commit is contained in:
Jason
2026-02-01 20:30:32 +08:00
parent 9c34d04def
commit 7f46a0b910
5 changed files with 95 additions and 20 deletions

View File

@@ -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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub openclaw: Option<String>,
}
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 {

View File

@@ -13,7 +13,7 @@ impl Database {
pub fn get_all_mcp_servers(&self) -> Result<IndexMap<String, McpServer>, 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()))?;

View File

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

View File

@@ -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<T: Serialize>(value: &T) -> Result<String, AppError> {

View File

@@ -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 到 v6OpenClaw 支持)");
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 返回的模型名称标准化后一致