Skip to main content

mofa_plugins/skill/
disclosure.rs

1//! 渐进式披露控制
2
3use crate::skill::{Requirement, RequirementCheck, metadata::SkillMetadata, parser::SkillParser};
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7/// 渐进式披露控制器
8///
9/// 支持多个 skills 目录按优先级搜索(workspace > builtin > standard)
10#[derive(Debug, Clone)]
11pub struct DisclosureController {
12    /// Skills 目录列表(按优先级排序)
13    search_dirs: Vec<PathBuf>,
14    /// 缓存的元数据(第1层)
15    metadata_cache: HashMap<String, SkillMetadata>,
16    /// Skill 名称到目录的映射(记录实际来源)
17    skill_sources: HashMap<String, PathBuf>,
18}
19
20impl DisclosureController {
21    /// 创建新的披露控制器(单目录)
22    pub fn new(skills_dir: impl Into<PathBuf>) -> Self {
23        Self {
24            search_dirs: vec![skills_dir.into()],
25            metadata_cache: HashMap::new(),
26            skill_sources: HashMap::new(),
27        }
28    }
29
30    /// 创建新的披露控制器(多目录)
31    ///
32    /// # Arguments
33    ///
34    /// * `search_dirs` - Skills 目录列表,按优先级排序(workspace > builtin > standard)
35    pub fn with_search_dirs(search_dirs: Vec<PathBuf>) -> Self {
36        Self {
37            search_dirs,
38            metadata_cache: HashMap::new(),
39            skill_sources: HashMap::new(),
40        }
41    }
42
43    /// 查找内置 skills 目录
44    ///
45    /// 按以下顺序查找:
46    /// 1. CARGO_MANIFEST_DIR/skills(开发时)
47    /// 2. 可执行文件父目录/skills(已安装)
48    /// 3. /usr/local/lib/mofa/skills(标准安装路径)
49    pub fn find_builtin_skills() -> Option<PathBuf> {
50        // Try CARGO_MANIFEST_DIR first (development)
51        if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
52            let skills_path = PathBuf::from(manifest_dir).join("skills");
53            if skills_path.exists() {
54                return Some(skills_path);
55            }
56        }
57
58        // Try executable parent directory (installed binary)
59        if let Ok(exe) = std::env::current_exe()
60            && let Some(parent) = exe.parent()
61        {
62            let skills_path = parent.join("skills");
63            if skills_path.exists() {
64                return Some(skills_path);
65            }
66        }
67
68        // Try grandparent directory (for /usr/local/bin/mofa -> /usr/local/lib/mofa/skills)
69        if let Ok(exe) = std::env::current_exe()
70            && let Some(grandparent) = exe.parent().and_then(|p| p.parent())
71        {
72            let skills_path = grandparent.join("lib").join("mofa").join("skills");
73            if skills_path.exists() {
74                return Some(skills_path);
75            }
76        }
77
78        // Try standard installation path
79        let standard_path = PathBuf::from("/usr/local/lib/mofa/skills");
80        if standard_path.exists() {
81            return Some(standard_path);
82        }
83
84        None
85    }
86
87    /// 扫描并加载所有 Skills 的元数据(第1层)
88    ///
89    /// 按优先级从多个目录扫描,先找到的优先
90    pub fn scan_metadata(&mut self) -> anyhow::Result<usize> {
91        let mut count = 0;
92
93        for skills_dir in &self.search_dirs {
94            if !skills_dir.exists() {
95                continue;
96            }
97
98            for entry in walkdir::WalkDir::new(skills_dir)
99                .min_depth(1)
100                .max_depth(1)
101                .into_iter()
102                .filter_map(|e| e.ok())
103            {
104                if entry.file_type().is_dir() {
105                    let skill_name = entry.file_name().to_string_lossy().to_string();
106
107                    // Skip if already found from higher priority dir
108                    if self.metadata_cache.contains_key(&skill_name) {
109                        continue;
110                    }
111
112                    let skill_md = entry.path().join("SKILL.md");
113                    if skill_md.exists()
114                        && let Ok((metadata, _)) = SkillParser::parse_from_file(&skill_md)
115                    {
116                        self.metadata_cache.insert(metadata.name.clone(), metadata);
117                        self.skill_sources.insert(skill_name, skills_dir.clone());
118                        count += 1;
119                    }
120                }
121            }
122        }
123
124        tracing::info!(
125            "Scanned {} skills from {} directories",
126            count,
127            self.search_dirs.len()
128        );
129        Ok(count)
130    }
131
132    /// 第1层:获取所有 Skills 的元数据(用于系统提示)
133    pub fn get_all_metadata(&self) -> Vec<SkillMetadata> {
134        self.metadata_cache.values().cloned().collect()
135    }
136
137    /// 构建系统提示(仅包含元数据)
138    pub fn build_system_prompt(&self) -> String {
139        let metadata: Vec<String> = self
140            .metadata_cache
141            .values()
142            .map(|m| format!("- {}: {}", m.name, m.description))
143            .collect();
144
145        format!(
146            "You have access to the following skills:\n{}\n\nWhen a task requires a specific skill, \
147             load the full SKILL.md file to get detailed instructions.",
148            metadata.join("\n")
149        )
150    }
151
152    /// 获取 Skill 目录路径
153    pub fn get_skill_path(&self, name: &str) -> Option<PathBuf> {
154        self.skill_sources.get(name).map(|dir| dir.join(name))
155    }
156
157    /// 检查 Skill 是否存在
158    pub fn has_skill(&self, name: &str) -> bool {
159        self.metadata_cache.contains_key(name)
160    }
161
162    /// 获取标记为 always 的技能名称列表
163    pub fn get_always_skills(&self) -> Vec<String> {
164        self.metadata_cache
165            .values()
166            .filter(|m| m.always)
167            .map(|m| m.name.clone())
168            .collect()
169    }
170
171    /// 检查技能依赖是否满足
172    pub fn check_requirements(&self, name: &str) -> RequirementCheck {
173        let metadata = match self.metadata_cache.get(name) {
174            Some(m) => m,
175            None => return RequirementCheck::default(),
176        };
177
178        let requires = metadata.requires.as_ref();
179        let mut missing = Vec::new();
180
181        if let Some(reqs) = requires {
182            // Check CLI tools
183            for tool in &reqs.cli_tools {
184                if !Self::command_exists(tool) {
185                    missing.push(Requirement::CliTool(tool.clone()));
186                }
187            }
188
189            // Check environment variables
190            for env_var in &reqs.env_vars {
191                if std::env::var(env_var).is_err() {
192                    missing.push(Requirement::EnvVar(env_var.clone()));
193                }
194            }
195        }
196
197        RequirementCheck {
198            satisfied: missing.is_empty(),
199            missing,
200        }
201    }
202
203    /// 获取技能的安装指令
204    pub fn get_install_instructions(&self, name: &str) -> Option<String> {
205        self.metadata_cache
206            .get(name)
207            .and_then(|m| m.install.as_ref())
208            .cloned()
209    }
210
211    /// 获取缺失依赖的描述字符串
212    pub fn get_missing_requirements_description(&self, name: &str) -> String {
213        let check = self.check_requirements(name);
214        if check.satisfied {
215            String::new()
216        } else {
217            check
218                .missing
219                .iter()
220                .map(|r| match r {
221                    Requirement::CliTool(t) => format!("CLI: {}", t),
222                    Requirement::EnvVar(v) => format!("ENV: {}", v),
223                })
224                .collect::<Vec<_>>()
225                .join(", ")
226        }
227    }
228
229    /// 检查命令是否存在
230    fn command_exists(cmd: &str) -> bool {
231        which::which(cmd).is_ok()
232    }
233
234    /// 按关键词搜索相关 Skills
235    pub fn search(&self, query: &str) -> Vec<String> {
236        let query_lower = query.to_lowercase();
237
238        self.metadata_cache
239            .values()
240            .filter(|m| {
241                m.name.to_lowercase().contains(&query_lower)
242                    || m.description.to_lowercase().contains(&query_lower)
243                    || m.tags
244                        .iter()
245                        .any(|t| t.to_lowercase().contains(&query_lower))
246            })
247            .map(|m| m.name.clone())
248            .collect()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use std::fs;
256    use std::path::Path;
257    use tempfile::TempDir;
258
259    fn create_test_skill(dir: &Path, name: &str, description: &str) -> std::io::Result<()> {
260        let skill_dir = dir.join(name);
261        fs::create_dir_all(&skill_dir)?;
262
263        let content = format!(
264            r#"---
265name: {}
266description: {}
267category: test
268tags: [test]
269version: "1.0.0"
270---
271
272# {} Skill
273
274This is a test skill."#,
275            name, description, name
276        );
277
278        fs::write(skill_dir.join("SKILL.md"), content)?;
279        Ok(())
280    }
281
282    #[test]
283    fn test_scan_metadata() {
284        let temp_dir = TempDir::new().unwrap();
285        let skills_dir = temp_dir.path();
286
287        create_test_skill(skills_dir, "skill1", "First skill").unwrap();
288        create_test_skill(skills_dir, "skill2", "Second skill").unwrap();
289
290        let mut controller = DisclosureController::new(skills_dir);
291        let count = controller.scan_metadata().unwrap();
292
293        assert_eq!(count, 2);
294        assert!(controller.has_skill("skill1"));
295        assert!(controller.has_skill("skill2"));
296        assert!(!controller.has_skill("skill3"));
297    }
298
299    #[test]
300    fn test_build_system_prompt() {
301        let temp_dir = TempDir::new().unwrap();
302        let skills_dir = temp_dir.path();
303
304        create_test_skill(skills_dir, "skill1", "First skill").unwrap();
305        create_test_skill(skills_dir, "skill2", "Second skill").unwrap();
306
307        let mut controller = DisclosureController::new(skills_dir);
308        controller.scan_metadata().unwrap();
309
310        let prompt = controller.build_system_prompt();
311        assert!(prompt.contains("skill1"));
312        assert!(prompt.contains("First skill"));
313        assert!(prompt.contains("skill2"));
314        assert!(prompt.contains("Second skill"));
315    }
316
317    #[test]
318    fn test_search() {
319        let temp_dir = TempDir::new().unwrap();
320        let skills_dir = temp_dir.path();
321
322        create_test_skill(skills_dir, "pdf_processing", "Process PDF files").unwrap();
323        create_test_skill(skills_dir, "web_scraping", "Scrape web pages").unwrap();
324
325        let mut controller = DisclosureController::new(skills_dir);
326        controller.scan_metadata().unwrap();
327
328        let results = controller.search("pdf");
329        assert_eq!(results, vec!["pdf_processing".to_string()]);
330
331        let results = controller.search("web");
332        assert_eq!(results, vec!["web_scraping".to_string()]);
333    }
334}