Skip to main content

cc_switch_lib/database/dao/
skills.rs

1//! Skills 数据访问对象
2//!
3//! 提供 Skills 和 Skill Repos 的 CRUD 操作。
4//!
5//! v3.10.0+ 统一管理架构:
6//! - Skills 使用统一的 id 主键,支持四应用启用标志
7//! - 实际文件存储在 ~/.cc-switch-tui/skills/,同步到各应用目录
8
9use crate::app_config::{InstalledSkill, SkillApps};
10use crate::database::{lock_conn, Database};
11use crate::error::AppError;
12use crate::services::skill::SkillRepo;
13use indexmap::IndexMap;
14use rusqlite::params;
15
16impl Database {
17    // ========== InstalledSkill CRUD ==========
18
19    /// 获取所有已安装的 Skills
20    pub fn get_all_installed_skills(&self) -> Result<IndexMap<String, InstalledSkill>, AppError> {
21        let conn = lock_conn!(self.conn);
22        let mut stmt = conn
23            .prepare(
24                "SELECT id, name, description, directory, repo_owner, repo_name, repo_branch,
25                        readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_hermes, installed_at
26                 FROM skills ORDER BY name ASC",
27            )
28            .map_err(|e| AppError::Database(e.to_string()))?;
29
30        let skill_iter = stmt
31            .query_map([], |row| {
32                Ok(InstalledSkill {
33                    id: row.get(0)?,
34                    name: row.get(1)?,
35                    description: row.get(2)?,
36                    directory: row.get(3)?,
37                    repo_owner: row.get(4)?,
38                    repo_name: row.get(5)?,
39                    repo_branch: row.get(6)?,
40                    readme_url: row.get(7)?,
41                    apps: SkillApps {
42                        claude: row.get(8)?,
43                        codex: row.get(9)?,
44                        gemini: row.get(10)?,
45                        opencode: row.get(11)?,
46                        hermes: row.get(12)?,
47                    },
48                    installed_at: row.get(13)?,
49                })
50            })
51            .map_err(|e| AppError::Database(e.to_string()))?;
52
53        let mut skills = IndexMap::new();
54        for skill_res in skill_iter {
55            let skill = skill_res.map_err(|e| AppError::Database(e.to_string()))?;
56            skills.insert(skill.id.clone(), skill);
57        }
58        Ok(skills)
59    }
60
61    /// 获取单个已安装的 Skill
62    pub fn get_installed_skill(&self, id: &str) -> Result<Option<InstalledSkill>, AppError> {
63        let conn = lock_conn!(self.conn);
64        let mut stmt = conn
65            .prepare(
66                "SELECT id, name, description, directory, repo_owner, repo_name, repo_branch,
67                        readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_hermes, installed_at
68                 FROM skills WHERE id = ?1",
69            )
70            .map_err(|e| AppError::Database(e.to_string()))?;
71
72        let result = stmt.query_row([id], |row| {
73            Ok(InstalledSkill {
74                id: row.get(0)?,
75                name: row.get(1)?,
76                description: row.get(2)?,
77                directory: row.get(3)?,
78                repo_owner: row.get(4)?,
79                repo_name: row.get(5)?,
80                repo_branch: row.get(6)?,
81                readme_url: row.get(7)?,
82                apps: SkillApps {
83                    claude: row.get(8)?,
84                    codex: row.get(9)?,
85                    gemini: row.get(10)?,
86                    opencode: row.get(11)?,
87                    hermes: row.get(12)?,
88                },
89                installed_at: row.get(13)?,
90            })
91        });
92
93        match result {
94            Ok(skill) => Ok(Some(skill)),
95            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
96            Err(e) => Err(AppError::Database(e.to_string())),
97        }
98    }
99
100    /// 保存 Skill(添加或更新)
101    pub fn save_skill(&self, skill: &InstalledSkill) -> Result<(), AppError> {
102        let conn = lock_conn!(self.conn);
103        conn.execute(
104            "INSERT OR REPLACE INTO skills
105             (id, name, description, directory, repo_owner, repo_name, repo_branch,
106              readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_hermes, installed_at)
107             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
108            params![
109                skill.id,
110                skill.name,
111                skill.description,
112                skill.directory,
113                skill.repo_owner,
114                skill.repo_name,
115                skill.repo_branch,
116                skill.readme_url,
117                skill.apps.claude,
118                skill.apps.codex,
119                skill.apps.gemini,
120                skill.apps.opencode,
121                skill.apps.hermes,
122                skill.installed_at,
123            ],
124        )
125        .map_err(|e| AppError::Database(e.to_string()))?;
126        Ok(())
127    }
128
129    /// 删除 Skill
130    pub fn delete_skill(&self, id: &str) -> Result<bool, AppError> {
131        let conn = lock_conn!(self.conn);
132        let affected = conn
133            .execute("DELETE FROM skills WHERE id = ?1", params![id])
134            .map_err(|e| AppError::Database(e.to_string()))?;
135        Ok(affected > 0)
136    }
137
138    /// 清空所有 Skills(用于迁移)
139    pub fn clear_skills(&self) -> Result<(), AppError> {
140        let conn = lock_conn!(self.conn);
141        conn.execute("DELETE FROM skills", [])
142            .map_err(|e| AppError::Database(e.to_string()))?;
143        Ok(())
144    }
145
146    /// 更新 Skill 的应用启用状态
147    pub fn update_skill_apps(&self, id: &str, apps: &SkillApps) -> Result<bool, AppError> {
148        let conn = lock_conn!(self.conn);
149        let affected = conn
150            .execute(
151                "UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4, enabled_hermes = ?5 WHERE id = ?6",
152                params![apps.claude, apps.codex, apps.gemini, apps.opencode, apps.hermes, id],
153            )
154            .map_err(|e| AppError::Database(e.to_string()))?;
155        Ok(affected > 0)
156    }
157
158    // ========== SkillRepo CRUD(保持原有) ==========
159
160    /// 获取所有 Skill 仓库
161    pub fn get_skill_repos(&self) -> Result<Vec<SkillRepo>, AppError> {
162        let conn = lock_conn!(self.conn);
163        let mut stmt = conn
164            .prepare(
165                "SELECT owner, name, branch, enabled FROM skill_repos ORDER BY owner ASC, name ASC",
166            )
167            .map_err(|e| AppError::Database(e.to_string()))?;
168
169        let repo_iter = stmt
170            .query_map([], |row| {
171                Ok(SkillRepo {
172                    owner: row.get(0)?,
173                    name: row.get(1)?,
174                    branch: row.get(2)?,
175                    enabled: row.get(3)?,
176                })
177            })
178            .map_err(|e| AppError::Database(e.to_string()))?;
179
180        let mut repos = Vec::new();
181        for repo_res in repo_iter {
182            repos.push(repo_res.map_err(|e| AppError::Database(e.to_string()))?);
183        }
184        Ok(repos)
185    }
186
187    /// 保存 Skill 仓库
188    pub fn save_skill_repo(&self, repo: &SkillRepo) -> Result<(), AppError> {
189        let conn = lock_conn!(self.conn);
190        conn.execute(
191            "INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled) VALUES (?1, ?2, ?3, ?4)",
192            params![repo.owner, repo.name, repo.branch, repo.enabled],
193        )
194        .map_err(|e| AppError::Database(e.to_string()))?;
195        Ok(())
196    }
197
198    /// 删除 Skill 仓库
199    pub fn delete_skill_repo(&self, owner: &str, name: &str) -> Result<(), AppError> {
200        let conn = lock_conn!(self.conn);
201        conn.execute(
202            "DELETE FROM skill_repos WHERE owner = ?1 AND name = ?2",
203            params![owner, name],
204        )
205        .map_err(|e| AppError::Database(e.to_string()))?;
206        Ok(())
207    }
208
209    /// 初始化默认的 Skill 仓库(启动时调用,补充缺失的默认仓库)
210    pub fn init_default_skill_repos(&self) -> Result<usize, AppError> {
211        // 获取已有仓库列表
212        let existing = self.get_skill_repos()?;
213        let existing_keys: std::collections::HashSet<(String, String)> = existing
214            .iter()
215            .map(|r| (r.owner.clone(), r.name.clone()))
216            .collect();
217
218        // 获取默认仓库列表
219        let default_store = crate::services::skill::SkillStore::default();
220        let mut count = 0;
221
222        // 仅插入缺失的默认仓库
223        for repo in &default_store.repos {
224            let key = (repo.owner.clone(), repo.name.clone());
225            if !existing_keys.contains(&key) {
226                self.save_skill_repo(repo)?;
227                count += 1;
228                log::info!("补充默认 Skill 仓库: {}/{}", repo.owner, repo.name);
229            }
230        }
231
232        if count > 0 {
233            log::info!("补充默认 Skill 仓库完成,新增 {count} 个");
234        }
235        Ok(count)
236    }
237}