mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
333c9f277b
Introduce list/restore/delete commands for skill backups created during uninstall. Restore copies files back to SSOT, saves the DB record, and syncs to the current app with rollback on failure. Delete removes the backup directory after a confirmation dialog. ConfirmDialog gains a configurable zIndex prop to support nested dialog stacking.
293 lines
8.4 KiB
Rust
293 lines
8.4 KiB
Rust
//! Skills 命令层
|
||
//!
|
||
//! v3.10.0+ 统一管理架构:
|
||
//! - 支持三应用开关(Claude/Codex/Gemini)
|
||
//! - SSOT 存储在 ~/.cc-switch/skills/
|
||
|
||
use crate::app_config::{AppType, InstalledSkill, UnmanagedSkill};
|
||
use crate::error::format_skill_error;
|
||
use crate::services::skill::{
|
||
DiscoverableSkill, ImportSkillSelection, Skill, SkillBackupEntry, SkillRepo, SkillService,
|
||
SkillUninstallResult,
|
||
};
|
||
use crate::store::AppState;
|
||
use std::sync::Arc;
|
||
use tauri::State;
|
||
|
||
/// SkillService 状态包装
|
||
pub struct SkillServiceState(pub Arc<SkillService>);
|
||
|
||
/// 解析 app 参数为 AppType
|
||
fn parse_app_type(app: &str) -> Result<AppType, String> {
|
||
match app.to_lowercase().as_str() {
|
||
"claude" => Ok(AppType::Claude),
|
||
"codex" => Ok(AppType::Codex),
|
||
"gemini" => Ok(AppType::Gemini),
|
||
"opencode" => Ok(AppType::OpenCode),
|
||
_ => Err(format!("不支持的 app 类型: {app}")),
|
||
}
|
||
}
|
||
|
||
// ========== 统一管理命令 ==========
|
||
|
||
/// 获取所有已安装的 Skills
|
||
#[tauri::command]
|
||
pub fn get_installed_skills(app_state: State<'_, AppState>) -> Result<Vec<InstalledSkill>, String> {
|
||
SkillService::get_all_installed(&app_state.db).map_err(|e| e.to_string())
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub fn get_skill_backups() -> Result<Vec<SkillBackupEntry>, String> {
|
||
SkillService::list_backups().map_err(|e| e.to_string())
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub fn delete_skill_backup(backup_id: String) -> Result<bool, String> {
|
||
SkillService::delete_backup(&backup_id).map_err(|e| e.to_string())?;
|
||
Ok(true)
|
||
}
|
||
|
||
/// 安装 Skill(新版统一安装)
|
||
///
|
||
/// 参数:
|
||
/// - skill: 从发现列表获取的技能信息
|
||
/// - current_app: 当前选中的应用,安装后默认启用该应用
|
||
#[tauri::command]
|
||
pub async fn install_skill_unified(
|
||
skill: DiscoverableSkill,
|
||
current_app: String,
|
||
service: State<'_, SkillServiceState>,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<InstalledSkill, String> {
|
||
let app_type = parse_app_type(¤t_app)?;
|
||
|
||
service
|
||
.0
|
||
.install(&app_state.db, &skill, &app_type)
|
||
.await
|
||
.map_err(|e| e.to_string())
|
||
}
|
||
|
||
/// 卸载 Skill(新版统一卸载)
|
||
#[tauri::command]
|
||
pub fn uninstall_skill_unified(
|
||
id: String,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<SkillUninstallResult, String> {
|
||
SkillService::uninstall(&app_state.db, &id).map_err(|e| e.to_string())
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub fn restore_skill_backup(
|
||
backup_id: String,
|
||
current_app: String,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<InstalledSkill, String> {
|
||
let app_type = parse_app_type(¤t_app)?;
|
||
SkillService::restore_from_backup(&app_state.db, &backup_id, &app_type)
|
||
.map_err(|e| e.to_string())
|
||
}
|
||
|
||
/// 切换 Skill 的应用启用状态
|
||
#[tauri::command]
|
||
pub fn toggle_skill_app(
|
||
id: String,
|
||
app: String,
|
||
enabled: bool,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<bool, String> {
|
||
let app_type = parse_app_type(&app)?;
|
||
SkillService::toggle_app(&app_state.db, &id, &app_type, enabled).map_err(|e| e.to_string())?;
|
||
Ok(true)
|
||
}
|
||
|
||
/// 扫描未管理的 Skills
|
||
#[tauri::command]
|
||
pub fn scan_unmanaged_skills(
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<Vec<UnmanagedSkill>, String> {
|
||
SkillService::scan_unmanaged(&app_state.db).map_err(|e| e.to_string())
|
||
}
|
||
|
||
/// 从应用目录导入 Skills
|
||
#[tauri::command]
|
||
pub fn import_skills_from_apps(
|
||
imports: Vec<ImportSkillSelection>,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<Vec<InstalledSkill>, String> {
|
||
SkillService::import_from_apps(&app_state.db, imports).map_err(|e| e.to_string())
|
||
}
|
||
|
||
// ========== 发现功能命令 ==========
|
||
|
||
/// 发现可安装的 Skills(从仓库获取)
|
||
#[tauri::command]
|
||
pub async fn discover_available_skills(
|
||
service: State<'_, SkillServiceState>,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<Vec<DiscoverableSkill>, String> {
|
||
let repos = app_state.db.get_skill_repos().map_err(|e| e.to_string())?;
|
||
service
|
||
.0
|
||
.discover_available(repos)
|
||
.await
|
||
.map_err(|e| e.to_string())
|
||
}
|
||
|
||
// ========== 兼容旧 API 的命令 ==========
|
||
|
||
/// 获取技能列表(兼容旧 API)
|
||
#[tauri::command]
|
||
pub async fn get_skills(
|
||
service: State<'_, SkillServiceState>,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<Vec<Skill>, String> {
|
||
let repos = app_state.db.get_skill_repos().map_err(|e| e.to_string())?;
|
||
service
|
||
.0
|
||
.list_skills(repos, &app_state.db)
|
||
.await
|
||
.map_err(|e| e.to_string())
|
||
}
|
||
|
||
/// 获取指定应用的技能列表(兼容旧 API)
|
||
#[tauri::command]
|
||
pub async fn get_skills_for_app(
|
||
app: String,
|
||
service: State<'_, SkillServiceState>,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<Vec<Skill>, String> {
|
||
// 新版本不再区分应用,统一返回所有技能
|
||
let _ = parse_app_type(&app)?; // 验证 app 参数有效
|
||
get_skills(service, app_state).await
|
||
}
|
||
|
||
/// 安装技能(兼容旧 API)
|
||
#[tauri::command]
|
||
pub async fn install_skill(
|
||
directory: String,
|
||
service: State<'_, SkillServiceState>,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<bool, String> {
|
||
install_skill_for_app("claude".to_string(), directory, service, app_state).await
|
||
}
|
||
|
||
/// 安装指定应用的技能(兼容旧 API)
|
||
#[tauri::command]
|
||
pub async fn install_skill_for_app(
|
||
app: String,
|
||
directory: String,
|
||
service: State<'_, SkillServiceState>,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<bool, String> {
|
||
let app_type = parse_app_type(&app)?;
|
||
|
||
// 先获取技能信息
|
||
let repos = app_state.db.get_skill_repos().map_err(|e| e.to_string())?;
|
||
let skills = service
|
||
.0
|
||
.discover_available(repos)
|
||
.await
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
let skill = skills
|
||
.into_iter()
|
||
.find(|s| {
|
||
let install_name = std::path::Path::new(&s.directory)
|
||
.file_name()
|
||
.map(|n| n.to_string_lossy().to_string())
|
||
.unwrap_or_else(|| s.directory.clone());
|
||
install_name.eq_ignore_ascii_case(&directory)
|
||
|| s.directory.eq_ignore_ascii_case(&directory)
|
||
})
|
||
.ok_or_else(|| {
|
||
format_skill_error(
|
||
"SKILL_NOT_FOUND",
|
||
&[("directory", &directory)],
|
||
Some("checkRepoUrl"),
|
||
)
|
||
})?;
|
||
|
||
service
|
||
.0
|
||
.install(&app_state.db, &skill, &app_type)
|
||
.await
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
Ok(true)
|
||
}
|
||
|
||
/// 卸载技能(兼容旧 API)
|
||
#[tauri::command]
|
||
pub fn uninstall_skill(
|
||
directory: String,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<SkillUninstallResult, String> {
|
||
uninstall_skill_for_app("claude".to_string(), directory, app_state)
|
||
}
|
||
|
||
/// 卸载指定应用的技能(兼容旧 API)
|
||
#[tauri::command]
|
||
pub fn uninstall_skill_for_app(
|
||
app: String,
|
||
directory: String,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<SkillUninstallResult, String> {
|
||
let _ = parse_app_type(&app)?; // 验证参数
|
||
|
||
// 通过 directory 找到对应的 skill id
|
||
let skills = SkillService::get_all_installed(&app_state.db).map_err(|e| e.to_string())?;
|
||
|
||
let skill = skills
|
||
.into_iter()
|
||
.find(|s| s.directory.eq_ignore_ascii_case(&directory))
|
||
.ok_or_else(|| format!("未找到已安装的 Skill: {directory}"))?;
|
||
|
||
SkillService::uninstall(&app_state.db, &skill.id).map_err(|e| e.to_string())
|
||
}
|
||
|
||
// ========== 仓库管理命令 ==========
|
||
|
||
/// 获取技能仓库列表
|
||
#[tauri::command]
|
||
pub fn get_skill_repos(app_state: State<'_, AppState>) -> Result<Vec<SkillRepo>, String> {
|
||
app_state.db.get_skill_repos().map_err(|e| e.to_string())
|
||
}
|
||
|
||
/// 添加技能仓库
|
||
#[tauri::command]
|
||
pub fn add_skill_repo(repo: SkillRepo, app_state: State<'_, AppState>) -> Result<bool, String> {
|
||
app_state
|
||
.db
|
||
.save_skill_repo(&repo)
|
||
.map_err(|e| e.to_string())?;
|
||
Ok(true)
|
||
}
|
||
|
||
/// 删除技能仓库
|
||
#[tauri::command]
|
||
pub fn remove_skill_repo(
|
||
owner: String,
|
||
name: String,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<bool, String> {
|
||
app_state
|
||
.db
|
||
.delete_skill_repo(&owner, &name)
|
||
.map_err(|e| e.to_string())?;
|
||
Ok(true)
|
||
}
|
||
|
||
/// 从 ZIP 文件安装 Skills
|
||
#[tauri::command]
|
||
pub fn install_skills_from_zip(
|
||
file_path: String,
|
||
current_app: String,
|
||
app_state: State<'_, AppState>,
|
||
) -> Result<Vec<InstalledSkill>, String> {
|
||
let app_type = parse_app_type(¤t_app)?;
|
||
let path = std::path::Path::new(&file_path);
|
||
|
||
SkillService::install_from_zip(&app_state.db, path, &app_type).map_err(|e| e.to_string())
|
||
}
|