mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 08:44:41 +08:00
fix(openclaw): address code review findings across P0-P3 issues
- Add 25 missing i18n keys for OpenClawFormFields in all 3 locales (P0)
- Replace key={index} with stable crypto.randomUUID() keys in EnvPanel,
ToolsPanel, and OpenClawFormFields to prevent list state bugs (P1)
- Exclude openclaw from ProxyToggle/FailoverToggle in App.tsx (P1)
- Add merge_additive_config() for openclaw/opencode deep link imports (P1)
- Normalize serde(flatten) field naming to `extra` + HashMap (P2)
- Add directory existence check in remove_openclaw_provider_from_live (P2)
- Remove dead code in import_default_config and openclaw API methods (P2)
- Add duplicate key validation in EnvPanel before save (P2)
- Add openclawConfigDir to Settings type (P2)
- Add staleTime to OpenClaw query hooks (P3)
- Fix type-unsafe delete via destructuring in mutations.ts (P3)
This commit is contained in:
@@ -482,6 +482,10 @@ pub fn parse_and_merge_config(
|
||||
"claude" => merge_claude_config(&mut merged, &config_value)?,
|
||||
"codex" => merge_codex_config(&mut merged, &config_value)?,
|
||||
"gemini" => merge_gemini_config(&mut merged, &config_value)?,
|
||||
// Additive mode apps use JSON config directly; pass through as-is
|
||||
"openclaw" | "opencode" => {
|
||||
merge_additive_config(&mut merged, &config_value)?;
|
||||
}
|
||||
"" => {
|
||||
// No app specified, skip merging
|
||||
return Ok(merged);
|
||||
@@ -653,6 +657,47 @@ fn merge_gemini_config(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Merge configuration for additive mode apps (OpenClaw, OpenCode)
|
||||
///
|
||||
/// These apps use JSON config directly, so we only extract common fields
|
||||
/// (api_key, endpoint, model) from the config if not already set in URL params.
|
||||
fn merge_additive_config(
|
||||
request: &mut DeepLinkImportRequest,
|
||||
config: &serde_json::Value,
|
||||
) -> Result<(), AppError> {
|
||||
// Extract api_key from config if not provided in URL
|
||||
if request.api_key.as_ref().is_none_or(|s| s.is_empty()) {
|
||||
if let Some(api_key) = config
|
||||
.get("apiKey")
|
||||
.or_else(|| config.get("api_key"))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
request.api_key = Some(api_key.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Extract endpoint from config if not provided in URL
|
||||
if request.endpoint.as_ref().is_none_or(|s| s.is_empty()) {
|
||||
if let Some(base_url) = config
|
||||
.get("baseUrl")
|
||||
.or_else(|| config.get("base_url"))
|
||||
.or_else(|| config.get("options").and_then(|o| o.get("baseURL")))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
request.endpoint = Some(base_url.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-fill homepage from endpoint
|
||||
if request.homepage.as_ref().is_none_or(|s| s.is_empty()) {
|
||||
if let Some(endpoint) = request.endpoint.as_ref().filter(|s| !s.is_empty()) {
|
||||
request.homepage = infer_homepage_from_endpoint(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract base_url from Codex TOML config
|
||||
fn extract_codex_base_url(toml_value: &toml::Value) -> Option<String> {
|
||||
// Try to find base_url in model_providers section
|
||||
|
||||
@@ -95,9 +95,9 @@ pub struct OpenClawProviderConfig {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub models: Vec<OpenClawModelEntry>,
|
||||
|
||||
/// 其他自定义字段(保留原始配置)
|
||||
/// Other custom fields (preserve unknown fields)
|
||||
#[serde(flatten)]
|
||||
pub extra: Map<String, Value>,
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// OpenClaw 模型条目
|
||||
@@ -123,9 +123,9 @@ pub struct OpenClawModelEntry {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub context_window: Option<u32>,
|
||||
|
||||
/// 其他自定义字段(保留未知字段)
|
||||
/// Other custom fields (preserve unknown fields)
|
||||
#[serde(flatten)]
|
||||
pub extra: Map<String, Value>,
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// OpenClaw 模型成本配置
|
||||
@@ -137,9 +137,9 @@ pub struct OpenClawModelCost {
|
||||
/// 输出价格(每百万 token)
|
||||
pub output: f64,
|
||||
|
||||
/// 其他自定义字段(保留未知字段)
|
||||
/// Other custom fields (preserve unknown fields)
|
||||
#[serde(flatten)]
|
||||
pub other: HashMap<String, Value>,
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// OpenClaw 默认模型配置(agents.defaults.model)
|
||||
@@ -152,9 +152,9 @@ pub struct OpenClawDefaultModel {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub fallbacks: Vec<String>,
|
||||
|
||||
/// 其他自定义字段(保留未知字段)
|
||||
/// Other custom fields (preserve unknown fields)
|
||||
#[serde(flatten)]
|
||||
pub other: HashMap<String, Value>,
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// OpenClaw 模型目录条目(agents.defaults.models 中的值)
|
||||
@@ -164,9 +164,9 @@ pub struct OpenClawModelCatalogEntry {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub alias: Option<String>,
|
||||
|
||||
/// 其他自定义字段(保留未知字段)
|
||||
/// Other custom fields (preserve unknown fields)
|
||||
#[serde(flatten)]
|
||||
pub other: HashMap<String, Value>,
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// OpenClaw agents.defaults 配置
|
||||
@@ -180,21 +180,22 @@ pub struct OpenClawAgentsDefaults {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub models: Option<HashMap<String, OpenClawModelCatalogEntry>>,
|
||||
|
||||
/// 其他自定义字段(保留未知字段)
|
||||
/// Other custom fields (preserve unknown fields)
|
||||
#[serde(flatten)]
|
||||
pub other: HashMap<String, Value>,
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// OpenClaw agents 顶层配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct OpenClawAgents {
|
||||
/// 默认配置
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub defaults: Option<OpenClawAgentsDefaults>,
|
||||
|
||||
/// 其他自定义字段(保留未知字段)
|
||||
/// Other custom fields (preserve unknown fields)
|
||||
#[serde(flatten)]
|
||||
pub other: HashMap<String, Value>,
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -513,7 +514,7 @@ pub struct OpenClawToolsConfig {
|
||||
|
||||
/// Other custom fields (preserve unknown fields)
|
||||
#[serde(flatten)]
|
||||
pub other: HashMap<String, Value>,
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// Read the tools config section
|
||||
@@ -525,7 +526,7 @@ pub fn get_tools_config() -> Result<OpenClawToolsConfig, AppError> {
|
||||
profile: None,
|
||||
allow: Vec::new(),
|
||||
deny: Vec::new(),
|
||||
other: HashMap::new(),
|
||||
extra: HashMap::new(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -478,40 +478,9 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool
|
||||
"config": config_obj
|
||||
})
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode uses additive mode - import from live is not the same pattern
|
||||
// For now, return an empty config structure
|
||||
use crate::opencode_config::{get_opencode_config_path, read_opencode_config};
|
||||
|
||||
let config_path = get_opencode_config_path();
|
||||
if !config_path.exists() {
|
||||
return Err(AppError::localized(
|
||||
"opencode.live.missing",
|
||||
"OpenCode 配置文件不存在",
|
||||
"OpenCode configuration file is missing",
|
||||
));
|
||||
}
|
||||
|
||||
// For OpenCode, we return the full config - but note that OpenCode
|
||||
// uses additive mode, so importing defaults works differently
|
||||
read_opencode_config()?
|
||||
}
|
||||
AppType::OpenClaw => {
|
||||
// OpenClaw uses additive mode - import from live is not the same pattern
|
||||
use crate::openclaw_config::{get_openclaw_config_path, read_openclaw_config};
|
||||
|
||||
let config_path = get_openclaw_config_path();
|
||||
if !config_path.exists() {
|
||||
return Err(AppError::localized(
|
||||
"openclaw.live.missing",
|
||||
"OpenClaw 配置文件不存在",
|
||||
"OpenClaw configuration file is missing",
|
||||
));
|
||||
}
|
||||
|
||||
// For OpenClaw, we return the full config - but note that OpenClaw
|
||||
// uses additive mode, so importing defaults works differently
|
||||
read_openclaw_config()?
|
||||
// OpenCode and OpenClaw use additive mode and are handled by early return above
|
||||
AppType::OpenCode | AppType::OpenClaw => {
|
||||
unreachable!("additive mode apps are handled by early return")
|
||||
}
|
||||
};
|
||||
|
||||
@@ -762,6 +731,12 @@ pub fn import_openclaw_providers_from_live(state: &AppState) -> Result<usize, Ap
|
||||
pub fn remove_openclaw_provider_from_live(provider_id: &str) -> Result<(), AppError> {
|
||||
use crate::openclaw_config;
|
||||
|
||||
// Check if OpenClaw config directory exists
|
||||
if !openclaw_config::get_openclaw_dir().exists() {
|
||||
log::debug!("OpenClaw config directory doesn't exist, skipping removal of '{provider_id}'");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
openclaw_config::remove_provider(provider_id)?;
|
||||
log::info!("OpenClaw provider '{provider_id}' removed from live config");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user