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<String>` field for icon name
- Add `icon_color: Option<String>` 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.
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -616,6 +616,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"objc2-app-kit 0.2.2",
|
"objc2-app-kit 0.2.2",
|
||||||
|
"once_cell",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rquickjs",
|
"rquickjs",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ serde_yaml = "0.9"
|
|||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
auto-launch = "0.5"
|
auto-launch = "0.5"
|
||||||
|
once_cell = "1.21.3"
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies]
|
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies]
|
||||||
tauri-plugin-single-instance = "2"
|
tauri-plugin-single-instance = "2"
|
||||||
|
|||||||
@@ -319,6 +319,8 @@ requires_openai_auth = true
|
|||||||
sort_index: None,
|
sort_index: None,
|
||||||
notes: request.notes.clone(),
|
notes: request.notes.clone(),
|
||||||
meta: None,
|
meta: None,
|
||||||
|
icon: None,
|
||||||
|
icon_color: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(provider)
|
Ok(provider)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ mod mcp;
|
|||||||
mod prompt;
|
mod prompt;
|
||||||
mod prompt_files;
|
mod prompt_files;
|
||||||
mod provider;
|
mod provider;
|
||||||
|
mod provider_defaults;
|
||||||
mod services;
|
mod services;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod store;
|
mod store;
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ pub struct Provider {
|
|||||||
/// 供应商元数据(不写入 live 配置,仅存于 ~/.cc-switch/config.json)
|
/// 供应商元数据(不写入 live 配置,仅存于 ~/.cc-switch/config.json)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub meta: Option<ProviderMeta>,
|
pub meta: Option<ProviderMeta>,
|
||||||
|
/// 图标名称(如 "openai", "anthropic")
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub icon: Option<String>,
|
||||||
|
/// 图标颜色(Hex 格式,如 "#00A67E")
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "iconColor")]
|
||||||
|
pub icon_color: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Provider {
|
impl Provider {
|
||||||
@@ -48,6 +55,8 @@ impl Provider {
|
|||||||
sort_index: None,
|
sort_index: None,
|
||||||
notes: None,
|
notes: None,
|
||||||
meta: None,
|
meta: None,
|
||||||
|
icon: None,
|
||||||
|
icon_color: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
238
src-tauri/src/provider_defaults.rs
Normal file
238
src-tauri/src/provider_defaults.rs
Normal file
@@ -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<HashMap<&'static str, ProviderIcon>> = 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<ProviderIcon> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -450,7 +450,9 @@ impl SkillService {
|
|||||||
// 根据 skills_path 确定源目录路径
|
// 根据 skills_path 确定源目录路径
|
||||||
let source = if let Some(ref skills_path) = repo.skills_path {
|
let source = if let Some(ref skills_path) = repo.skills_path {
|
||||||
// 如果指定了 skills_path,源路径为: temp_dir/skills_path/directory
|
// 如果指定了 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 {
|
} else {
|
||||||
// 否则源路径为: temp_dir/directory
|
// 否则源路径为: temp_dir/directory
|
||||||
temp_dir.join(&directory)
|
temp_dir.join(&directory)
|
||||||
@@ -458,10 +460,7 @@ impl SkillService {
|
|||||||
|
|
||||||
if !source.exists() {
|
if !source.exists() {
|
||||||
let _ = fs::remove_dir_all(&temp_dir);
|
let _ = fs::remove_dir_all(&temp_dir);
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!("技能目录不存在: {}", source.display()));
|
||||||
"技能目录不存在: {}",
|
|
||||||
source.display()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除旧版本
|
// 删除旧版本
|
||||||
|
|||||||
Reference in New Issue
Block a user