feat: add tool manager capability
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
Generated
+105
@@ -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"
|
||||
@@ -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
|
||||
@@ -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}));
|
||||
}
|
||||
Reference in New Issue
Block a user