use crate::tools::skill::{Skill, SkillSource, SkillSpec}; use std::fs; use std::path::Path; /// 解析 SKILL.md 文件,返回 Skill /// /// SKILL.md 格式: /// ```markdown /// # Skill Name /// Description text... /// /// ## Risk Level /// Low | High /// /// ## Parameters /// ```json /// { ... } /// ``` /// /// ## Execute /// ```bash /// command to run /// ``` /// ``` pub fn parse_skill(skill_dir: &Path) -> Result { let md_path = skill_dir.join("SKILL.md"); let content = fs::read_to_string(&md_path).map_err(|e| format!("读取 {} 失败: {}", md_path.display(), e))?; let lines: Vec<&str> = content.lines().collect(); let mut name = String::new(); let mut description = String::new(); let mut risk_level = String::new(); let mut params_raw = String::new(); let mut execute_raw = String::new(); let mut in_params = false; let mut in_execute = false; let mut params_fence = 0; let mut execute_fence = 0; let mut desc_lines: Vec<&str> = Vec::new(); let mut in_desc = false; for (i, line) in lines.iter().enumerate() { let trimmed = line.trim(); // 标题行 → 技能名 if trimmed.starts_with("# ") && !trimmed.starts_with("## ") && name.is_empty() { name = trimmed.trim_start_matches("# ").trim().to_string(); continue; } // 二级标题 if trimmed.starts_with("## ") { let section = trimmed.trim_start_matches("## ").trim().to_lowercase(); match section.as_str() { "risk level" | "risk_level" | "risk" => { in_desc = false; in_params = false; in_execute = false; } "parameters" | "param" => { in_desc = false; in_params = true; in_execute = false; } "execute" | "execution" | "command" | "run" => { in_desc = false; in_params = false; in_execute = true; params_fence = 0; execute_fence = 0; } _ => { in_desc = false; in_params = false; in_execute = false; } } continue; } // 描述:标题和 Risk Level 之间的文本 if !in_params && !in_execute && !trimmed.starts_with("#") && !trimmed.is_empty() { if name.is_empty() { continue; } // 检查是否已进入 Risk Level 段 let is_risk_line = trimmed.to_lowercase().starts_with("low") || trimmed.to_lowercase().starts_with("high"); if risk_level.is_empty() && is_risk_line { risk_level = trimmed.to_string(); continue; } if risk_level.is_empty() { desc_lines.push(trimmed); } continue; } // Risk Level 值 if !in_params && !in_execute && risk_level.is_empty() { let lower = trimmed.to_lowercase(); if lower == "low" || lower == "high" { risk_level = trimmed.to_string(); continue; } } // Parameters: JSON 代码块 if in_params { if trimmed.starts_with("```") { params_fence += 1; if params_fence == 2 { in_params = false; } continue; } if params_fence == 1 { if !params_raw.is_empty() { params_raw.push('\n'); } params_raw.push_str(trimmed); } continue; } // Execute: shell 命令 if in_execute { if trimmed.starts_with("```") { execute_fence += 1; if execute_fence == 2 { in_execute = false; } continue; } if execute_fence == 1 { // 跳过 shebang 行 if trimmed.starts_with("#!/") { continue; } if !execute_raw.is_empty() { execute_raw.push('\n'); } execute_raw.push_str(trimmed); } continue; } } // 验证 if name.is_empty() { return Err(format!("{}: 未找到技能名称 (# Title)", md_path.display())); } description = desc_lines.join(" ").trim().to_string(); let risk = match risk_level.to_lowercase().trim() { "high" => crate::tools::skill::RiskLevel::High, _ => crate::tools::skill::RiskLevel::Low, }; // 解析参数 JSON let parameters: serde_json::Value = if params_raw.is_empty() { serde_json::json!({ "type": "object", "properties": {} }) } else { serde_json::from_str(¶ms_raw).unwrap_or_else(|_| { serde_json::json!({ "type": "object", "properties": {} }) }) }; // 执行命令(去掉首尾空白) let execute_command = execute_raw.trim().to_string(); if execute_command.is_empty() { return Err(format!("{}: 未找到 Execute 命令", md_path.display())); } let spec = SkillSpec { name, description, risk_level: risk, parameters, timeout_secs: 30, }; Ok(Skill::new( spec, // 先设成 User,由调用方修正 SkillSource::User, skill_dir.to_path_buf(), execute_command, )) } #[cfg(test)] mod tests { use super::*; use std::io::Write; #[test] fn test_load_all_system_skills() { let rt = tokio::runtime::Runtime::new().unwrap(); let result = rt.block_on(crate::tools::registry::SkillRegistry::load( "resources/skills", "/nonexistent", )); match result { Ok(registry) => { assert_eq!(registry.count(), 8, "应该有 8 个系统技能"); let names: Vec<&str> = registry.list_specs().iter().map(|s| s.name.as_str()).collect(); println!("已加载技能: {:?}", names); assert!(names.contains(&"manage_memos")); assert!(names.contains(&"get_current_datetime")); assert!(names.contains(&"query_weather")); assert!(names.contains(&"search_web")); assert!(names.contains(&"email")); assert!(names.contains(&"execute_shell")); assert!(names.contains(&"list_scheduled_tasks")); assert!(names.contains(&"manage_scheduled_task")); } Err(errors) => { for e in &errors { eprintln!("加载失败: {}", e); } panic!("技能加载失败"); } } } #[test] fn test_parse_basic_skill() { let dir = std::env::temp_dir().join("ias-test-skill"); fs::create_dir_all(&dir).ok(); let mut f = fs::File::create(dir.join("SKILL.md")).unwrap(); write!( f, r#"# Get Current Datetime 获取当前日期时间(北京时间 UTC+8)。 ## Risk Level Low ## Parameters ```json {{ "type": "object", "properties": {{ "format": {{ "type": "string", "description": "full | date | time" }} }} }} ``` ## Execute ```bash date '+%Y-%m-%d %H:%M:%S' ``` "# ) .unwrap(); let skill = parse_skill(&dir).unwrap(); assert_eq!(skill.spec.name, "Get Current Datetime"); assert!(skill.spec.description.contains("日期时间")); assert_eq!(skill.spec.risk_level, crate::tools::skill::RiskLevel::Low); assert!(!skill.execute_command.is_empty()); assert!(skill.execute_command.contains("date")); fs::remove_dir_all(&dir).ok(); } }