diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0950d9063..bd5fd8c38 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -714,6 +714,7 @@ dependencies = [ "futures", "hyper", "indexmap 2.11.4", + "json5", "log", "objc2 0.5.2", "objc2-app-kit 0.2.2", @@ -2521,6 +2522,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jsonptr" version = "0.6.3" @@ -3405,6 +3417,49 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.8.0" @@ -5888,6 +5943,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 12e288837..9547eb342 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -62,6 +62,7 @@ indexmap = { version = "2", features = ["serde"] } rust_decimal = "1.33" uuid = { version = "1.11", features = ["v4"] } sha2 = "0.10" +json5 = "0.4" [target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies] tauri-plugin-single-instance = "2" diff --git a/src-tauri/src/openclaw_config.rs b/src-tauri/src/openclaw_config.rs new file mode 100644 index 000000000..5a30d2565 --- /dev/null +++ b/src-tauri/src/openclaw_config.rs @@ -0,0 +1,326 @@ +//! OpenClaw 配置文件读写模块 +//! +//! 处理 `~/.openclaw/openclaw.json` 配置文件的读写操作(JSON5 格式)。 +//! OpenClaw 使用累加式供应商管理,所有供应商配置共存于同一配置文件中。 +//! +//! ## 配置文件格式 +//! +//! ```json5 +//! { +//! // 模型供应商配置(映射为 CC Switch 的"供应商") +//! models: { +//! mode: "merge", +//! providers: { +//! "custom-provider": { +//! baseUrl: "https://api.example.com/v1", +//! apiKey: "${API_KEY}", +//! api: "openai-completions", +//! models: [{ id: "model-id", name: "Model Name" }] +//! } +//! } +//! }, +//! // 环境变量配置 +//! env: { +//! ANTHROPIC_API_KEY: "sk-...", +//! vars: { ... } +//! }, +//! // Agent 默认模型配置 +//! agents: { +//! defaults: { +//! model: { +//! primary: "provider/model", +//! fallbacks: ["provider2/model2"] +//! } +//! } +//! } +//! } +//! ``` + +use crate::config::write_json_file; +use crate::error::AppError; +use crate::settings::get_openclaw_override_dir; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Map, Value}; +use std::path::PathBuf; + +// ============================================================================ +// Path Functions +// ============================================================================ + +/// 获取 OpenClaw 配置目录 +/// +/// 默认路径: `~/.openclaw/` +/// 可通过 settings.openclaw_config_dir 覆盖 +pub fn get_openclaw_dir() -> PathBuf { + if let Some(override_dir) = get_openclaw_override_dir() { + return override_dir; + } + + // 所有平台统一使用 ~/.openclaw + dirs::home_dir() + .map(|h| h.join(".openclaw")) + .unwrap_or_else(|| PathBuf::from(".openclaw")) +} + +/// 获取 OpenClaw 配置文件路径 +/// +/// 返回 `~/.openclaw/openclaw.json` +pub fn get_openclaw_config_path() -> PathBuf { + get_openclaw_dir().join("openclaw.json") +} + +/// 获取 OpenClaw Skills 目录路径 +/// +/// 返回 `~/.openclaw/skills/` +#[allow(dead_code)] +pub fn get_openclaw_skills_dir() -> PathBuf { + get_openclaw_dir().join("skills") +} + +// ============================================================================ +// Type Definitions +// ============================================================================ + +/// OpenClaw 供应商配置(对应 models.providers 中的条目) +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OpenClawProviderConfig { + /// API 基础 URL + #[serde(skip_serializing_if = "Option::is_none")] + pub base_url: Option, + + /// API Key(支持环境变量引用 ${VAR_NAME}) + #[serde(skip_serializing_if = "Option::is_none")] + pub api_key: Option, + + /// API 类型(如 "openai-completions", "anthropic" 等) + #[serde(skip_serializing_if = "Option::is_none")] + pub api: Option, + + /// 支持的模型列表 + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub models: Vec, + + /// 其他自定义字段(保留原始配置) + #[serde(flatten)] + pub extra: Map, +} + +/// OpenClaw 模型条目 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenClawModelEntry { + /// 模型 ID + pub id: String, + + /// 模型显示名称 + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// 其他自定义字段 + #[serde(flatten)] + pub extra: Map, +} + +// ============================================================================ +// Core Read/Write Functions +// ============================================================================ + +/// 读取 OpenClaw 配置文件 +/// +/// 支持 JSON5 格式,返回完整的配置 JSON 对象 +pub fn read_openclaw_config() -> Result { + let path = get_openclaw_config_path(); + + if !path.exists() { + // Return empty config structure + return Ok(json!({ + "models": { + "mode": "merge", + "providers": {} + } + })); + } + + let content = std::fs::read_to_string(&path).map_err(|e| AppError::io(&path, e))?; + + // 尝试 JSON5 解析(支持注释和尾随逗号) + json5::from_str(&content).map_err(|e| { + AppError::Config(format!( + "Failed to parse OpenClaw config as JSON5: {}", + e + )) + }) +} + +/// 写入 OpenClaw 配置文件(原子写入) +/// +/// 使用标准 JSON 格式写入(JSON5 是 JSON 的超集) +pub fn write_openclaw_config(config: &Value) -> Result<(), AppError> { + let path = get_openclaw_config_path(); + + // 确保目录存在 + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?; + } + + // 复用统一的原子写入逻辑 + write_json_file(&path, config)?; + + log::debug!("OpenClaw config written to {path:?}"); + Ok(()) +} + +// ============================================================================ +// Provider Functions (Untyped - for raw JSON operations) +// ============================================================================ + +/// 获取所有供应商配置(原始 JSON) +/// +/// 从 `models.providers` 读取 +pub fn get_providers() -> Result, AppError> { + let config = read_openclaw_config()?; + Ok(config + .get("models") + .and_then(|m| m.get("providers")) + .and_then(|v| v.as_object()) + .cloned() + .unwrap_or_default()) +} + +/// 设置供应商配置(原始 JSON) +/// +/// 写入到 `models.providers` +pub fn set_provider(id: &str, provider_config: Value) -> Result<(), AppError> { + let mut full_config = read_openclaw_config()?; + + // 确保 models 结构存在 + if full_config.get("models").is_none() { + full_config["models"] = json!({ + "mode": "merge", + "providers": {} + }); + } + + // 确保 providers 对象存在 + if full_config["models"].get("providers").is_none() { + full_config["models"]["providers"] = json!({}); + } + + // 设置供应商 + if let Some(providers) = full_config["models"] + .get_mut("providers") + .and_then(|v| v.as_object_mut()) + { + providers.insert(id.to_string(), provider_config); + } + + write_openclaw_config(&full_config) +} + +/// 删除供应商配置 +pub fn remove_provider(id: &str) -> Result<(), AppError> { + let mut config = read_openclaw_config()?; + + if let Some(providers) = config + .get_mut("models") + .and_then(|m| m.get_mut("providers")) + .and_then(|v| v.as_object_mut()) + { + providers.remove(id); + } + + write_openclaw_config(&config) +} + +// ============================================================================ +// Provider Functions (Typed) +// ============================================================================ + +/// 获取所有供应商配置(类型化) +pub fn get_typed_providers() -> Result, AppError> { + let providers = get_providers()?; + let mut result = IndexMap::new(); + + for (id, value) in providers { + match serde_json::from_value::(value.clone()) { + Ok(config) => { + result.insert(id, config); + } + Err(e) => { + log::warn!("Failed to parse OpenClaw provider '{id}': {e}"); + // Skip invalid providers but continue + } + } + } + + Ok(result) +} + +/// 设置供应商配置(类型化) +pub fn set_typed_provider(id: &str, config: &OpenClawProviderConfig) -> Result<(), AppError> { + let value = serde_json::to_value(config).map_err(|e| AppError::JsonSerialize { source: e })?; + set_provider(id, value) +} + +// ============================================================================ +// Environment Variables +// ============================================================================ + +/// 获取环境变量配置 +#[allow(dead_code)] +pub fn get_env_vars() -> Result, AppError> { + let config = read_openclaw_config()?; + Ok(config + .get("env") + .and_then(|v| v.as_object()) + .cloned() + .unwrap_or_default()) +} + +/// 设置环境变量 +#[allow(dead_code)] +pub fn set_env_var(key: &str, value: &str) -> Result<(), AppError> { + let mut config = read_openclaw_config()?; + + if config.get("env").is_none() { + config["env"] = json!({}); + } + + if let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) { + env.insert(key.to_string(), Value::String(value.to_string())); + } + + write_openclaw_config(&config) +} + +// ============================================================================ +// MCP Functions (Reserved for future use) +// ============================================================================ + +/// 获取所有 MCP 服务器配置 +/// +/// OpenClaw MCP 配置在 `agents.list[].mcp.servers` +/// 由于 OpenClaw MCP 支持仍在开发中(Issue #4834),暂时返回空 +#[allow(dead_code)] +pub fn get_mcp_servers() -> Result, AppError> { + // OpenClaw MCP support is under development + // Return empty for now + Ok(Map::new()) +} + +/// 设置 MCP 服务器配置 +#[allow(dead_code)] +pub fn set_mcp_server(_id: &str, _config: Value) -> Result<(), AppError> { + // OpenClaw MCP support is under development + log::warn!("OpenClaw MCP support is not yet implemented"); + Ok(()) +} + +/// 删除 MCP 服务器配置 +#[allow(dead_code)] +pub fn remove_mcp_server(_id: &str) -> Result<(), AppError> { + // OpenClaw MCP support is under development + log::warn!("OpenClaw MCP support is not yet implemented"); + Ok(()) +}