Files
iAs/src/tools/parser.rs
T
wunianxiao f7ba23db94 feat: 导入的 skills 默认存到 ~/.ias/skills/
- Import/Delete 目标改为 ~/.ias/skills/(全局共享)
- SkillRegistry::load() 返回 (Self, Vec<String>) 替代 Result
  允许部分加载:格式不兼容的 skill 记录警告但不阻止加载
- 新增 global_skills_dir() 辅助函数
2026-06-02 10:02:54 +08:00

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(&params_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();
}
}