feat: add skill storage location toggle between CC Switch and ~/.agents/skills

Allow users to choose between storing skills in CC Switch's managed
directory (~/.cc-switch/skills/) or the Agent Skills open standard
directory (~/.agents/skills/). Includes migration logic that safely
moves files before updating settings, with confirmation dialog for
non-empty installations.
This commit is contained in:
Jason
2026-04-05 20:21:51 +08:00
parent 6d220b2528
commit 8cfce8abfc
12 changed files with 383 additions and 5 deletions
+114 -2
View File
@@ -34,6 +34,17 @@ pub enum SyncMethod {
Copy,
}
/// Skill 存储位置(SSOT 目录选择)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SkillStorageLocation {
/// CC Switch 管理目录 (~/.cc-switch/skills/)
#[default]
CcSwitch,
/// Agent Skills 统一标准目录 (~/.agents/skills/)
Unified,
}
/// 可发现的技能(来自仓库)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoverableSkill {
@@ -174,6 +185,15 @@ pub struct SkillUpdateInfo {
pub remote_hash: String,
}
/// Skill 存储位置迁移结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MigrationResult {
pub migrated_count: usize,
pub skipped_count: usize,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillBackupEntry {
@@ -403,9 +423,20 @@ impl SkillService {
// ========== 路径管理 ==========
/// 获取 SSOT 目录(~/.cc-switch/skills/
/// 获取 SSOT 目录(根据设置返回 ~/.cc-switch/skills/ 或 ~/.agents/skills/
pub fn get_ssot_dir() -> Result<PathBuf> {
let dir = get_app_config_dir().join("skills");
let location = crate::settings::get_skill_storage_location();
let dir = match location {
SkillStorageLocation::CcSwitch => get_app_config_dir().join("skills"),
SkillStorageLocation::Unified => {
let home = dirs::home_dir().context(format_skill_error(
"GET_HOME_DIR_FAILED",
&[],
Some("checkPermission"),
))?;
home.join(".agents").join("skills")
}
};
fs::create_dir_all(&dir)?;
Ok(dir)
}
@@ -1052,6 +1083,87 @@ impl SkillService {
Ok(count)
}
/// 迁移 Skill 存储位置(在两个 SSOT 目录间移动文件)
///
/// 安全策略:先移文件,后改设置。中途崩溃时设置仍指向旧目录。
pub fn migrate_storage(
db: &Arc<Database>,
target: SkillStorageLocation,
) -> Result<MigrationResult> {
let current = crate::settings::get_skill_storage_location();
if current == target {
return Ok(MigrationResult {
migrated_count: 0,
skipped_count: 0,
errors: vec![],
});
}
// 1. 解析旧目录和新目录(不改设置)
let old_dir = Self::get_ssot_dir()?;
let new_dir = match target {
SkillStorageLocation::CcSwitch => get_app_config_dir().join("skills"),
SkillStorageLocation::Unified => {
let home = dirs::home_dir().context("Cannot determine home directory")?;
home.join(".agents").join("skills")
}
};
fs::create_dir_all(&new_dir)?;
// 2. 逐个移动 skill 目录
let skills = db.get_all_installed_skills()?;
let mut result = MigrationResult {
migrated_count: 0,
skipped_count: 0,
errors: vec![],
};
for skill in skills.values() {
let src = old_dir.join(&skill.directory);
let dst = new_dir.join(&skill.directory);
if !src.exists() {
result.skipped_count += 1;
continue;
}
if dst.exists() {
result.skipped_count += 1;
continue;
}
// 优先 rename(同文件系统原子操作),失败则 copy+delete
match fs::rename(&src, &dst) {
Ok(()) => result.migrated_count += 1,
Err(_) => match Self::copy_dir_recursive(&src, &dst) {
Ok(()) => {
let _ = fs::remove_dir_all(&src);
result.migrated_count += 1;
}
Err(e) => {
result.errors.push(format!("{}: {e}", skill.directory));
}
},
}
}
// 3. 文件移动完成后才持久化设置
crate::settings::set_skill_storage_location(target)?;
// 4. 刷新所有应用目录的 symlink(指向新 SSOT
for app in AppType::all() {
let _ = Self::sync_to_app(db, &app);
}
log::info!(
"Skill 存储迁移完成: {} 迁移, {} 跳过, {} 错误",
result.migrated_count,
result.skipped_count,
result.errors.len()
);
Ok(result)
}
pub fn list_backups() -> Result<Vec<SkillBackupEntry>> {
let backup_dir = Self::get_backup_dir()?;
let mut entries = Vec::new();