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