mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
7097a0d710
Skills import previously inferred app enablement from filesystem presence, causing incorrect multi-app activation when the same skill directory existed under multiple app paths. Now the frontend submits explicit app selections via ImportSkillSelection, and schema migration preserves a snapshot of legacy app mappings to avoid lossy reconstruction. Also adds reconciliation to sync_to_app (removes disabled/orphaned symlinks) and MCP sync_all_enabled (removes disabled servers from live config).
385 lines
14 KiB
Rust
385 lines
14 KiB
Rust
use indexmap::IndexMap;
|
||
use std::collections::HashMap;
|
||
|
||
use crate::app_config::{AppType, McpServer};
|
||
use crate::error::AppError;
|
||
use crate::mcp;
|
||
use crate::store::AppState;
|
||
|
||
/// MCP 相关业务逻辑(v3.7.0 统一结构)
|
||
pub struct McpService;
|
||
|
||
impl McpService {
|
||
/// 获取所有 MCP 服务器(统一结构)
|
||
pub fn get_all_servers(state: &AppState) -> Result<IndexMap<String, McpServer>, AppError> {
|
||
state.db.get_all_mcp_servers()
|
||
}
|
||
|
||
/// 添加或更新 MCP 服务器
|
||
pub fn upsert_server(state: &AppState, server: McpServer) -> Result<(), AppError> {
|
||
// 读取旧状态:用于处理“编辑时取消勾选某个应用”的场景(需要从对应 live 配置中移除)
|
||
let prev_apps = state
|
||
.db
|
||
.get_all_mcp_servers()?
|
||
.get(&server.id)
|
||
.map(|s| s.apps.clone())
|
||
.unwrap_or_default();
|
||
|
||
state.db.save_mcp_server(&server)?;
|
||
|
||
// 处理禁用:若旧版本启用但新版本取消,则需要从该应用的 live 配置移除
|
||
if prev_apps.claude && !server.apps.claude {
|
||
Self::remove_server_from_app(state, &server.id, &AppType::Claude)?;
|
||
}
|
||
if prev_apps.codex && !server.apps.codex {
|
||
Self::remove_server_from_app(state, &server.id, &AppType::Codex)?;
|
||
}
|
||
if prev_apps.gemini && !server.apps.gemini {
|
||
Self::remove_server_from_app(state, &server.id, &AppType::Gemini)?;
|
||
}
|
||
if prev_apps.opencode && !server.apps.opencode {
|
||
Self::remove_server_from_app(state, &server.id, &AppType::OpenCode)?;
|
||
}
|
||
|
||
// 同步到各个启用的应用
|
||
Self::sync_server_to_apps(state, &server)?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 删除 MCP 服务器
|
||
pub fn delete_server(state: &AppState, id: &str) -> Result<bool, AppError> {
|
||
let server = state.db.get_all_mcp_servers()?.shift_remove(id);
|
||
|
||
if let Some(server) = server {
|
||
state.db.delete_mcp_server(id)?;
|
||
|
||
// 从所有应用的 live 配置中移除
|
||
Self::remove_server_from_all_apps(state, id, &server)?;
|
||
Ok(true)
|
||
} else {
|
||
Ok(false)
|
||
}
|
||
}
|
||
|
||
/// 切换指定应用的启用状态
|
||
pub fn toggle_app(
|
||
state: &AppState,
|
||
server_id: &str,
|
||
app: AppType,
|
||
enabled: bool,
|
||
) -> Result<(), AppError> {
|
||
let mut servers = state.db.get_all_mcp_servers()?;
|
||
|
||
if let Some(server) = servers.get_mut(server_id) {
|
||
server.apps.set_enabled_for(&app, enabled);
|
||
state.db.save_mcp_server(server)?;
|
||
|
||
// 同步到对应应用
|
||
if enabled {
|
||
Self::sync_server_to_app(state, server, &app)?;
|
||
} else {
|
||
Self::remove_server_from_app(state, server_id, &app)?;
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 将 MCP 服务器同步到所有启用的应用
|
||
fn sync_server_to_apps(_state: &AppState, server: &McpServer) -> Result<(), AppError> {
|
||
for app in server.apps.enabled_apps() {
|
||
Self::sync_server_to_app_no_config(server, &app)?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 将 MCP 服务器同步到指定应用
|
||
fn sync_server_to_app(
|
||
_state: &AppState,
|
||
server: &McpServer,
|
||
app: &AppType,
|
||
) -> Result<(), AppError> {
|
||
Self::sync_server_to_app_no_config(server, app)
|
||
}
|
||
|
||
fn sync_server_to_app_no_config(server: &McpServer, app: &AppType) -> Result<(), AppError> {
|
||
match app {
|
||
AppType::Claude => {
|
||
mcp::sync_single_server_to_claude(&Default::default(), &server.id, &server.server)?;
|
||
}
|
||
AppType::Codex => {
|
||
// Codex uses TOML format, must use the correct function
|
||
mcp::sync_single_server_to_codex(&Default::default(), &server.id, &server.server)?;
|
||
}
|
||
AppType::Gemini => {
|
||
mcp::sync_single_server_to_gemini(&Default::default(), &server.id, &server.server)?;
|
||
}
|
||
AppType::OpenCode => {
|
||
mcp::sync_single_server_to_opencode(
|
||
&Default::default(),
|
||
&server.id,
|
||
&server.server,
|
||
)?;
|
||
}
|
||
AppType::OpenClaw => {
|
||
// OpenClaw MCP support is still in development (Issue #4834)
|
||
// Skip for now
|
||
log::debug!("OpenClaw MCP support is still in development, skipping sync");
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// 从所有曾启用过该服务器的应用中移除
|
||
fn remove_server_from_all_apps(
|
||
state: &AppState,
|
||
id: &str,
|
||
server: &McpServer,
|
||
) -> Result<(), AppError> {
|
||
// 从所有曾启用的应用中移除
|
||
for app in server.apps.enabled_apps() {
|
||
Self::remove_server_from_app(state, id, &app)?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn remove_server_from_app(_state: &AppState, id: &str, app: &AppType) -> Result<(), AppError> {
|
||
match app {
|
||
AppType::Claude => mcp::remove_server_from_claude(id)?,
|
||
AppType::Codex => mcp::remove_server_from_codex(id)?,
|
||
AppType::Gemini => mcp::remove_server_from_gemini(id)?,
|
||
AppType::OpenCode => {
|
||
mcp::remove_server_from_opencode(id)?;
|
||
}
|
||
AppType::OpenClaw => {
|
||
// OpenClaw MCP support is still in development
|
||
log::debug!("OpenClaw MCP support is still in development, skipping remove");
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// 手动同步所有启用的 MCP 服务器到对应的应用
|
||
pub fn sync_all_enabled(state: &AppState) -> Result<(), AppError> {
|
||
let servers = Self::get_all_servers(state)?;
|
||
|
||
for app in AppType::all() {
|
||
if matches!(app, AppType::OpenClaw) {
|
||
continue;
|
||
}
|
||
|
||
for server in servers.values() {
|
||
if server.apps.is_enabled_for(&app) {
|
||
Self::sync_server_to_app(state, server, &app)?;
|
||
} else {
|
||
Self::remove_server_from_app(state, &server.id, &app)?;
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// ========================================================================
|
||
// 兼容层:支持旧的 v3.6.x 命令(已废弃,将在 v4.0 移除)
|
||
// ========================================================================
|
||
|
||
/// [已废弃] 获取指定应用的 MCP 服务器(兼容旧 API)
|
||
#[deprecated(since = "3.7.0", note = "Use get_all_servers instead")]
|
||
pub fn get_servers(
|
||
state: &AppState,
|
||
app: AppType,
|
||
) -> Result<HashMap<String, serde_json::Value>, AppError> {
|
||
let all_servers = Self::get_all_servers(state)?;
|
||
let mut result = HashMap::new();
|
||
|
||
for (id, server) in all_servers {
|
||
if server.apps.is_enabled_for(&app) {
|
||
result.insert(id, server.server);
|
||
}
|
||
}
|
||
|
||
Ok(result)
|
||
}
|
||
|
||
/// [已废弃] 设置 MCP 服务器在指定应用的启用状态(兼容旧 API)
|
||
#[deprecated(since = "3.7.0", note = "Use toggle_app instead")]
|
||
pub fn set_enabled(
|
||
state: &AppState,
|
||
app: AppType,
|
||
id: &str,
|
||
enabled: bool,
|
||
) -> Result<bool, AppError> {
|
||
Self::toggle_app(state, id, app, enabled)?;
|
||
Ok(true)
|
||
}
|
||
|
||
/// [已废弃] 同步启用的 MCP 到指定应用(兼容旧 API)
|
||
#[deprecated(since = "3.7.0", note = "Use sync_all_enabled instead")]
|
||
pub fn sync_enabled(state: &AppState, app: AppType) -> Result<(), AppError> {
|
||
let servers = Self::get_all_servers(state)?;
|
||
|
||
for server in servers.values() {
|
||
if server.apps.is_enabled_for(&app) {
|
||
Self::sync_server_to_app(state, server, &app)?;
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 从 Claude 导入 MCP(v3.7.0 已更新为统一结构)
|
||
pub fn import_from_claude(state: &AppState) -> Result<usize, AppError> {
|
||
// 创建临时 MultiAppConfig 用于导入
|
||
let mut temp_config = crate::app_config::MultiAppConfig::default();
|
||
|
||
// 调用原有的导入逻辑(从 mcp.rs)
|
||
let count = crate::mcp::import_from_claude(&mut temp_config)?;
|
||
|
||
let mut new_count = 0;
|
||
|
||
// 如果有导入的服务器,保存到数据库
|
||
if count > 0 {
|
||
if let Some(servers) = &temp_config.mcp.servers {
|
||
let mut existing = state.db.get_all_mcp_servers()?;
|
||
for server in servers.values() {
|
||
// 已存在:仅启用 Claude,不覆盖其他字段(与导入模块语义保持一致)
|
||
let to_save = if let Some(existing_server) = existing.get(&server.id) {
|
||
let mut merged = existing_server.clone();
|
||
merged.apps.claude = true;
|
||
merged
|
||
} else {
|
||
// 真正的新服务器
|
||
new_count += 1;
|
||
server.clone()
|
||
};
|
||
|
||
state.db.save_mcp_server(&to_save)?;
|
||
existing.insert(to_save.id.clone(), to_save.clone());
|
||
|
||
// 同步到对应应用 live 配置
|
||
Self::sync_server_to_apps(state, &to_save)?;
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(new_count)
|
||
}
|
||
|
||
/// 从 Codex 导入 MCP(v3.7.0 已更新为统一结构)
|
||
pub fn import_from_codex(state: &AppState) -> Result<usize, AppError> {
|
||
// 创建临时 MultiAppConfig 用于导入
|
||
let mut temp_config = crate::app_config::MultiAppConfig::default();
|
||
|
||
// 调用原有的导入逻辑(从 mcp.rs)
|
||
let count = crate::mcp::import_from_codex(&mut temp_config)?;
|
||
|
||
let mut new_count = 0;
|
||
|
||
// 如果有导入的服务器,保存到数据库
|
||
if count > 0 {
|
||
if let Some(servers) = &temp_config.mcp.servers {
|
||
let mut existing = state.db.get_all_mcp_servers()?;
|
||
for server in servers.values() {
|
||
// 已存在:仅启用 Codex,不覆盖其他字段(与导入模块语义保持一致)
|
||
let to_save = if let Some(existing_server) = existing.get(&server.id) {
|
||
let mut merged = existing_server.clone();
|
||
merged.apps.codex = true;
|
||
merged
|
||
} else {
|
||
// 真正的新服务器
|
||
new_count += 1;
|
||
server.clone()
|
||
};
|
||
|
||
state.db.save_mcp_server(&to_save)?;
|
||
existing.insert(to_save.id.clone(), to_save.clone());
|
||
|
||
// 同步到对应应用 live 配置
|
||
Self::sync_server_to_apps(state, &to_save)?;
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(new_count)
|
||
}
|
||
|
||
/// 从 Gemini 导入 MCP(v3.7.0 已更新为统一结构)
|
||
pub fn import_from_gemini(state: &AppState) -> Result<usize, AppError> {
|
||
// 创建临时 MultiAppConfig 用于导入
|
||
let mut temp_config = crate::app_config::MultiAppConfig::default();
|
||
|
||
// 调用原有的导入逻辑(从 mcp.rs)
|
||
let count = crate::mcp::import_from_gemini(&mut temp_config)?;
|
||
|
||
let mut new_count = 0;
|
||
|
||
// 如果有导入的服务器,保存到数据库
|
||
if count > 0 {
|
||
if let Some(servers) = &temp_config.mcp.servers {
|
||
let mut existing = state.db.get_all_mcp_servers()?;
|
||
for server in servers.values() {
|
||
// 已存在:仅启用 Gemini,不覆盖其他字段(与导入模块语义保持一致)
|
||
let to_save = if let Some(existing_server) = existing.get(&server.id) {
|
||
let mut merged = existing_server.clone();
|
||
merged.apps.gemini = true;
|
||
merged
|
||
} else {
|
||
// 真正的新服务器
|
||
new_count += 1;
|
||
server.clone()
|
||
};
|
||
|
||
state.db.save_mcp_server(&to_save)?;
|
||
existing.insert(to_save.id.clone(), to_save.clone());
|
||
|
||
// 同步到对应应用 live 配置
|
||
Self::sync_server_to_apps(state, &to_save)?;
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(new_count)
|
||
}
|
||
|
||
/// 从 OpenCode 导入 MCP(v3.9.2+ 新增)
|
||
pub fn import_from_opencode(state: &AppState) -> Result<usize, AppError> {
|
||
// 创建临时 MultiAppConfig 用于导入
|
||
let mut temp_config = crate::app_config::MultiAppConfig::default();
|
||
|
||
// 调用原有的导入逻辑(从 mcp/opencode.rs)
|
||
let count = crate::mcp::import_from_opencode(&mut temp_config)?;
|
||
|
||
let mut new_count = 0;
|
||
|
||
// 如果有导入的服务器,保存到数据库
|
||
if count > 0 {
|
||
if let Some(servers) = &temp_config.mcp.servers {
|
||
let mut existing = state.db.get_all_mcp_servers()?;
|
||
for server in servers.values() {
|
||
// 已存在:仅启用 OpenCode,不覆盖其他字段(与导入模块语义保持一致)
|
||
let to_save = if let Some(existing_server) = existing.get(&server.id) {
|
||
let mut merged = existing_server.clone();
|
||
merged.apps.opencode = true;
|
||
merged
|
||
} else {
|
||
// 真正的新服务器
|
||
new_count += 1;
|
||
server.clone()
|
||
};
|
||
|
||
state.db.save_mcp_server(&to_save)?;
|
||
existing.insert(to_save.id.clone(), to_save.clone());
|
||
|
||
// 同步到对应应用 live 配置
|
||
Self::sync_server_to_apps(state, &to_save)?;
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(new_count)
|
||
}
|
||
}
|