Files
iAs/src/tools/parser.rs
T
wunianxiao b9de3665d9 feat: iPet → iAs 完整迁移,Rust 版微信 AI 助手
Phase 1  CLI 框架 + 配置系统
  - clap 子命令: login / listen / send / whoami / usage
  - config.json + env var 替换
  - tracing 日志系统
  - state 持久化(auth/runtime 文件存 + PostgreSQL)

Phase 2  微信通道
  - wechat::client — 完整 iLink Bot HTTP API 实现
  - 扫码登录(终端二维码 + 轮询状态)
  - 长轮询 getupdates / 消息收发 / 监听注册

Phase 3  AI 对话(纯文本 + function calling)
  - LlmProvider trait: DeepSeek + LM Studio 实现
  - SSE 流式解析(text / reasoning / tool_calls delta / usage)
  - Conversation: 消息历史 + chat / chat_with_tools 工具循环

Phase 4  PostgreSQL 集成
  - app_state(认证 KV 存储)
  - chat_records(消息收发记录)
  - llm_usage(Token 用量统计缓存命中率)
  - user_memories(长期记忆持久化)
  - pending_approvals(审批确认码)
  - scheduled_tasks(定时任务表)

Phase 5  一切皆 Skill(工具系统)
  - SkillRegistry: 系统 + 用户 skills 双目录合并
  - SKILL.md 解析器 + 子进程执行器(stdin JSON → stdout)
  - 9 个系统 Skills: datetime / weather / search / email /
    shell / schedule / memos / read_memories / read_summaries
  - ApprovalManager: High 风险技能 → 确认码审批(透明模式)
  - High 风险技能:确认码审批(透明模式)

Phase 6  定时任务调度器

上下文管理
  - ChatSession: checkpoint + token budget (28K) + summaries
  - Token 估算器(中英文自适应)
  - 12h 空闲 → trigger_idle_summary(不入会话)
  - Budget 溢出 → trigger_overflow_summary(入会话 + drain 旧消息)
  - Summarizer: LLM 生成自然语言摘要(fallback 简单截断)
  - 长期记忆 / 摘要 通过 read_memories / read_summaries 工具按需读取

工具调用日志 + Token 统计
  - INFO: 工具名 + 参数 + 结果摘要
  - DEBUG: 子进程 exit/stdout/stderr
  - ias usage --since --until --model 查看用量和缓存命中率
2026-06-01 17:21:43 +08:00

280 lines
8.0 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 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(&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 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();
}
}