Refactor tool execution pipeline

This commit is contained in:
2026-06-21 14:30:40 +08:00
parent 3e5f0658b3
commit 99c1e9cd6c
81 changed files with 3587 additions and 5317 deletions
+304
View File
@@ -0,0 +1,304 @@
//! # 工具注册器
//!
//! 扫描每个工具目录下的 `specs/*.tool.yaml`,按 `name` 路由分发。
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::context::MemoryStore;
use crate::tools::approval::ApprovalManager;
use crate::tools::executor;
use crate::tools::parser;
use crate::tools::spec::ToolSpec;
use serde_json::Value;
#[derive(Debug, Clone, serde::Serialize)]
pub struct ToolSummary {
pub name: String,
pub desc: String,
pub params: Vec<ParamSummary>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ParamSummary {
pub name: String,
pub required: bool,
pub desc: String,
}
/// 解析工具目录。相对路径基于二进制所在目录。
fn resolve_dir(dir: &str) -> PathBuf {
let path = Path::new(dir);
if path.is_absolute() {
return path.to_path_buf();
}
let exe_dir = std::env::current_exe()
.ok()
.and_then(|e| e.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| PathBuf::from("."));
let candidate = exe_dir.join(dir);
if candidate.exists() {
return candidate;
}
let dev = exe_dir.join("../..").join(dir);
if dev.exists() {
tracing::info!(target:"ias::registry","dev模式: {}", dev.display());
return dev;
}
path.to_path_buf()
}
pub struct Registry {
tools: HashMap<String, ToolSpec>,
}
impl Registry {
/// 扫描工具根目录下各子目录的 `specs/*.tool.yaml`。
pub fn load(dir: &str) -> Self {
let root = resolve_dir(dir);
let mut tools = HashMap::new();
let entries = match std::fs::read_dir(&root) {
Ok(e) => e,
Err(e) => {
tracing::warn!(target:"ias::registry","读取目录失败 {}: {}", root.display(), e);
return Self { tools };
}
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let specs_dir = path.join("specs");
if !specs_dir.exists() {
continue;
}
let result = parser::parse_dir(&specs_dir);
for (file, err) in &result.errors {
tracing::warn!(target:"ias::registry","解析失败 {}: {}", file, err);
}
for mut spec in result.specs {
// 解析二进制路径
let binary = match &spec.path {
Some(p) => {
let p = p.trim_start_matches('/');
path.join(p)
}
None => {
// 默认: {tool_dir}/target/release/{tool}
path.join("target/release").join(&spec.tool)
}
};
if !binary.exists() {
tracing::warn!(target:"ias::registry","{} 二进制不存在: {}", spec.name, binary.display());
continue;
}
// 将绝对路径存入 path 字段(executor 直接使用)
spec.path = Some(binary.to_string_lossy().to_string());
tracing::info!(target:"ias::registry","注册: {} ({})", spec.name, spec.r#type);
tools.insert(spec.name.clone(), spec);
}
}
Self { tools }
}
pub fn list_tools(&self) -> Vec<ToolSummary> {
self.tools
.values()
.map(|s| ToolSummary {
name: s.name.clone(),
desc: s.desc.clone(),
params: s
.params
.iter()
.map(|p| ParamSummary {
name: p.name.clone(),
required: p.required,
desc: p.desc.clone(),
})
.collect(),
})
.collect()
}
pub fn get(&self, name: &str) -> Option<&ToolSpec> {
self.tools.get(name)
}
pub async fn dispatch(
&self,
name: &str,
params: &Value,
user_id: &str,
approved: bool,
approval: &Arc<ApprovalManager>,
memory: &Arc<MemoryStore>,
wechat_send: &(
dyn Fn(&str, &str) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>
+ Send
+ Sync
),
) -> String {
let spec = match self.get(name) {
Some(s) => s,
None => {
tracing::warn!(target:"ias::registry","未知: {name}");
return format!("未知工具: {name}");
}
};
if let Some(err) = validate_required_params(spec, params) {
return err;
}
executor::execute_loop(
spec,
params,
user_id,
approved,
approval,
memory,
wechat_send,
)
.await
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.tools.len()
}
}
fn validate_required_params(spec: &ToolSpec, params: &Value) -> Option<String> {
let Some(obj) = params.as_object() else {
return Some(format!("工具 '{}' 参数必须是 JSON 对象", spec.name));
};
let missing: Vec<&str> = spec
.params
.iter()
.filter(|p| p.required)
.filter_map(|p| match obj.get(&p.name) {
Some(Value::Null) | None => Some(p.name.as_str()),
Some(Value::String(s)) if s.trim().is_empty() => Some(p.name.as_str()),
Some(_) => None,
})
.collect();
if missing.is_empty() {
None
} else {
Some(format!(
"缺少必填参数: {}(工具: {}",
missing.join(", "),
spec.name
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_load_specs_dir() {
let dir = std::env::temp_dir().join("ias_test_specs_dir");
let _ = std::fs::remove_dir_all(&dir);
let tool_dir = dir.join("mytool");
std::fs::create_dir_all(tool_dir.join("specs")).unwrap();
std::fs::create_dir_all(tool_dir.join("target/release")).unwrap();
std::fs::write(
tool_dir.join("specs/test.tool.yaml"),
r#"
name: test_tool
desc: 测试
type: http
tool: mytool
path: /target/release/mytool
"#,
)
.unwrap();
let mut f = std::fs::File::create(tool_dir.join("target/release/mytool")).unwrap();
f.write_all(b"fake").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = f.set_permissions(std::fs::Permissions::from_mode(0o755));
}
let registry = Registry::load(dir.to_str().unwrap());
assert_eq!(registry.len(), 1);
let tools = registry.list_tools();
assert_eq!(tools[0].name, "test_tool");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_load_real_tools() {
let dir = std::path::Path::new("tools");
if !dir.exists() {
return;
}
let registry = Registry::load("tools");
assert!(registry.len() >= 1, "至少 1 个工具");
}
#[test]
fn test_validate_required_params_missing() {
let spec = ToolSpec {
name: "weather_now".into(),
desc: "天气".into(),
r#type: "http".into(),
tool: "weather".into(),
command: None,
path: None,
risk_level: "low".into(),
timeout_secs: 10,
env: vec![],
params: vec![crate::tools::spec::ParamSpec {
name: "location".into(),
required: true,
desc: "城市".into(),
}],
};
let err = validate_required_params(&spec, &serde_json::json!({})).unwrap();
assert!(err.contains("location"));
}
#[test]
fn test_validate_required_params_present() {
let spec = ToolSpec {
name: "weather_now".into(),
desc: "天气".into(),
r#type: "http".into(),
tool: "weather".into(),
command: None,
path: None,
risk_level: "low".into(),
timeout_secs: 10,
env: vec![],
params: vec![crate::tools::spec::ParamSpec {
name: "location".into(),
required: true,
desc: "城市".into(),
}],
};
assert!(
validate_required_params(&spec, &serde_json::json!({"location": "北京"})).is_none()
);
}
}