refactor: 删除外置 skills 系统,全部工具转为内置

删除:
- resources/skills/ (9个外置 skill, bash/python/node 脚本)
- src/tools/registry.rs (SkillRegistry 加载/执行)
- src/tools/parser.rs (SKILL.md 解析)
- src/skills_importer.rs (skills 导入)
- cli.rs Skills 子命令
- main.rs cmd_skills / global_skills_dir

保留:
- src/tools/builtin.rs (内置工具注册 + 审批流程)
- src/tools/weather.rs (天气查询)
- src/tools/skill.rs (精简为 SkillSpec/RiskLevel/SkillResult/ExecutionContext)
- src/tools/approval.rs (审批管理器)

main.rs 简化: 移除 SkillRegistry 加载, executor 只走 BuiltinRegistry
This commit is contained in:
2026-06-03 10:28:28 +08:00
parent afc43ce692
commit 4d149982c5
29 changed files with 354 additions and 1651 deletions
+62 -185
View File
@@ -4,19 +4,18 @@ mod db;
mod llm;
mod logger;
mod scheduler;
mod skills_importer;
mod state;
mod tools;
mod wechat;
use clap::Parser;
use cli::{Cli, Commands, SkillsAction};
use cli::{Cli, Commands};
use context::MemoryStore;
use db::Database;
use llm::{Conversation, ConversationConfig, DEFAULT_SYSTEM_PROMPT, ToolExecutor, Usage};
use std::sync::Arc;
use tools::approval::{ApprovalDecision, ApprovalManager};
use tools::registry::{ExecutionContext, SkillRegistry};
use tools::skill::ExecutionContext;
use tracing::{error, info};
use wechat::client::WeChatClient;
@@ -87,12 +86,7 @@ async fn main() {
} => {
cmd_usage(&database, since, until, model).await;
}
Commands::Service => {
cmd_listen(true, false, &database, &file_state, &memory_store).await
}
Commands::Skills { action } => {
cmd_skills(action).await;
}
Commands::Service => cmd_listen(true, false, &database, &file_state, &memory_store).await,
}
}
@@ -196,24 +190,15 @@ async fn cmd_listen(
return;
}
// 加载系统 Skills
let (skill_registry, _warnings) = SkillRegistry::load("resources/skills", "skills").await;
let skill_registry = {
if !_warnings.is_empty() {
info!("技能加载警告: {:?}", _warnings);
}
info!("已加载 {} 个技能", skill_registry.count());
Some(Arc::new(skill_registry))
};
let approval_manager = Arc::new(ApprovalManager::new(
database.as_ref().map(|db| Arc::new(db.pool().clone()))
database.as_ref().map(|db| Arc::new(db.pool().clone())),
));
let conversation = if enable_llm {
let sys_prompt = std::env::var("WEIXIN_LLM_SYSTEM_PROMPT")
.unwrap_or_else(|_| DEFAULT_SYSTEM_PROMPT.to_string());
let model = std::env::var("DEEPSEEK_MODEL").unwrap_or_else(|_| "deepseek-v4-flash".to_string());
let model =
std::env::var("DEEPSEEK_MODEL").unwrap_or_else(|_| "deepseek-v4-flash".to_string());
let mut cfg = ConversationConfig {
system_prompt: sys_prompt,
@@ -221,7 +206,7 @@ async fn cmd_listen(
..Default::default()
};
// 仅暴露两个元工具:查询能力 + 调用能力
// 各项能力通过这两个元工具实现:查询能力 + 调用能力
// 具体工具描述通过 query_capabilities 返回
let mut tools_list: Vec<serde_json::Value> = Vec::new();
@@ -240,12 +225,12 @@ async fn cmd_listen(
"type": "function",
"function": {
"name": "call_capability",
"description": "按工具名称调用一个已注册的工具。name 为工具名(来自 query_capabilities),prompt 为自然语言需求",
"description": "调用一个已注册的工具。name 为工具名(来自 query_capabilities),工具所需的具体参数(如 location、days)应直接放在 JSON 中。",
"parameters": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "工具名称" },
"prompt": { "type": "string", "description": "自然语言需求" }
"prompt": { "type": "string", "description": "json格式参数,来自 query_capabilities 中的对应工具的描述。" }
},
"required": ["name"]
}
@@ -306,10 +291,9 @@ async fn cmd_listen(
// 构建工具执行器
let session = conv.session();
let registry_opt = skill_registry.clone();
let approval = approval_manager.clone();
let memory_store = memory_store.clone();
let send_wechat: tools::registry::WechatSender = {
let send_wechat: tools::skill::WechatSender = {
let client = client.clone();
Arc::new(move |user_id: &str, text: &str| {
let client = client.clone();
@@ -326,7 +310,6 @@ async fn cmd_listen(
};
let executor: ToolExecutor = Arc::new(move |name: &str, args_json: &str| {
let reg = registry_opt.clone();
let approval = approval.clone();
let send = send_wechat.clone();
let session = session.clone();
@@ -338,36 +321,39 @@ async fn cmd_listen(
let user_id = session.lock().await.current_user_id.clone();
// 上下文工具直接处理
if n == "read_memories" { return Ok(memory_store.read_for(&user_id).await); }
if n == "read_memories" {
return Ok(memory_store.read_for(&user_id).await);
}
if n == "write_memory" {
let params: serde_json::Value = serde_json::from_str(&args).unwrap_or_default();
let content = params.get("content").and_then(|v| v.as_str()).unwrap_or("");
if content.is_empty() { return Ok("请提供 content 参数".to_string()); }
if content.is_empty() {
return Ok("请提供 content 参数".to_string());
}
return Ok(memory_store.write_for(&user_id, content).await);
}
if n == "read_summaries" { return Ok(context::tools::read_summaries(&session).await); }
if n == "read_summaries" {
return Ok(context::tools::read_summaries(&session).await);
}
// query_capabilities: 合并内置 + 外部(去重)
// query_capabilities: 列出所有内置工具
if n == "query_capabilities" {
let spec_list = if let Some(ref r) = reg {
let mut s: Vec<tools::skill::SkillSpec> = r.list_specs().into_iter().cloned().collect();
for builtin in tools::builtin::BuiltinRegistry::specs() {
s.retain(|x| x.name != builtin.name);
s.push(builtin);
}
s
} else {
tools::builtin::BuiltinRegistry::specs()
};
return Ok(build_capability_list(&spec_list));
let specs = tools::builtin::BuiltinRegistry::specs();
return Ok(build_capability_list(&specs));
}
// 确定目标工具名和参数(call_capability 提取 name + 透传完整参数)
let (target_name, target_args) = if n == "call_capability" {
let cp: serde_json::Value = serde_json::from_str(&args).unwrap_or_default();
let name = cp.get("name").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or_default();
let name = cp
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_default();
(name, serde_json::to_string(&cp).unwrap_or_default())
} else { (n.clone(), args.clone()) };
} else {
(n.clone(), args.clone())
};
// 内置工具
if tools::builtin::BuiltinRegistry::is_builtin(&target_name) {
@@ -377,25 +363,18 @@ async fn cmd_listen(
ctx = ctx.with_wechat(send.clone());
let approved = builtin_approve(&ctx, &target_name).await;
match approved {
Ok(true) => {},
Ok(true) => {}
Ok(false) => return Ok(SkillResult::rejected(&target_name).output),
Err(e) => return Ok(e),
}
}
if let Some(result) = tools::builtin::BuiltinRegistry::execute(&target_name, &target_args).await {
if let Some(result) =
tools::builtin::BuiltinRegistry::execute(&target_name, &target_args).await
{
return Ok(result.output);
}
}
// 回退到外部 SkillRegistry
if let Some(ref r) = reg {
let params: serde_json::Value = serde_json::from_str(&target_args).unwrap_or(serde_json::json!({}));
let mut ctx = ExecutionContext::new(&user_id);
ctx = ctx.with_approval(approval.clone());
ctx = ctx.with_wechat(send);
let result = r.execute(&target_name, params, &ctx).await;
return Ok(result.output);
}
Ok(format!("未知工具: {}", target_name))
})
});
@@ -533,7 +512,8 @@ async fn listen_loop(
}
// 审批回复检查
let is_approval_reply = approval_manager.handle_reply(from, text).await.is_some();
let is_approval_reply =
approval_manager.handle_reply(from, text).await.is_some();
if is_approval_reply {
info!("消息匹配审批确认码,不传给 LLM");
@@ -579,7 +559,11 @@ async fn listen_loop(
} else {
generate_reply(&conv, text_owned, &database).await
};
let reply = reply_result.ok().unwrap_or_default();
let reply = match reply_result {
Ok(r) if !r.trim().is_empty() => r,
Ok(_) => "抱歉,生成回复为空,请重试。".to_string(),
Err(e) => format!("处理消息时出错:{:.200}", e),
};
match client
.send_text(&from_owned, &reply, ctx_owned.as_deref())
.await
@@ -831,12 +815,6 @@ async fn cmd_send(
}
}
fn global_skills_dir() -> std::path::PathBuf {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".ias")
.join("skills")
}
use tools::skill::SkillResult;
@@ -852,7 +830,9 @@ async fn builtin_approve(ctx: &ExecutionContext, name: &str) -> Result<bool, Str
name, code
);
if let Some(ref send) = ctx.send_wechat {
if let Err(e) = send(&ctx.user_id, &msg).await { return Err(e); }
if let Err(e) = send(&ctx.user_id, &msg).await {
return Err(e);
}
}
match tokio::time::timeout(std::time::Duration::from_secs(300), rx).await {
@@ -864,130 +844,27 @@ async fn builtin_approve(ctx: &ExecutionContext, name: &str) -> Result<bool, Str
fn build_capability_list(specs: &[tools::skill::SkillSpec]) -> String {
let mut lines = vec!["📋 可用工具:".to_string()];
for spec in specs {
let risk = if spec.risk_level == tools::skill::RiskLevel::High { " ⚠️需确认" } else { "" };
let risk = if spec.risk_level == tools::skill::RiskLevel::High {
" ⚠️需确认"
} else {
""
};
lines.push(format!(
"\n🔹 {} {}{}", spec.name, risk, spec.description
"\n🔹 {} {}{}",
spec.name, risk, spec.description
));
if !spec
.parameters
.get("properties")
.and_then(|v| v.as_object())
.map_or(true, |o| o.is_empty())
{
lines.push(format!(
" 参数: {}",
serde_json::to_string(&spec.parameters).unwrap_or_default()
));
}
}
lines.join("\n")
}
async fn cmd_skills(action: SkillsAction) {
use dialoguer::{MultiSelect, theme::ColorfulTheme};
use console::Term;
let global_dir = global_skills_dir();
match action {
SkillsAction::List => {
let discovered = skills_importer::discover_skills();
if discovered.is_empty() {
println!("未发现可导入的 Skill");
return;
}
println!("发现 {} 个可导入的 Skill:", discovered.len());
for (source, name, _path) in &discovered {
println!(" [{source}] {name}");
}
}
SkillsAction::Import => {
let discovered = skills_importer::discover_skills();
if discovered.is_empty() {
println!("未发现可导入的 Skill");
return;
}
let items: Vec<String> = discovered.iter()
.map(|(src, name, _)| format!("[{}] {}", src, name))
.collect();
println!("发现 {} 个可导入的 Skill(空格选择,回车确认,q 退出):", discovered.len());
let selection = MultiSelect::with_theme(&ColorfulTheme::default())
.items(&items)
.interact_on(&Term::stderr())
.unwrap_or_default();
if selection.is_empty() {
println!("未选择任何 Skill");
return;
}
let dest = &global_dir;
std::fs::create_dir_all(dest).ok();
let mut imported = 0;
let mut skipped = 0;
for idx in selection {
let (src, name, path) = &discovered[idx];
println!(" 导入 [{src}] {name}...");
match skills_importer::copy_skill(path, name, &dest) {
Ok(_) => {
println!(" ✅ 已导入");
imported += 1;
}
Err(e) => {
println!(" ⚠️ {e}");
skipped += 1;
}
}
}
println!("\n完成:导入 {imported} 个,跳过 {skipped} 个 (→ {})", global_dir.display());
}
SkillsAction::Delete => {
if !global_dir.exists() {
println!("全局 skills 目录不存在,没有可删除的 Skill");
return;
}
let mut entries: Vec<(String, String)> = Vec::new();
if let Ok(dir) = std::fs::read_dir(&global_dir) {
for entry in dir.flatten() {
let path = entry.path();
if path.is_dir() && path.join("SKILL.md").exists() {
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
let desc = std::fs::read_to_string(path.join("SKILL.md"))
.ok()
.and_then(|s| s.lines().nth(0).map(|l| l.trim_start_matches("# ").to_string()))
.unwrap_or_default();
entries.push((name, desc));
}
}
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
if entries.is_empty() {
println!("skills 目录下没有可删除的 Skill");
return;
}
let items: Vec<String> = entries.iter()
.map(|(name, desc)| format!("{}{}", name, desc))
.collect();
println!("选择要删除的 Skill(空格选择,回车确认,q 退出):");
let selection = MultiSelect::with_theme(&ColorfulTheme::default())
.items(&items)
.interact_on(&Term::stderr())
.unwrap_or_default();
if selection.is_empty() {
println!("未选择任何 Skill");
return;
}
let mut deleted = 0;
for idx in selection {
let (name, _) = &entries[idx];
let path = global_dir.join(name);
match std::fs::remove_dir_all(&path) {
Ok(_) => {
println!(" ✅ 已删除 {name}");
deleted += 1;
}
Err(e) => println!(" ❌ 删除 {name} 失败: {e}"),
}
}
println!("\n完成:删除 {deleted}");
}
}
}