From 0c1d94e57bcf916040f7d494e283f5fb7f3be8c1 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Fri, 21 Nov 2025 23:22:51 +0800 Subject: [PATCH] feat(backend): add icon fields to Provider model and default mappings Extend Rust backend to support provider icon customization: ## Provider Model (src-tauri/src/provider.rs) - Add `icon: Option` field for icon name - Add `icon_color: Option` field for hex color - Use serde rename `iconColor` for frontend compatibility - Apply skip_serializing_if for clean JSON output - Update Provider::new() to initialize icon fields as None ## Provider Defaults (src-tauri/src/provider_defaults.rs) [NEW] - Define ProviderIcon struct with name and color fields - Create DEFAULT_PROVIDER_ICONS static HashMap with 23 providers: - AI providers: OpenAI, Anthropic, Claude, Google, Gemini, DeepSeek, Kimi, Moonshot, Zhipu, MiniMax, Baidu, Alibaba, Tencent, Meta, Microsoft, Cohere, Perplexity, Mistral, HuggingFace - Cloud platforms: AWS, Azure, Huawei, Cloudflare - Implement infer_provider_icon() with exact and fuzzy matching - Add unit tests for matching logic (exact, fuzzy, case-insensitive) ## Deep Link Support (src-tauri/src/deeplink.rs) - Initialize icon fields when creating Provider from deep link import ## Module Registration (src-tauri/src/lib.rs) - Register provider_defaults module ## Dependencies (Cargo.toml) - Add once_cell for lazy static initialization This backend support enables icon persistence and future features like auto-icon inference during provider creation. --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/deeplink.rs | 2 + src-tauri/src/lib.rs | 1 + src-tauri/src/provider.rs | 9 ++ src-tauri/src/provider_defaults.rs | 238 +++++++++++++++++++++++++++++ src-tauri/src/services/skill.rs | 9 +- 7 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 src-tauri/src/provider_defaults.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c8b8f84..3ae2f08 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -616,6 +616,7 @@ dependencies = [ "log", "objc2 0.5.2", "objc2-app-kit 0.2.2", + "once_cell", "regex", "reqwest", "rquickjs", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6656469..f7043da 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -49,6 +49,7 @@ serde_yaml = "0.9" tempfile = "3" url = "2.5" auto-launch = "0.5" +once_cell = "1.21.3" [target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies] tauri-plugin-single-instance = "2" diff --git a/src-tauri/src/deeplink.rs b/src-tauri/src/deeplink.rs index b6d062f..8b7cf05 100644 --- a/src-tauri/src/deeplink.rs +++ b/src-tauri/src/deeplink.rs @@ -319,6 +319,8 @@ requires_openai_auth = true sort_index: None, notes: request.notes.clone(), meta: None, + icon: None, + icon_color: None, }; Ok(provider) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1da4a09..acc4658 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -15,6 +15,7 @@ mod mcp; mod prompt; mod prompt_files; mod provider; +mod provider_defaults; mod services; mod settings; mod store; diff --git a/src-tauri/src/provider.rs b/src-tauri/src/provider.rs index e3b6298..2d753b5 100644 --- a/src-tauri/src/provider.rs +++ b/src-tauri/src/provider.rs @@ -28,6 +28,13 @@ pub struct Provider { /// 供应商元数据(不写入 live 配置,仅存于 ~/.cc-switch/config.json) #[serde(skip_serializing_if = "Option::is_none")] pub meta: Option, + /// 图标名称(如 "openai", "anthropic") + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + /// 图标颜色(Hex 格式,如 "#00A67E") + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "iconColor")] + pub icon_color: Option, } impl Provider { @@ -48,6 +55,8 @@ impl Provider { sort_index: None, notes: None, meta: None, + icon: None, + icon_color: None, } } } diff --git a/src-tauri/src/provider_defaults.rs b/src-tauri/src/provider_defaults.rs new file mode 100644 index 0000000..3fb2ad0 --- /dev/null +++ b/src-tauri/src/provider_defaults.rs @@ -0,0 +1,238 @@ +use once_cell::sync::Lazy; +use std::collections::HashMap; + +/// 供应商图标信息 +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct ProviderIcon { + pub name: &'static str, + pub color: &'static str, +} + +/// 供应商名称到图标的默认映射 +#[allow(dead_code)] +pub static DEFAULT_PROVIDER_ICONS: Lazy> = Lazy::new(|| { + let mut m = HashMap::new(); + + // AI 服务商 + m.insert( + "openai", + ProviderIcon { + name: "openai", + color: "#00A67E", + }, + ); + m.insert( + "anthropic", + ProviderIcon { + name: "anthropic", + color: "#D4915D", + }, + ); + m.insert( + "claude", + ProviderIcon { + name: "claude", + color: "#D4915D", + }, + ); + m.insert( + "google", + ProviderIcon { + name: "google", + color: "#4285F4", + }, + ); + m.insert( + "gemini", + ProviderIcon { + name: "gemini", + color: "#4285F4", + }, + ); + m.insert( + "deepseek", + ProviderIcon { + name: "deepseek", + color: "#1E88E5", + }, + ); + m.insert( + "kimi", + ProviderIcon { + name: "kimi", + color: "#6366F1", + }, + ); + m.insert( + "moonshot", + ProviderIcon { + name: "moonshot", + color: "#6366F1", + }, + ); + m.insert( + "zhipu", + ProviderIcon { + name: "zhipu", + color: "#0F62FE", + }, + ); + m.insert( + "minimax", + ProviderIcon { + name: "minimax", + color: "#FF6B6B", + }, + ); + m.insert( + "baidu", + ProviderIcon { + name: "baidu", + color: "#2932E1", + }, + ); + m.insert( + "alibaba", + ProviderIcon { + name: "alibaba", + color: "#FF6A00", + }, + ); + m.insert( + "tencent", + ProviderIcon { + name: "tencent", + color: "#00A4FF", + }, + ); + m.insert( + "meta", + ProviderIcon { + name: "meta", + color: "#0081FB", + }, + ); + m.insert( + "microsoft", + ProviderIcon { + name: "microsoft", + color: "#00A4EF", + }, + ); + m.insert( + "cohere", + ProviderIcon { + name: "cohere", + color: "#39594D", + }, + ); + m.insert( + "perplexity", + ProviderIcon { + name: "perplexity", + color: "#20808D", + }, + ); + m.insert( + "mistral", + ProviderIcon { + name: "mistral", + color: "#FF7000", + }, + ); + m.insert( + "huggingface", + ProviderIcon { + name: "huggingface", + color: "#FFD21E", + }, + ); + + // 云平台 + m.insert( + "aws", + ProviderIcon { + name: "aws", + color: "#FF9900", + }, + ); + m.insert( + "azure", + ProviderIcon { + name: "azure", + color: "#0078D4", + }, + ); + m.insert( + "huawei", + ProviderIcon { + name: "huawei", + color: "#FF0000", + }, + ); + m.insert( + "cloudflare", + ProviderIcon { + name: "cloudflare", + color: "#F38020", + }, + ); + + m +}); + +/// 根据供应商名称智能推断图标 +#[allow(dead_code)] +pub fn infer_provider_icon(provider_name: &str) -> Option { + let name_lower = provider_name.to_lowercase(); + + // 精确匹配 + if let Some(icon) = DEFAULT_PROVIDER_ICONS.get(name_lower.as_str()) { + return Some(icon.clone()); + } + + // 模糊匹配(包含关键词) + for (key, icon) in DEFAULT_PROVIDER_ICONS.iter() { + if name_lower.contains(key) { + return Some(icon.clone()); + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_exact_match() { + let icon = infer_provider_icon("openai"); + assert!(icon.is_some()); + let icon = icon.unwrap(); + assert_eq!(icon.name, "openai"); + assert_eq!(icon.color, "#00A67E"); + } + + #[test] + fn test_fuzzy_match() { + let icon = infer_provider_icon("OpenAI Official"); + assert!(icon.is_some()); + let icon = icon.unwrap(); + assert_eq!(icon.name, "openai"); + } + + #[test] + fn test_case_insensitive() { + let icon = infer_provider_icon("ANTHROPIC"); + assert!(icon.is_some()); + assert_eq!(icon.unwrap().name, "anthropic"); + } + + #[test] + fn test_no_match() { + let icon = infer_provider_icon("unknown provider"); + assert!(icon.is_none()); + } +} diff --git a/src-tauri/src/services/skill.rs b/src-tauri/src/services/skill.rs index f32a186..670053e 100644 --- a/src-tauri/src/services/skill.rs +++ b/src-tauri/src/services/skill.rs @@ -450,7 +450,9 @@ impl SkillService { // 根据 skills_path 确定源目录路径 let source = if let Some(ref skills_path) = repo.skills_path { // 如果指定了 skills_path,源路径为: temp_dir/skills_path/directory - temp_dir.join(skills_path.trim_matches('/')).join(&directory) + temp_dir + .join(skills_path.trim_matches('/')) + .join(&directory) } else { // 否则源路径为: temp_dir/directory temp_dir.join(&directory) @@ -458,10 +460,7 @@ impl SkillService { if !source.exists() { let _ = fs::remove_dir_all(&temp_dir); - return Err(anyhow::anyhow!( - "技能目录不存在: {}", - source.display() - )); + return Err(anyhow::anyhow!("技能目录不存在: {}", source.display())); } // 删除旧版本