matrixcode_core/tools/
skill.rs1use std::sync::Arc;
8
9use anyhow::Result;
10use async_trait::async_trait;
11use serde_json::{Value, json};
12
13use super::{Tool, ToolDefinition};
14use crate::skills::{Skill, list_skill_files};
15
16pub struct SkillTool {
17 skills: Arc<Vec<Skill>>,
18}
19
20impl SkillTool {
21 pub fn new(skills: Arc<Vec<Skill>>) -> Self {
22 Self { skills }
23 }
24}
25
26#[async_trait]
27impl Tool for SkillTool {
28 fn definition(&self) -> ToolDefinition {
29 let mut props = json!({
34 "name": {
35 "type": "string",
36 "description": "要加载的技能名称(必须匹配系统提示中列出的名称)"
37 }
38 });
39 if !self.skills.is_empty() {
40 let names: Vec<Value> = self
41 .skills
42 .iter()
43 .map(|s| Value::String(s.name.clone()))
44 .collect();
45 props["name"]["enum"] = Value::Array(names);
46 }
47
48 ToolDefinition {
49 name: "skill".to_string(),
50 description:
51 "加载指定技能的完整指令。当系统提示中列出的技能与用户请求相关时调用此工具。\
52 返回包含技能内容和技能目录中的文件列表,可用 `read` 工具读取。"
53 .to_string(),
54 parameters: json!({
55 "type": "object",
56 "properties": props,
57 "required": ["name"]
58 }),
59 }
60 }
61
62 async fn execute(&self, params: Value) -> Result<String> {
63 let name = params["name"]
64 .as_str()
65 .ok_or_else(|| anyhow::anyhow!("missing 'name'"))?;
66
67 let skill = self
71 .skills
72 .iter()
73 .find(|s| s.name == name)
74 .ok_or_else(|| {
75 let available: Vec<&str> = self.skills.iter().map(|s| s.name.as_str()).collect();
76 anyhow::anyhow!(
78 "unknown skill '{}'. Available: {}",
79 name,
80 if available.is_empty() {
81 "(none loaded)".to_string()
82 } else {
83 available.join(", ")
84 }
85 )
86 })?;
87
88 let files = list_skill_files(&skill.dir);
89 let files_section = if files.is_empty() {
90 String::new()
91 } else {
92 let mut s = String::from("\n\n---\nFiles in this skill (read with the `read` tool, paths are relative to the skill directory):\n");
93 s.push_str(&format!("skill_dir: {}\n", skill.dir.display()));
94 for f in files {
95 s.push_str(&format!("- {}\n", f));
96 }
97 s
98 };
99
100 let result = format!(
101 "# Skill: {}\n\n{}{}",
102 skill.name,
103 skill.body.trim_end(),
104 files_section
105 );
106
107 Ok(result)
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use std::path::PathBuf;
116
117 fn fake_skill(name: &str, body: &str) -> Skill {
118 Skill {
119 name: name.to_string(),
120 description: "desc".to_string(),
121 dir: PathBuf::from("/nonexistent"),
122 body: body.to_string(),
123 source_file: PathBuf::from("/nonexistent/SKILL.md"),
124 }
125 }
126
127 #[tokio::test]
128 async fn returns_skill_body() {
129 let skills = Arc::new(vec![fake_skill("demo", "do the thing")]);
130 let tool = SkillTool::new(skills);
131 let out = tool.execute(json!({"name": "demo"})).await.unwrap();
132 assert!(out.contains("# Skill: demo"));
133 assert!(out.contains("do the thing"));
134 }
135
136 #[tokio::test]
137 async fn unknown_skill_errors() {
138 let skills = Arc::new(vec![fake_skill("demo", "x")]);
139 let tool = SkillTool::new(skills);
140 let err = tool
141 .execute(json!({"name": "missing"}))
142 .await
143 .unwrap_err()
144 .to_string();
145 assert!(err.contains("unknown skill"));
146 assert!(err.contains("demo"));
147 }
148
149 #[tokio::test]
150 async fn missing_name_errors() {
151 let skills = Arc::new(vec![]);
152 let tool = SkillTool::new(skills);
153 let err = tool.execute(json!({})).await.unwrap_err().to_string();
154 assert!(err.contains("missing 'name'"));
155 }
156
157 #[test]
158 fn definition_enum_only_when_skills_present() {
159 let empty = SkillTool::new(Arc::new(vec![]));
160 let def = empty.definition();
161 assert!(def.parameters["properties"]["name"].get("enum").is_none());
162
163 let full = SkillTool::new(Arc::new(vec![fake_skill("a", ""), fake_skill("b", "")]));
164 let def = full.definition();
165 let names = def.parameters["properties"]["name"]["enum"]
166 .as_array()
167 .unwrap();
168 assert_eq!(names.len(), 2);
169 }
170}