1use crate::permission::JcliConfig;
2use serde::Deserialize;
3use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum SkillSource {
12 User,
14 Project,
16}
17
18impl SkillSource {
19 pub fn label(&self) -> &'static str {
21 match self {
22 SkillSource::User => "用户",
23 SkillSource::Project => "项目",
24 }
25 }
26}
27
28#[derive(Debug, Clone, Deserialize)]
30pub struct SkillFrontmatter {
31 pub name: String,
32 pub description: String,
33}
34
35#[derive(Debug, Clone)]
37pub struct Skill {
38 pub frontmatter: SkillFrontmatter,
39 pub body: String,
41 pub dir_path: PathBuf,
43 pub source: SkillSource,
45}
46
47pub fn skills_dir() -> PathBuf {
51 let dir = crate::constants::data_root().join("agent").join("skills");
52 let _ = fs::create_dir_all(&dir);
53 dir
54}
55
56pub fn project_skills_dir() -> Option<PathBuf> {
58 let config_dir = JcliConfig::find_config_dir()?;
59 let dir = config_dir.join("skills");
60 if dir.is_dir() { Some(dir) } else { None }
61}
62
63fn load_skills_from_dir(dir: &Path, source: SkillSource) -> Vec<Skill> {
65 let mut skills = Vec::new();
66 let entries = match fs::read_dir(dir) {
67 Ok(e) => e,
68 Err(_) => return skills,
69 };
70 for entry in entries.flatten() {
71 let path = entry.path();
72 if !path.is_dir() {
73 continue;
74 }
75 let skill_md = path.join("SKILL.md");
76 if skill_md.exists()
77 && let Some(mut skill) = parse_skill_md(&skill_md, &path)
78 {
79 skill.source = source;
80 skills.push(skill);
81 }
82 }
83 skills
84}
85
86pub fn load_all_skills() -> Vec<Skill> {
88 let mut map: HashMap<String, Skill> = HashMap::new();
89
90 for skill in load_skills_from_dir(&skills_dir(), SkillSource::User) {
92 map.insert(skill.frontmatter.name.clone(), skill);
93 }
94
95 if let Some(dir) = project_skills_dir() {
97 for skill in load_skills_from_dir(&dir, SkillSource::Project) {
98 map.insert(skill.frontmatter.name.clone(), skill);
99 }
100 }
101
102 let mut skills: Vec<Skill> = map.into_values().collect();
103 skills.sort_by(|a, b| a.frontmatter.name.cmp(&b.frontmatter.name));
104 skills
105}
106
107fn parse_skill_md(path: &Path, dir: &Path) -> Option<Skill> {
109 let content = fs::read_to_string(path).ok()?;
110 let (fm_str, body) = split_frontmatter(&content)?;
111 let frontmatter: SkillFrontmatter = serde_yaml::from_str(&fm_str).ok()?;
112
113 if frontmatter.name.is_empty() {
114 return None;
115 }
116
117 Some(Skill {
118 frontmatter,
119 body: body.trim().to_string(),
120 dir_path: dir.to_path_buf(),
121 source: SkillSource::User, })
123}
124
125pub(super) fn split_frontmatter(content: &str) -> Option<(String, String)> {
127 let trimmed = content.trim_start();
128 if !trimmed.starts_with("---") {
129 return None;
130 }
131 let rest = &trimmed[3..];
133 let end_idx = rest.find("\n---")?;
134 let fm = rest[..end_idx].trim().to_string();
135 let body = rest[end_idx + 4..].to_string();
136 Some((fm, body))
137}
138
139pub fn resolve_skill_content(skill: &Skill) -> String {
141 let mut result = skill.body.clone();
142
143 if let Some(paths) = list_dir_files(&skill.dir_path.join("references")) {
145 result.push_str("\n\n## 参考文件\n\n以下参考文件可按需使用 Read 工具读取:\n");
146 for p in &paths {
147 result.push_str(&format!("- `{}`\n", p));
148 }
149 }
150
151 if let Some(paths) = list_dir_files(&skill.dir_path.join("scripts")) {
153 result.push_str("\n\n## 脚本\n\n以下脚本可按需使用 Shell/BackgroundRun 工具执行:\n");
154 for p in &paths {
155 result.push_str(&format!("- `{}`\n", p));
156 }
157 }
158
159 result
160}
161
162fn list_dir_files(dir: &Path) -> Option<Vec<String>> {
164 if !dir.is_dir() {
165 return None;
166 }
167 let entries = fs::read_dir(dir).ok()?;
168 let mut files: Vec<_> = entries.flatten().collect();
169 files.sort_by_key(|e| e.file_name());
170 let paths: Vec<String> = files
171 .iter()
172 .filter(|e| e.path().is_file())
173 .map(|e| e.path().display().to_string())
174 .collect();
175 if paths.is_empty() { None } else { Some(paths) }
176}
177
178pub fn build_skills_summary(skills: &[Skill], disabled_skills: &[String]) -> String {
183 let filtered: Vec<&Skill> = skills
184 .iter()
185 .filter(|s| !disabled_skills.iter().any(|d| d == &s.frontmatter.name))
186 .collect();
187 if filtered.is_empty() {
188 return "(暂无可用技能)".to_string();
189 }
190 let mut md = String::new();
191 for s in &filtered {
192 md.push_str(&format!(
193 "- {}: {}\n",
194 s.frontmatter.name, s.frontmatter.description
195 ));
196 }
197 md.trim_end().to_string()
198}