Added tool implementation for reading tools.

This commit is contained in:
2026-06-23 20:36:16 +08:00
parent 132089c8eb
commit 3476d8cbcb
6 changed files with 441 additions and 11 deletions
+4 -1
View File
@@ -478,7 +478,10 @@ pub const DEFAULT_SYSTEM_PROMPT: &str = "\
1. `query_capabilities` — 查询所有可用能力工具\n\ 1. `query_capabilities` — 查询所有可用能力工具\n\
2. `call_capability` — 调用能力工具,参数 {\"name\":\"工具名\",\"prompt\":{\"参数\":\"\"}}\n\ 2. `call_capability` — 调用能力工具,参数 {\"name\":\"工具名\",\"prompt\":{\"参数\":\"\"}}\n\
\n\ \n\
流程:收到消息 → query_capabilities → call_capability → 回复\n\ 流程:收到消息 → query_capabilities → call_capability → 回复
\
\
需要创建或修改本地工具时:先用 `tool_inspector`action=read_tool)查看现有实现,再用 `tool_manager` 修改。`tool_manager` 是高风险操作,执行前会请求用户确认。\n\
"; ";
#[cfg(test)] #[cfg(test)]
+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.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
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_inspector"
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_inspector"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "tool_inspector"
path = "src/main.rs"
[dependencies]
serde_json = "1.0"
@@ -0,0 +1,17 @@
name: tool_inspector
desc: 只读查看本地工具的源码、Cargo.toml 和 spec 配置。在用 tool_manager 修改某工具前,先用本工具查看其当前实现,避免盲目覆盖丢失逻辑。低风险,无需审批
type: stdio
tool: tool_inspector
path: /target/release/tool_inspector
risk_level: low
timeout_secs: 10
params:
- name: action
required: true
desc: 操作类型:list(列出所有工具及文件构成)、read_tool(读取某工具的全部 Cargo.toml/specs/src 源码)、read_file(读取工具目录下单个文件)
- name: tool_name
required: false
desc: 工具名称(read_tool/read_file 必填),只允许字母、数字、下划线和短横线
- name: file
required: false
desc: read_file 时的相对路径,如 src/main.rs、Cargo.toml、specs/weather.tool.yaml;必须是相对路径,禁止 .. 和 target 目录
+270
View File
@@ -0,0 +1,270 @@
//! tool_inspector — 只读查看本地工具的源码与配置
//!
//! 用途:在用 tool_manager 修改工具前,先用本工具查看当前实现,避免盲目覆盖丢失逻辑。
//!
//! 安全约束:
//! - 纯只读:不写文件、不执行命令、不联网
//! - tool_name 与相对路径严格校验,禁止路径穿越
//! - 跳过 target/ 构建产物目录
//! - 低风险(risk_level: low),无需用户审批
use serde_json::{Value, json};
use std::fs;
use std::path::{Component, Path, PathBuf};
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 out = match run(action, params) {
Ok(s) => s,
Err(e) => format!("查看失败: {e}"),
};
result(&out);
}
fn run(action: &str, params: &Value) -> Result<String, String> {
let tools_root = locate_tools_root()?;
match action {
"list" => list_tools(&tools_root),
"read_tool" => {
let name = required_str(params, "tool_name")?;
validate_tool_name(name)?;
read_tool(&tools_root, name)
}
"read_file" => {
let name = required_str(params, "tool_name")?;
validate_tool_name(name)?;
let file = required_str(params, "file")?;
read_file(&tools_root, name, file)
}
_ => Err("action 必须是 list、read_tool 或 read_file".to_string()),
}
}
/// 列出 tools 目录下所有工具及其文件构成
fn list_tools(tools_root: &Path) -> Result<String, String> {
let mut names: Vec<String> = Vec::new();
for entry in fs::read_dir(tools_root).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = entry.file_name().to_string_lossy().to_string();
// 跳过产物目录与隐藏目录
if name == "bin" || name.starts_with('.') {
continue;
}
// 只有含 specs/src/Cargo.toml 之一的目录才视为工具
if path.join("specs").exists()
|| path.join("src").exists()
|| path.join("Cargo.toml").exists()
{
names.push(name);
}
}
names.sort();
if names.is_empty() {
return Ok("tools 目录下没有工具".into());
}
let mut out = format!("本地工具列表({} 个):\n", names.len());
for n in &names {
let dir = tools_root.join(n);
let mark = |has: bool| if has { "" } else { "" };
out.push_str(&format!(
"{n} (src:{} specs:{} Cargo.toml:{})\n",
mark(dir.join("src").exists()),
mark(dir.join("specs").exists()),
mark(dir.join("Cargo.toml").exists()),
));
}
out.push_str("\n用 read_tool 查看某工具的全部源码与配置,或用 read_file 读取单个文件。");
Ok(out)
}
/// 一次性读取某工具的 Cargo.toml + 所有 specs + src 下所有 .rs 文件
fn read_tool(tools_root: &Path, name: &str) -> Result<String, String> {
let tool_dir = tools_root.join(name);
if !tool_dir.exists() {
return Err(format!("工具 {name} 不存在"));
}
let mut out = String::new();
if let Ok(c) = fs::read_to_string(tool_dir.join("Cargo.toml")) {
out.push_str(&format!("===== {name}/Cargo.toml =====\n{c}\n\n"));
}
// specs/*.tool.yaml(可能有多个文件),按文件名排序
let specs_dir = tool_dir.join("specs");
if specs_dir.exists() {
let mut specs: Vec<PathBuf> = fs::read_dir(&specs_dir)
.map_err(|e| e.to_string())?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| matches!(p.extension().and_then(|e| e.to_str()), Some("yaml" | "yml")))
.collect();
specs.sort();
for spec in specs {
let rel = spec.strip_prefix(&tool_dir).unwrap_or(&spec).display().to_string();
match fs::read_to_string(&spec) {
Ok(c) => out.push_str(&format!("===== {name}/{rel} =====\n{c}\n\n")),
Err(e) => out.push_str(&format!("===== {name}/{rel} =====\n(读取失败: {e}\n\n")),
}
}
}
// src 下所有 .rs 文件(递归,支持多模块工具)
let mut src_files: Vec<PathBuf> = Vec::new();
collect_rs_files(&tool_dir.join("src"), &mut src_files)?;
if src_files.is_empty() {
out.push_str(&format!("{name} 无 src/*.rs 文件)\n"));
} else {
src_files.sort();
for f in src_files {
let rel = f.strip_prefix(&tool_dir).unwrap_or(&f).display().to_string();
match fs::read_to_string(&f) {
Ok(c) => out.push_str(&format!("===== {name}/{rel} =====\n{c}\n\n")),
Err(e) => out.push_str(&format!("===== {name}/{rel} =====\n(读取失败: {e}\n\n")),
}
}
}
if out.is_empty() {
return Err(format!("工具 {name} 没有可读的源码或配置"));
}
Ok(out.trim_end().to_string())
}
/// 读取工具目录下指定的单个文件(严格防路径穿越)
fn read_file(tools_root: &Path, name: &str, file: &str) -> Result<String, String> {
let tool_dir = tools_root.join(name);
if !tool_dir.exists() {
return Err(format!("工具 {name} 不存在"));
}
let rel = sanitize_rel_path(file)?;
let target = tool_dir.join(&rel);
// 规范化后必须仍在 tool_dir 内(双重防穿越)
let canonical_tool = tool_dir.canonicalize().map_err(|e| e.to_string())?;
let canonical_target = target
.canonicalize()
.map_err(|_| format!("文件不存在: {file}"))?;
if !canonical_target.starts_with(&canonical_tool) {
return Err("路径越界,禁止访问工具目录之外的文件".into());
}
if canonical_target.is_dir() {
return Err(format!("{file} 是目录,请用 read_tool 查看或指定具体文件"));
}
let content = fs::read_to_string(&canonical_target)
.map_err(|e| format!("读取 {file} 失败: {e}"))?;
Ok(format!("===== {name}/{file} =====\n{content}"))
}
/// 递归收集 .rs 文件,跳过隐藏目录
fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), String> {
if !dir.exists() {
return Ok(());
}
for entry in fs::read_dir(dir).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if path.is_dir() {
if !entry.file_name().to_string_lossy().starts_with('.') {
collect_rs_files(&path, out)?;
}
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
out.push(path);
}
}
Ok(())
}
/// 校验相对路径:禁止绝对路径、禁止 ..、禁止 target 构建产物
fn sanitize_rel_path(file: &str) -> Result<PathBuf, String> {
if file.is_empty() {
return Err("file 不能为空".into());
}
let p = PathBuf::from(file);
for comp in p.components() {
match comp {
Component::Normal(_) | Component::CurDir => {}
Component::ParentDir => return Err("file 禁止包含 ..".into()),
Component::RootDir | Component::Prefix(_) => {
return Err("file 必须是相对路径".into())
}
}
}
if p.starts_with("target") {
return Err("禁止读取 target 构建产物目录".into());
}
Ok(p)
}
fn validate_tool_name(name: &str) -> Result<(), String> {
if name.is_empty() {
return Err("缺少 tool_name".into());
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err("tool_name 只允许字母、数字、下划线和短横线".into());
}
Ok(())
}
fn required_str<'a>(params: &'a Value, key: &str) -> Result<&'a str, String> {
params[key]
.as_str()
.filter(|s| !s.is_empty())
.ok_or_else(|| format!("缺少参数 {key}"))
}
/// 定位 tools 根目录。
///
/// 产物可能位于两处之一:
/// - `tools/bin/tool_inspector`(统一产物目录,registry 优先使用)
/// - `tools/tool_inspector/target/release/tool_inspector`(构建产物)
///
/// 因此不依赖固定向上层数,而是从 exe 向上找第一个“含 bin/ 子目录且至少含一个
/// 工具子目录”的祖先,即为 tools 根。对两种位置均正确。
fn locate_tools_root() -> Result<PathBuf, String> {
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
let mut cur = exe.parent();
while let Some(dir) = cur {
if dir.join("bin").is_dir() && has_tool_subdir(dir) {
return Ok(dir.to_path_buf());
}
cur = dir.parent();
}
Err("无法定位 tools 根目录".into())
}
/// 判断目录下是否存在至少一个工具子目录(含 specs/Cargo.toml/src 之一)
fn has_tool_subdir(dir: &Path) -> bool {
let Ok(entries) = fs::read_dir(dir) else {
return false;
};
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir()
&& (p.join("specs").exists()
|| p.join("Cargo.toml").exists()
|| p.join("src").exists())
{
return true;
}
}
false
}
fn result(content: &str) {
println!("{}", json!({"type":"result","content":content}));
}
+34 -10
View File
@@ -188,18 +188,42 @@ fn build_tool(tool_dir: &Path, tool_name: &str) -> Result<(), String> {
Ok(()) Ok(())
} }
/// 定位 tools 根目录。
///
/// 产物可能位于两处之一:
/// - `tools/bin/tool_manager`(统一产物目录,registry 优先使用)
/// - `tools/tool_manager/target/release/tool_manager`(构建产物)
///
/// 因此不依赖固定向上层数,而是从 exe 向上找第一个“含 bin/ 子目录且至少含一个
/// 工具子目录”的祖先,即为 tools 根。对两种位置均正确。
fn locate_tools_root() -> Result<PathBuf, String> { fn locate_tools_root() -> Result<PathBuf, String> {
let exe = std::env::current_exe().map_err(|e| e.to_string())?; let exe = std::env::current_exe().map_err(|e| e.to_string())?;
// exe = tools/tool_manager/target/release/tool_manager let mut cur = exe.parent();
let tool_dir = exe while let Some(dir) = cur {
.parent() // .../target/release if dir.join("bin").is_dir() && has_tool_subdir(dir) {
.and_then(Path::parent) // .../target return Ok(dir.to_path_buf());
.and_then(Path::parent) // .../tool_manager }
.ok_or_else(|| "无法定位 tool_manager 目录".to_string())?; cur = dir.parent();
tool_dir }
.parent() Err("无法定位 tools 根目录".into())
.map(Path::to_path_buf) }
.ok_or_else(|| "无法定位 tools 根目录".to_string())
/// 判断目录下是否存在至少一个工具子目录(含 specs/Cargo.toml/src 之一)
fn has_tool_subdir(dir: &Path) -> bool {
let Ok(entries) = fs::read_dir(dir) else {
return false;
};
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir()
&& (p.join("specs").exists()
|| p.join("Cargo.toml").exists()
|| p.join("src").exists())
{
return true;
}
}
false
} }
fn cargo_toml(tool_name: &str) -> String { fn cargo_toml(tool_name: &str) -> String {