feat(openclaw): add OpenClaw configuration module

- Create openclaw_config.rs for reading/writing OpenClaw config
- Support JSON5 format (~/.openclaw/openclaw.json)
- Implement provider CRUD operations for additive mode
- Add json5 dependency to Cargo.toml
This commit is contained in:
Jason
2026-02-01 20:30:47 +08:00
parent 7f46a0b910
commit 63fafd6608
3 changed files with 388 additions and 0 deletions
+61
View File
@@ -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"
+1
View File
@@ -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"
+326
View File
@@ -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<String>,
/// API Key(支持环境变量引用 ${VAR_NAME}
#[serde(skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
/// API 类型(如 "openai-completions", "anthropic" 等)
#[serde(skip_serializing_if = "Option::is_none")]
pub api: Option<String>,
/// 支持的模型列表
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub models: Vec<OpenClawModelEntry>,
/// 其他自定义字段(保留原始配置)
#[serde(flatten)]
pub extra: Map<String, Value>,
}
/// OpenClaw 模型条目
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenClawModelEntry {
/// 模型 ID
pub id: String,
/// 模型显示名称
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// 其他自定义字段
#[serde(flatten)]
pub extra: Map<String, Value>,
}
// ============================================================================
// Core Read/Write Functions
// ============================================================================
/// 读取 OpenClaw 配置文件
///
/// 支持 JSON5 格式,返回完整的配置 JSON 对象
pub fn read_openclaw_config() -> Result<Value, AppError> {
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<Map<String, Value>, 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<IndexMap<String, OpenClawProviderConfig>, AppError> {
let providers = get_providers()?;
let mut result = IndexMap::new();
for (id, value) in providers {
match serde_json::from_value::<OpenClawProviderConfig>(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<Map<String, Value>, 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<Map<String, Value>, 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(())
}