203 lines
6.2 KiB
Rust
203 lines
6.2 KiB
Rust
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<SkillsLoader>,
|
||
}
|
||
|
||
impl GetSkillTool {
|
||
pub fn new(skills_loader: Arc<SkillsLoader>) -> 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<ToolResult> {
|
||
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<ToolResult> {
|
||
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::<Vec<_>>()
|
||
.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<ToolResult> {
|
||
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());
|
||
}
|
||
}
|