f7ba23db94
- Import/Delete 目标改为 ~/.ias/skills/(全局共享) - SkillRegistry::load() 返回 (Self, Vec<String>) 替代 Result 允许部分加载:格式不兼容的 skill 记录警告但不阻止加载 - 新增 global_skills_dir() 辅助函数
281 lines
7.9 KiB
Rust
281 lines
7.9 KiB
Rust
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<Skill, String> {
|
|
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 frontmatter_fences = 0;
|
|
|
|
for (_i, line) in lines.iter().enumerate() {
|
|
let trimmed = line.trim();
|
|
|
|
// 跳过 YAML frontmatter (---...---)
|
|
if trimmed == "---" && frontmatter_fences < 2 {
|
|
frontmatter_fences += 1;
|
|
continue;
|
|
}
|
|
if frontmatter_fences == 1 {
|
|
continue;
|
|
}
|
|
|
|
// 标题行 → 技能名
|
|
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_params = false;
|
|
in_execute = false;
|
|
}
|
|
"parameters" | "param" => {
|
|
in_params = true;
|
|
in_execute = false;
|
|
}
|
|
"execute" | "execution" | "command" | "run" => {
|
|
in_params = false;
|
|
in_execute = true;
|
|
params_fence = 0;
|
|
execute_fence = 0;
|
|
}
|
|
_ => {
|
|
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 (registry, errors) = rt.block_on(crate::tools::registry::SkillRegistry::load(
|
|
"resources/skills",
|
|
"/nonexistent",
|
|
));
|
|
if !errors.is_empty() {
|
|
for e in &errors {
|
|
eprintln!("加载警告: {}", e);
|
|
}
|
|
}
|
|
assert_eq!(registry.count(), 9, "应该有 9 个系统技能");
|
|
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(&"manage_skill"));
|
|
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"));
|
|
}
|
|
|
|
#[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();
|
|
}
|
|
}
|