1use 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 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 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 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 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 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 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 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 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 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 pub fn init_default_skill_repos(&self) -> Result<usize, AppError> {
214 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 let default_store = crate::services::skill::SkillStore::default();
223 let mut count = 0;
224
225 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}