use std::sync::Arc; use async_trait::async_trait; use serde_json::json; use crate::skills::{Skill, SkillsLoader}; use crate::tools::traits::{Tool, ToolResult}; pub struct GetSkillTool { skills_loader: Arc, } impl GetSkillTool { pub fn new(skills_loader: Arc) -> Self { Self { skills_loader } } fn format_skill(&self, skill: &Skill) -> String { let mut result = format!("# Skill: {}\n\n{}", skill.name, skill.description); if let Some(path) = &skill.path { result.push_str(&format!( "\n\n**Skill Root Directory:** `{}`\n\nAll files and references in this skill are relative to this directory.", path.to_string_lossy() )); } result.push_str(&format!("\n\n---\n\n{}", skill.content)); result } } #[async_trait] impl Tool for GetSkillTool { fn name(&self) -> &str { "get_skill" } fn description(&self) -> &str { "Get complete content and guidance for a specified skill. Use this when you need detailed instructions for a specific type of task." } fn parameters_schema(&self) -> serde_json::Value { json!({ "type": "object", "properties": { "action": { "type": "string", "enum": ["get", "list"], "description": "操作类型: get 获取指定 skill 完整内容, list 列出所有可用 skill" }, "skill_name": { "type": "string", "description": "Name of the skill to retrieve,仅在 action 为 get 时必填" } }, "required": [] }) } fn read_only(&self) -> bool { true } async fn execute(&self, args: serde_json::Value) -> anyhow::Result { let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("get"); match action { "list" => self.list_skills_full(), _ => self.get_skill_by_name(&args), } } } impl GetSkillTool { fn get_skill_by_name(&self, args: &serde_json::Value) -> anyhow::Result { let skill_name = match args.get("skill_name").and_then(|v| v.as_str()) { Some(name) => name, None => { return Ok(ToolResult { success: false, output: String::new(), error: Some("Missing required parameter: skill_name".to_string()), }); } }; match self.skills_loader.get_skill(skill_name) { Some(skill) => { let formatted = self.format_skill(&skill); Ok(ToolResult { success: true, output: formatted, error: None, }) } None => { let available = self.skills_loader.list_skills(); let available_str = if available.is_empty() { "No skills available".to_string() } else { available .iter() .map(|(name, _)| name.as_str()) .collect::>() .join(", ") }; Ok(ToolResult { success: false, output: String::new(), error: Some(format!( "Skill '{}' not found. Available skills: {}", skill_name, available_str )), }) } } } fn list_skills_full(&self) -> anyhow::Result { let skills = self.skills_loader.get_loaded_skills(); if skills.is_empty() { return Ok(ToolResult { success: true, output: "当前没有安装任何 skill".to_string(), error: None, }); } let mut output = format!("可用 skill (共 {} 个):\n", skills.len()); for s in &skills { let always_mark = if s.always { " [常驻]" } else { "" }; let path_str = s.path.as_ref() .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|| "—".to_string()); output.push_str(&format!( "- {}{}\n 简介: {}\n 路径: {}\n", s.name, always_mark, s.description, path_str )); } Ok(ToolResult { success: true, output, error: None, }) } } #[cfg(test)] mod tests { use super::*; use tempfile::tempdir; use std::fs::File; use std::io::Write; use std::path::PathBuf; #[tokio::test] async fn test_get_existing_skill() { let temp_dir = tempdir().unwrap(); let skill_dir = temp_dir.path().join("test-skill"); std::fs::create_dir(&skill_dir).unwrap(); let mut skill_file = File::create(skill_dir.join("SKILL.md")).unwrap(); writeln!(skill_file, "---").unwrap(); writeln!(skill_file, "name: test-skill").unwrap(); writeln!(skill_file, "description: A test skill").unwrap(); writeln!(skill_file, "---").unwrap(); writeln!(skill_file, "# Test Skill").unwrap(); writeln!(skill_file, "This is the test content.").unwrap(); let loader = SkillsLoader::new_for_testing( temp_dir.path().to_path_buf(), PathBuf::from("/nonexistent"), ); loader.load_skills(); let tool = GetSkillTool::new(Arc::new(loader)); let result = tool .execute(json!({ "skill_name": "test-skill" })) .await .unwrap(); assert!(result.success); assert!(result.output.contains("test-skill")); assert!(result.output.contains("test content")); } #[tokio::test] async fn test_get_nonexistent_skill() { let loader = SkillsLoader::new(); let tool = GetSkillTool::new(Arc::new(loader)); let result = tool .execute(json!({ "skill_name": "nonexistent" })) .await .unwrap(); assert!(!result.success); assert!(result.error.is_some()); } }