fix: 5 项问题修复

1. (High) call_capability 路由内置工具 — 优先 BuiltinRegistry
2. (High) 内置 High 工具走审批 — builtin_approve 独立检查
3. (Medium) query_capabilities 去重 — 内置覆盖外部同名
4. (Medium) manage_memos 错误处理 — .ok() → 检查 Result
5. (Low) 重复 file_state — 删除第二个声明
This commit is contained in:
2026-06-02 15:15:40 +08:00
parent 079150b1d0
commit e5c288ac6f
2 changed files with 79 additions and 52 deletions
+74 -50
View File
@@ -15,7 +15,7 @@ use context::MemoryStore;
use db::Database; use db::Database;
use llm::{Conversation, ConversationConfig, DEFAULT_SYSTEM_PROMPT, ToolExecutor, Usage}; use llm::{Conversation, ConversationConfig, DEFAULT_SYSTEM_PROMPT, ToolExecutor, Usage};
use std::sync::Arc; use std::sync::Arc;
use tools::approval::ApprovalManager; use tools::approval::{ApprovalDecision, ApprovalManager};
use tools::registry::{ExecutionContext, SkillRegistry}; use tools::registry::{ExecutionContext, SkillRegistry};
use tracing::{error, info}; use tracing::{error, info};
use wechat::client::WeChatClient; use wechat::client::WeChatClient;
@@ -60,9 +60,6 @@ async fn main() {
)); ));
memory_store.load("").await; memory_store.load("").await;
// 文件状态管理器(降级方案)
let file_state = state::StateManager::new(".data/weixin-ilink");
match cli.command { match cli.command {
Commands::Login { timeout } => { Commands::Login { timeout } => {
cmd_login(timeout, &database, &file_state).await; cmd_login(timeout, &database, &file_state).await;
@@ -343,59 +340,63 @@ async fn cmd_listen(
Box::pin(async move { Box::pin(async move {
let user_id = session.lock().await.current_user_id.clone(); let user_id = session.lock().await.current_user_id.clone();
// 内置工具优先 // 上下文工具直接处理
if let Some(result) = tools::builtin::BuiltinRegistry::execute(&n, &args).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()); }
return Ok(memory_store.write_for(&user_id, content).await);
}
if n == "read_summaries" { return Ok(context::tools::read_summaries(&session).await); }
// 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));
}
// 确定目标工具名(call_capability 需要提取 name 参数)
let target_name = if n == "call_capability" {
let cp: serde_json::Value = serde_json::from_str(&args).unwrap_or_default();
cp.get("name").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or_default()
} else { n.clone() };
// 内置工具
if let Some(mut result) = tools::builtin::BuiltinRegistry::execute(&target_name, &args).await {
if tools::builtin::BuiltinRegistry::is_high_risk(&target_name) {
let mut ctx = ExecutionContext::new(&user_id);
ctx = ctx.with_approval(approval.clone());
ctx = ctx.with_wechat(send);
let approved = builtin_approve(&ctx, &target_name).await;
match approved {
Ok(true) => return Ok(result.output),
Ok(false) => return Ok(SkillResult::rejected(&target_name).output),
Err(e) => return Ok(e),
}
}
return Ok(result.output); return Ok(result.output);
} }
match n.as_str() { // 回退到外部 SkillRegistry
"read_memories" => return Ok(memory_store.read_for(&user_id).await), if let Some(ref r) = reg {
"write_memory" => { let params: serde_json::Value = serde_json::from_str(&args).unwrap_or(serde_json::json!({}));
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());
}
return Ok(memory_store.write_for(&user_id, content).await);
}
"read_summaries" => return Ok(context::tools::read_summaries(&session).await),
"query_capabilities" => {
if let Some(r) = reg.as_ref() {
let mut specs: Vec<tools::skill::SkillSpec> = r.list_specs().into_iter().cloned().collect();
specs.extend(tools::builtin::BuiltinRegistry::specs());
return Ok(build_capability_list(&specs));
}
return Ok("暂无工具".to_string());
}
"call_capability" => {
let cap_params: serde_json::Value = serde_json::from_str(&args).unwrap_or_default();
let cap_name = cap_params.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
if let Some(name) = cap_name {
if let Some(ref reg) = reg {
let mut ctx = ExecutionContext::new(&user_id);
ctx = ctx.with_approval(approval.clone());
ctx = ctx.with_wechat(send);
let result = reg.execute(&name, cap_params, &ctx).await;
return Ok(result.output);
}
}
return Ok("请提供 name 参数(要调用的 skill 名称)".to_string());
}
_ => {}
}
let user_id = current_user.lock().await.clone();
if let Some(ref reg) = reg {
let params: serde_json::Value =
serde_json::from_str(&args).unwrap_or(serde_json::json!({}));
let mut ctx = ExecutionContext::new(&user_id); let mut ctx = ExecutionContext::new(&user_id);
ctx = ctx.with_approval(approval.clone()); ctx = ctx.with_approval(approval.clone());
ctx = ctx.with_wechat(send); ctx = ctx.with_wechat(send);
let result = reg.execute(&n, params, &ctx).await; let result = r.execute(&target_name, params, &ctx).await;
Ok(result.output) return Ok(result.output);
} else {
Ok(format!("未知工具: {}", n))
} }
Ok(format!("未知工具: {}", target_name))
}) })
}); });
@@ -808,6 +809,29 @@ fn global_skills_dir() -> std::path::PathBuf {
.join("skills") .join("skills")
} }
use tools::skill::SkillResult;
async fn builtin_approve(ctx: &ExecutionContext, name: &str) -> Result<bool, String> {
let manager = match ctx.approval_manager.as_ref() {
Some(m) => m.clone(),
None => return Ok(true),
};
let (code, rx) = manager.create(&ctx.user_id, name).await?;
let msg = format!(
"⚠️ 操作确认\n\n技能:{}\n\n确认码:{}\n\n回复确认码以继续\n回复 0 或「取消」以取消\n(5分钟内有效,最多3次尝试)",
name, code
);
if let Some(ref send) = ctx.send_wechat {
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 {
Ok(Ok(ApprovalDecision::Approved)) => Ok(true),
_ => Ok(false),
}
}
fn build_capability_list(specs: &[tools::skill::SkillSpec]) -> String { fn build_capability_list(specs: &[tools::skill::SkillSpec]) -> String {
let mut lines = vec!["📋 可用工具:".to_string()]; let mut lines = vec!["📋 可用工具:".to_string()];
for spec in specs { for spec in specs {
+5 -2
View File
@@ -5,7 +5,6 @@ use chrono::Local;
pub struct BuiltinRegistry; pub struct BuiltinRegistry;
impl BuiltinRegistry { impl BuiltinRegistry {
/// 执行内置工具,返回 Some(SkillResult) 或 None(不是内置工具)
pub async fn execute(name: &str, args_json: &str) -> Option<SkillResult> { pub async fn execute(name: &str, args_json: &str) -> Option<SkillResult> {
match name { match name {
"get_current_datetime" => Some(execute_datetime()), "get_current_datetime" => Some(execute_datetime()),
@@ -33,6 +32,10 @@ impl BuiltinRegistry {
}, },
] ]
} }
pub fn is_high_risk(name: &str) -> bool {
Self::specs().iter().any(|s| s.name == name && s.risk_level == RiskLevel::High)
}
} }
fn execute_datetime() -> SkillResult { fn execute_datetime() -> SkillResult {
@@ -44,7 +47,7 @@ async fn handle_memos(args_json: &str) -> SkillResult {
let params: serde_json::Value = serde_json::from_str(args_json).unwrap_or_default(); let params: serde_json::Value = serde_json::from_str(args_json).unwrap_or_default();
let action = params.get("action").and_then(|v| v.as_str()).unwrap_or("list"); let action = params.get("action").and_then(|v| v.as_str()).unwrap_or("list");
let path = ".data/memos.json"; let path = ".data/memos.json";
std::fs::create_dir_all(".data").ok(); if let Err(e) = std::fs::create_dir_all(".data") { return SkillResult::error(format!("创建目录失败: {}", e)); }
let mut data: Vec<serde_json::Value> = if std::path::Path::new(path).exists() { let mut data: Vec<serde_json::Value> = if std::path::Path::new(path).exists() {
serde_json::from_str(&std::fs::read_to_string(path).unwrap_or_default()).unwrap_or_default() serde_json::from_str(&std::fs::read_to_string(path).unwrap_or_default()).unwrap_or_default()