diff --git a/src/llm/conversation.rs b/src/llm/conversation.rs index 9b63e60..e0189bb 100644 --- a/src/llm/conversation.rs +++ b/src/llm/conversation.rs @@ -478,7 +478,10 @@ pub const DEFAULT_SYSTEM_PROMPT: &str = "\ 1. `query_capabilities` — 查询所有可用能力工具\n\ 2. `call_capability` — 调用能力工具,参数 {\"name\":\"工具名\",\"prompt\":{\"参数\":\"值\"}}\n\ \n\ -流程:收到消息 → query_capabilities → call_capability → 回复\n\ +流程:收到消息 → query_capabilities → call_capability → 回复 +\ +\ +需要创建或修改本地工具时:先用 `tool_inspector`(action=read_tool)查看现有实现,再用 `tool_manager` 修改。`tool_manager` 是高风险操作,执行前会请求用户确认。\n\ "; #[cfg(test)] diff --git a/tools/tool_inspector/Cargo.lock b/tools/tool_inspector/Cargo.lock new file mode 100644 index 0000000..8d2cc94 --- /dev/null +++ b/tools/tool_inspector/Cargo.lock @@ -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" diff --git a/tools/tool_inspector/Cargo.toml b/tools/tool_inspector/Cargo.toml new file mode 100644 index 0000000..ef8fb39 --- /dev/null +++ b/tools/tool_inspector/Cargo.toml @@ -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" diff --git a/tools/tool_inspector/specs/tool_inspector.tool.yaml b/tools/tool_inspector/specs/tool_inspector.tool.yaml new file mode 100644 index 0000000..aa34b70 --- /dev/null +++ b/tools/tool_inspector/specs/tool_inspector.tool.yaml @@ -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 目录 diff --git a/tools/tool_inspector/src/main.rs b/tools/tool_inspector/src/main.rs new file mode 100644 index 0000000..14c96a7 --- /dev/null +++ b/tools/tool_inspector/src/main.rs @@ -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 { + 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 { + let mut names: Vec = 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 { + 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 = 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 = 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 { + 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) -> 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 { + 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 { + 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})); +} diff --git a/tools/tool_manager/src/main.rs b/tools/tool_manager/src/main.rs index dbc98cb..a8348c8 100644 --- a/tools/tool_manager/src/main.rs +++ b/tools/tool_manager/src/main.rs @@ -188,18 +188,42 @@ fn build_tool(tool_dir: &Path, tool_name: &str) -> Result<(), String> { Ok(()) } +/// 定位 tools 根目录。 +/// +/// 产物可能位于两处之一: +/// - `tools/bin/tool_manager`(统一产物目录,registry 优先使用) +/// - `tools/tool_manager/target/release/tool_manager`(构建产物) +/// +/// 因此不依赖固定向上层数,而是从 exe 向上找第一个“含 bin/ 子目录且至少含一个 +/// 工具子目录”的祖先,即为 tools 根。对两种位置均正确。 fn locate_tools_root() -> Result { let exe = std::env::current_exe().map_err(|e| e.to_string())?; - // exe = tools/tool_manager/target/release/tool_manager - let tool_dir = exe - .parent() // .../target/release - .and_then(Path::parent) // .../target - .and_then(Path::parent) // .../tool_manager - .ok_or_else(|| "无法定位 tool_manager 目录".to_string())?; - tool_dir - .parent() - .map(Path::to_path_buf) - .ok_or_else(|| "无法定位 tools 根目录".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 cargo_toml(tool_name: &str) -> String {