Skip to main content

j_cli/command/chat/
skill.rs

1use super::tools::{Tool, ToolResult};
2use crate::config::YamlConfig;
3use serde::Deserialize;
4use serde_json::{Value, json};
5use std::fs;
6use std::path::PathBuf;
7
8// ========== 数据结构 ==========
9
10#[derive(Debug, Clone, Deserialize)]
11pub struct SkillFrontmatter {
12    pub name: String,
13    pub description: String,
14    #[serde(rename = "argument-hint")]
15    pub argument_hint: Option<String>,
16}
17
18#[derive(Debug, Clone)]
19pub struct Skill {
20    pub frontmatter: SkillFrontmatter,
21    /// frontmatter 之后的 Markdown 正文
22    pub body: String,
23    /// skill 目录路径
24    pub dir_path: PathBuf,
25}
26
27// ========== 加载与解析 ==========
28
29/// 返回 skills 目录: ~/.jdata/agent/skills/
30pub fn skills_dir() -> PathBuf {
31    let dir = YamlConfig::data_dir().join("agent").join("skills");
32    let _ = fs::create_dir_all(&dir);
33    dir
34}
35
36/// 扫描 skills 目录,加载所有 skill
37pub fn load_all_skills() -> Vec<Skill> {
38    let dir = skills_dir();
39    let mut skills = Vec::new();
40
41    let entries = match fs::read_dir(&dir) {
42        Ok(e) => e,
43        Err(_) => return skills,
44    };
45
46    for entry in entries.flatten() {
47        let path = entry.path();
48        if !path.is_dir() {
49            continue;
50        }
51        let skill_md = path.join("SKILL.md");
52        if skill_md.exists() {
53            if let Some(skill) = parse_skill_md(&skill_md, &path) {
54                skills.push(skill);
55            }
56        }
57    }
58
59    skills.sort_by(|a, b| a.frontmatter.name.cmp(&b.frontmatter.name));
60    skills
61}
62
63/// 解析 SKILL.md: YAML frontmatter + body
64fn parse_skill_md(path: &PathBuf, dir: &PathBuf) -> Option<Skill> {
65    let content = fs::read_to_string(path).ok()?;
66    let (fm_str, body) = split_frontmatter(&content)?;
67    let frontmatter: SkillFrontmatter = serde_yaml::from_str(&fm_str).ok()?;
68
69    if frontmatter.name.is_empty() {
70        return None;
71    }
72
73    Some(Skill {
74        frontmatter,
75        body: body.trim().to_string(),
76        dir_path: dir.clone(),
77    })
78}
79
80/// 按 `---` 分隔 frontmatter 和 body
81fn split_frontmatter(content: &str) -> Option<(String, String)> {
82    let trimmed = content.trim_start();
83    if !trimmed.starts_with("---") {
84        return None;
85    }
86    // 跳过第一个 ---
87    let rest = &trimmed[3..];
88    let end_idx = rest.find("\n---")?;
89    let fm = rest[..end_idx].trim().to_string();
90    let body = rest[end_idx + 4..].to_string();
91    Some((fm, body))
92}
93
94/// 拼合 body + references/ 下的参考文件,截断到 MAX_BYTES
95pub fn resolve_skill_content(skill: &Skill) -> String {
96    const MAX_BYTES: usize = 12000;
97    let mut result = skill.body.clone();
98
99    // 读取 references/ 目录
100    let refs_dir = skill.dir_path.join("references");
101    if refs_dir.is_dir() {
102        if let Ok(entries) = fs::read_dir(&refs_dir) {
103            let mut ref_files: Vec<_> = entries.flatten().collect();
104            ref_files.sort_by_key(|e| e.file_name());
105            for entry in ref_files {
106                let path = entry.path();
107                if path.is_file() {
108                    if let Ok(content) = fs::read_to_string(&path) {
109                        let filename = path.file_name().unwrap_or_default().to_string_lossy();
110                        result
111                            .push_str(&format!("\n\n--- 参考文件: {} ---\n{}", filename, content));
112                    }
113                }
114                if result.len() > MAX_BYTES {
115                    break;
116                }
117            }
118        }
119    }
120
121    // 截断到 MAX_BYTES
122    if result.len() > MAX_BYTES {
123        let mut end = MAX_BYTES;
124        while !result.is_char_boundary(end) {
125            end -= 1;
126        }
127        result.truncate(end);
128        result.push_str("\n...(内容已截断)");
129    }
130
131    result
132}
133
134// ========== build_skills_summary ==========
135
136/// 构建 skills 摘要列表(name + description),用于系统提示词的 {{.skills}} 占位符
137pub fn build_skills_summary(skills: &[Skill]) -> String {
138    if skills.is_empty() {
139        return "(暂无已安装的技能)".to_string();
140    }
141    let mut result = String::new();
142    for skill in skills {
143        result.push_str(&format!(
144            "- **{}**: {}\n",
145            skill.frontmatter.name, skill.frontmatter.description
146        ));
147    }
148    result.trim_end().to_string()
149}
150
151// ========== LoadSkillTool: 统一的技能加载工具 ==========
152
153pub struct LoadSkillTool {
154    pub skills: Vec<Skill>,
155}
156
157impl Tool for LoadSkillTool {
158    fn name(&self) -> &str {
159        "load_skill"
160    }
161
162    fn description(&self) -> &str {
163        "加载指定技能的完整内容到上下文。当你判断需要某个技能时调用此工具。"
164    }
165
166    fn parameters_schema(&self) -> Value {
167        json!({
168            "type": "object",
169            "properties": {
170                "name": {
171                    "type": "string",
172                    "description": "要加载的技能名称"
173                },
174                "arguments": {
175                    "type": "string",
176                    "description": "传递给技能的参数(可选)"
177                }
178            },
179            "required": ["name"]
180        })
181    }
182
183    fn execute(&self, arguments: &str) -> ToolResult {
184        let parsed = serde_json::from_str::<Value>(arguments).ok();
185
186        let skill_name = parsed
187            .as_ref()
188            .and_then(|v| v.get("name").and_then(|n| n.as_str()))
189            .unwrap_or("");
190
191        let args_str = parsed
192            .as_ref()
193            .and_then(|v| v.get("arguments").and_then(|a| a.as_str()))
194            .unwrap_or("");
195
196        if skill_name.is_empty() {
197            return ToolResult {
198                output: "参数缺少 name 字段".to_string(),
199                is_error: true,
200            };
201        }
202
203        match self
204            .skills
205            .iter()
206            .find(|s| s.frontmatter.name == skill_name)
207        {
208            Some(skill) => {
209                let content = resolve_skill_content(skill);
210                let resolved = content.replace("$ARGUMENTS", args_str);
211                ToolResult {
212                    output: resolved,
213                    is_error: false,
214                }
215            }
216            None => {
217                let available: Vec<&str> = self
218                    .skills
219                    .iter()
220                    .map(|s| s.frontmatter.name.as_str())
221                    .collect();
222                ToolResult {
223                    output: format!(
224                        "未找到技能 '{}'。可用技能: {}",
225                        skill_name,
226                        available.join(", ")
227                    ),
228                    is_error: true,
229                }
230            }
231        }
232    }
233
234    fn requires_confirmation(&self) -> bool {
235        false
236    }
237}