feat: add tool manager capability

This commit is contained in:
2026-06-22 10:30:12 +08:00
parent 99c1e9cd6c
commit 32c4fcf3a8
11 changed files with 590 additions and 46 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
set -e
cd "$(dirname "$0")"
TOOLS=(datetime weather amap web_search fetch_page memories)
TOOLS=(datetime weather amap web_search fetch_page memories tool_manager)
echo "=== 构建工具 ==="
for tool in "${TOOLS[@]}"; do
+105
View File
@@ -0,0 +1,105 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tool_manager"
version = "0.1.0"
dependencies = [
"serde_json",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "tool_manager"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "tool_manager"
path = "src/main.rs"
[dependencies]
serde_json = "1.0"
@@ -0,0 +1,32 @@
name: tool_manager
desc: 创建、修改和构建本地能力工具。用于给助手新增或更新 tools 目录下的 stdio Rust 工具;高风险操作,执行前需要用户确认。
type: stdio
tool: tool_manager
path: /target/release/tool_manager
risk_level: high
timeout_secs: 120
params:
- name: action
required: true
desc: 操作类型:create_tool、update_tool 或 build_tool
- name: tool_name
required: true
desc: 工具名称,只允许字母、数字、下划线和短横线
- name: description
required: false
desc: 工具说明,创建或更新 spec 时使用
- name: rust_code
required: false
desc: 完整的 src/main.rs Rust 源码;省略时 create_tool 会生成默认模板
- name: params
required: false
desc: 参数列表数组,每项包含 name、required、desc
- name: risk_level
required: false
desc: 新工具风险级别,low 或 high,默认 low
- name: timeout_secs
required: false
desc: 新工具超时时间秒数,默认 30
- name: overwrite
required: false
desc: create_tool 目标已存在时是否覆盖,默认 false
+258
View File
@@ -0,0 +1,258 @@
//! tool_manager — 受控创建、修改、构建本地工具
use serde_json::{Value, json};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
let input: Value = match serde_json::from_reader(std::io::stdin()) {
Ok(v) => v,
Err(e) => return result(&format!("读取输入失败: {e}")),
};
let params = &input["params"];
let action = params["action"].as_str().unwrap_or("");
let tool_name = params["tool_name"].as_str().unwrap_or("");
let output = match run(action, tool_name, params) {
Ok(msg) => msg,
Err(e) => format!("工具管理失败: {e}"),
};
result(&output);
}
fn run(action: &str, tool_name: &str, params: &Value) -> Result<String, String> {
validate_tool_name(tool_name)?;
let tools_root = locate_tools_root()?;
let tool_dir = tools_root.join(tool_name);
match action {
"create_tool" => {
let overwrite = truthy(params.get("overwrite"));
if tool_dir.exists() && !overwrite {
return Err(format!(
"工具 {tool_name} 已存在;如需覆盖,请设置 overwrite=true"
));
}
write_tool_project(&tool_dir, tool_name, params)?;
build_tool(&tool_dir, tool_name)?;
Ok(format!(
"已创建并构建工具 {tool_name}。可通过 query_capabilities 查看并用 call_capability 调用。"
))
}
"update_tool" => {
if !tool_dir.exists() {
return Err(format!("工具 {tool_name} 不存在,无法修改"));
}
update_tool_project(&tool_dir, tool_name, params)?;
build_tool(&tool_dir, tool_name)?;
Ok(format!(
"已修改并构建工具 {tool_name}。注册表重载后即可使用最新版本。"
))
}
"build_tool" => {
if !tool_dir.exists() {
return Err(format!("工具 {tool_name} 不存在,无法构建"));
}
build_tool(&tool_dir, tool_name)?;
Ok(format!("已构建工具 {tool_name}"))
}
_ => Err("action 必须是 create_tool、update_tool 或 build_tool".to_string()),
}
}
fn write_tool_project(tool_dir: &Path, tool_name: &str, params: &Value) -> Result<(), String> {
fs::create_dir_all(tool_dir.join("src")).map_err(|e| e.to_string())?;
fs::create_dir_all(tool_dir.join("specs")).map_err(|e| e.to_string())?;
fs::write(tool_dir.join("Cargo.toml"), cargo_toml(tool_name)).map_err(|e| e.to_string())?;
fs::write(tool_dir.join("src/main.rs"), rust_code(tool_name, params))
.map_err(|e| e.to_string())?;
fs::write(
tool_dir
.join("specs")
.join(format!("{tool_name}.tool.yaml")),
spec_yaml(tool_name, params),
)
.map_err(|e| e.to_string())?;
Ok(())
}
fn update_tool_project(tool_dir: &Path, tool_name: &str, params: &Value) -> Result<(), String> {
if let Some(code) = params["rust_code"].as_str() {
fs::write(tool_dir.join("src/main.rs"), code).map_err(|e| e.to_string())?;
}
let should_update_spec = params.get("description").is_some()
|| params.get("params").is_some()
|| params.get("risk_level").is_some()
|| params.get("timeout_secs").is_some();
if should_update_spec {
fs::create_dir_all(tool_dir.join("specs")).map_err(|e| e.to_string())?;
fs::write(
tool_dir
.join("specs")
.join(format!("{tool_name}.tool.yaml")),
spec_yaml(tool_name, params),
)
.map_err(|e| e.to_string())?;
}
Ok(())
}
fn build_tool(tool_dir: &Path, tool_name: &str) -> Result<(), String> {
let output = Command::new("cargo")
.arg("build")
.arg("--release")
.current_dir(tool_dir)
.output()
.map_err(|e| format!("启动 cargo 失败: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("cargo build 失败: {}", stderr.trim()));
}
let binary = tool_dir.join("target/release").join(tool_name);
if !binary.exists() {
return Err(format!("构建后未找到二进制: {}", binary.display()));
}
Ok(())
}
fn locate_tools_root() -> Result<PathBuf, String> {
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
let tool_dir = exe
.parent()
.and_then(Path::parent)
.and_then(Path::parent)
.ok_or_else(|| "无法定位 tool_manager 目录".to_string())?;
tool_dir
.parent()
.map(Path::to_path_buf)
.ok_or_else(|| "无法定位 tools 根目录".to_string())
}
fn cargo_toml(tool_name: &str) -> String {
format!(
r#"[package]
name = "{tool_name}"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "{tool_name}"
path = "src/main.rs"
[dependencies]
serde_json = "1.0"
"#
)
}
fn rust_code(tool_name: &str, params: &Value) -> String {
params["rust_code"]
.as_str()
.map(str::to_string)
.unwrap_or_else(|| default_rust_code(tool_name))
}
fn default_rust_code(tool_name: &str) -> String {
format!(
r#"//! {tool_name} — generated tool
fn main() {{
let input: serde_json::Value = serde_json::from_reader(std::io::stdin()).unwrap_or_default();
let content = format!("工具 {tool_name} 已收到参数: {{}}", input["params"]);
println!("{{}}", serde_json::json!({{"type":"result","content":content}}));
}}
"#
)
}
fn spec_yaml(tool_name: &str, params: &Value) -> String {
let desc = params["description"]
.as_str()
.unwrap_or("由 tool_manager 创建的本地工具");
let risk_level = match params["risk_level"].as_str() {
Some("high") => "high",
_ => "low",
};
let timeout_secs = params["timeout_secs"].as_u64().unwrap_or(30).clamp(1, 300);
let mut out = format!(
"name: {tool_name}\n\
desc: {}\n\
type: stdio\n\
tool: {tool_name}\n\
path: /target/release/{tool_name}\n\
risk_level: {risk_level}\n\
timeout_secs: {timeout_secs}\n",
yaml_scalar(desc)
);
if let Some(items) = params["params"].as_array() {
out.push_str("params:\n");
for item in items {
let Some(name) = item["name"].as_str() else {
continue;
};
if validate_param_name(name).is_err() {
continue;
}
let required = item["required"].as_bool().unwrap_or(false);
let desc = item["desc"].as_str().unwrap_or("");
out.push_str(&format!(
" - name: {name}\n required: {required}\n desc: {}\n",
yaml_scalar(desc)
));
}
} else {
out.push_str("params: []\n");
}
out
}
fn validate_tool_name(name: &str) -> Result<(), String> {
if name.is_empty() {
return Err("缺少 tool_name".to_string());
}
if matches!(name, "target" | "specs" | "src" | "." | "..") {
return Err("tool_name 使用了保留名称".to_string());
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err("tool_name 只允许字母、数字、下划线和短横线".to_string());
}
Ok(())
}
fn validate_param_name(name: &str) -> Result<(), String> {
if name.is_empty()
|| !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err("参数名非法".to_string());
}
Ok(())
}
fn yaml_scalar(value: &str) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
}
fn truthy(value: Option<&Value>) -> bool {
match value {
Some(Value::Bool(v)) => *v,
Some(Value::String(v)) => matches!(v.as_str(), "true" | "1" | "yes" | ""),
_ => false,
}
}
fn result(content: &str) {
println!("{}", json!({"type":"result","content":content}));
}