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:
+62
-185
@@ -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} 个");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user