diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index c70939e04..87430b46f 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -59,6 +59,15 @@ pub async fn get_config_status(app: String) -> Result { Ok(ConfigStatus { exists, path }) } + AppType::OpenClaw => { + let config_path = crate::openclaw_config::get_openclaw_config_path(); + let exists = config_path.exists(); + let path = crate::openclaw_config::get_openclaw_dir() + .to_string_lossy() + .to_string(); + + Ok(ConfigStatus { exists, path }) + } } } @@ -74,6 +83,7 @@ pub async fn get_config_dir(app: String) -> Result { AppType::Codex => codex_config::get_codex_config_dir(), AppType::Gemini => crate::gemini_config::get_gemini_dir(), AppType::OpenCode => crate::opencode_config::get_opencode_dir(), + AppType::OpenClaw => crate::openclaw_config::get_openclaw_dir(), }; Ok(dir.to_string_lossy().to_string()) @@ -86,6 +96,7 @@ pub async fn open_config_folder(handle: AppHandle, app: String) -> Result codex_config::get_codex_config_dir(), AppType::Gemini => crate::gemini_config::get_gemini_dir(), AppType::OpenCode => crate::opencode_config::get_opencode_dir(), + AppType::OpenClaw => crate::openclaw_config::get_openclaw_dir(), }; if !config_dir.exists() { diff --git a/src-tauri/src/commands/provider.rs b/src-tauri/src/commands/provider.rs index 47de928f5..70ce9048e 100644 --- a/src-tauri/src/commands/provider.rs +++ b/src-tauri/src/commands/provider.rs @@ -318,3 +318,27 @@ pub fn get_opencode_live_provider_ids() -> Result, String> { .map(|providers| providers.keys().cloned().collect()) .map_err(|e| e.to_string()) } + +// ============================================================================ +// OpenClaw 专属命令 +// ============================================================================ + +/// 从 OpenClaw live 配置导入供应商到数据库 +/// +/// 这是 OpenClaw 特有的功能,因为 OpenClaw 使用累加模式, +/// 用户可能已经在 openclaw.json 中配置了供应商。 +#[tauri::command] +pub fn import_openclaw_providers_from_live(state: State<'_, AppState>) -> Result { + crate::services::provider::import_openclaw_providers_from_live(state.inner()) + .map_err(|e| e.to_string()) +} + +/// 获取 OpenClaw live 配置中的供应商 ID 列表 +/// +/// 用于前端判断供应商是否已添加到 openclaw.json +#[tauri::command] +pub fn get_openclaw_live_provider_ids() -> Result, String> { + crate::openclaw_config::get_providers() + .map(|providers| providers.keys().cloned().collect()) + .map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/deeplink/mcp.rs b/src-tauri/src/deeplink/mcp.rs index 04d3f11b9..67f3d92cb 100644 --- a/src-tauri/src/deeplink/mcp.rs +++ b/src-tauri/src/deeplink/mcp.rs @@ -167,6 +167,7 @@ pub(crate) fn parse_mcp_apps(apps_str: &str) -> Result { codex: false, gemini: false, opencode: false, + openclaw: false, }; for app in apps_str.split(',') { @@ -175,6 +176,7 @@ pub(crate) fn parse_mcp_apps(apps_str: &str) -> Result { "codex" => apps.codex = true, "gemini" => apps.gemini = true, "opencode" => apps.opencode = true, + "openclaw" => apps.openclaw = true, other => { return Err(AppError::InvalidInput(format!( "Invalid app in 'apps': {other}" diff --git a/src-tauri/src/deeplink/provider.rs b/src-tauri/src/deeplink/provider.rs index ec75d55e5..0528a2e49 100644 --- a/src-tauri/src/deeplink/provider.rs +++ b/src-tauri/src/deeplink/provider.rs @@ -146,6 +146,7 @@ pub(crate) fn build_provider_from_request( AppType::Codex => build_codex_settings(request), AppType::Gemini => build_gemini_settings(request), AppType::OpenCode => build_opencode_settings(request), + AppType::OpenClaw => build_openclaw_settings(request), }; // Build usage script configuration if provided @@ -391,6 +392,35 @@ fn build_opencode_settings(request: &DeepLinkImportRequest) -> serde_json::Value }) } +fn build_openclaw_settings(request: &DeepLinkImportRequest) -> serde_json::Value { + let endpoint = get_primary_endpoint(request); + + // Build OpenClaw provider config + // Format: { baseUrl, apiKey, api, models } + let mut config = serde_json::Map::new(); + + if !endpoint.is_empty() { + config.insert("baseUrl".to_string(), json!(endpoint)); + } + + if let Some(api_key) = &request.api_key { + config.insert("apiKey".to_string(), json!(api_key)); + } + + // Default to OpenAI-compatible API + config.insert("api".to_string(), json!("openai-completions")); + + // Build models array + if let Some(model) = &request.model { + config.insert( + "models".to_string(), + json!([{ "id": model, "name": model }]), + ); + } + + json!(config) +} + // ============================================================================= // Config Merge Logic // ============================================================================= diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ef47fdaee..dc36d60d9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -14,6 +14,7 @@ mod gemini_mcp; mod init_status; mod mcp; mod opencode_config; +mod openclaw_config; mod panic_hook; mod prompt; mod prompt_files; @@ -525,6 +526,17 @@ pub fn run() { } } + // 2.3 OpenClaw 供应商导入(累加式模式,需特殊处理) + // OpenClaw 与 OpenCode 类似:配置文件中可同时存在多个供应商 + // 需要遍历 models.providers 字段下的每个供应商并导入 + match crate::services::provider::import_openclaw_providers_from_live(&app_state) { + Ok(count) if count > 0 => { + log::info!("✓ Imported {count} OpenClaw provider(s) from live config"); + } + Ok(_) => log::debug!("○ No OpenClaw providers found to import"), + Err(e) => log::debug!("○ Failed to import OpenClaw providers: {e}"), + } + // 3. 导入 MCP 服务器配置(表空时触发) if app_state.db.is_mcp_table_empty().unwrap_or(false) { log::info!("MCP table empty, importing from live configurations..."); @@ -992,6 +1004,9 @@ pub fn run() { // OpenCode specific commands::import_opencode_providers_from_live, commands::get_opencode_live_provider_ids, + // OpenClaw specific + commands::import_openclaw_providers_from_live, + commands::get_openclaw_live_provider_ids, // Global upstream proxy commands::get_global_proxy_url, commands::set_global_proxy_url, diff --git a/src-tauri/src/mcp/claude.rs b/src-tauri/src/mcp/claude.rs index 25d2f426a..3ba1a310b 100644 --- a/src-tauri/src/mcp/claude.rs +++ b/src-tauri/src/mcp/claude.rs @@ -92,6 +92,7 @@ pub fn import_from_claude(config: &mut MultiAppConfig) -> Result Result codex: true, gemini: false, opencode: false, + openclaw: false, }, description: None, homepage: None, diff --git a/src-tauri/src/mcp/gemini.rs b/src-tauri/src/mcp/gemini.rs index c8a7809f3..b2725da27 100644 --- a/src-tauri/src/mcp/gemini.rs +++ b/src-tauri/src/mcp/gemini.rs @@ -88,6 +88,7 @@ pub fn import_from_gemini(config: &mut MultiAppConfig) -> Result Result Result { @@ -14,6 +15,7 @@ pub fn prompt_file_path(app: &AppType) -> Result { AppType::Codex => get_base_dir_with_fallback(get_codex_auth_path(), ".codex")?, AppType::Gemini => get_gemini_dir(), AppType::OpenCode => get_opencode_dir(), + AppType::OpenClaw => get_openclaw_dir(), }; let filename = match app { @@ -21,6 +23,7 @@ pub fn prompt_file_path(app: &AppType) -> Result { AppType::Codex => "AGENTS.md", AppType::Gemini => "GEMINI.md", AppType::OpenCode => "AGENTS.md", + AppType::OpenClaw => "AGENTS.md", // OpenClaw uses AGENTS.md for agent instructions }; Ok(base_dir.join(filename))