diff --git a/src/tools/definitions.rs b/src/tools/definitions.rs deleted file mode 100644 index ca9dcfd..0000000 --- a/src/tools/definitions.rs +++ /dev/null @@ -1,393 +0,0 @@ -//! ## JSON 工具定义格式 -//! -//! 本模块定义了一种"声明式工具"格式 —— 只需要写一个 JSON 文件就能注册一个新工具, -//! 无需改 Rust 代码、重新编译。 -//! -//! ### 设计理念:像 curl + jq 一样描述工具 -//! -//! ```json -//! { -//! "name": "stocks", -//! "description": "股票行情查询", -//! "url": "https://api.example.com/stocks/{{code}}", -//! "method": "GET", -//! "headers": { "Authorization": "${env:API_KEY}" }, -//! "extract": "data.items", -//! "fields": ["name", "price"] -//! } -//! ``` -//! -//! ### 三要素 -//! 1. **URL** — API 端点模板(支持 `{{var}}` 和 `${param:name:type}` 传参) -//! 2. **取节点** — `extract` 字段指定 JSON 点路径(类似 jq) -//! 3. **选字段** — `fields` 字段只保留需要的字段 -//! -//! ### 参数推导 -//! 自动从 URL/Body/Headers 中推导参数定义(无需手动写 parameters 字段), -//! 也支持显式 `parameters` 声明以获得更精确的类型描述。 - -use std::collections::HashMap; - -/// HTTP 方法 -#[derive(Debug, Clone, PartialEq)] -pub enum HttpMethod { - Get, - Post, - Put, - Delete, -} - -#[allow(dead_code)] -impl HttpMethod { - pub fn as_str(&self) -> &'static str { - match self { - HttpMethod::Get => "GET", - HttpMethod::Post => "POST", - HttpMethod::Put => "PUT", - HttpMethod::Delete => "DELETE", - } - } -} - -impl Default for HttpMethod { - fn default() -> Self { - HttpMethod::Get - } -} - -impl<'de> serde::Deserialize<'de> for HttpMethod { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - match s.to_uppercase().as_str() { - "GET" => Ok(HttpMethod::Get), - "POST" => Ok(HttpMethod::Post), - "PUT" => Ok(HttpMethod::Put), - "DELETE" => Ok(HttpMethod::Delete), - other => Err(serde::de::Error::custom(format!( - "不支持的 HTTP 方法: {} (支持: GET/POST/PUT/DELETE)", - other - ))), - } - } -} - -/// ## JSON 工具定义(ToolDef) -/// -/// 描述一个通过 HTTP API 调用的外部工具。 -/// -/// ### 字段说明 -/// * `name` — 工具名称,用作 `call_capability` 的 name -/// * `description` — 工具描述(给 LLM 看的) -/// * `url` — URL 模板,支持 `{{var}}` 和 `${param:name:type}` 两种传参 -/// * `method` — HTTP 方法(GET/POST/PUT/DELETE),默认 GET -/// * `headers` — HTTP 请求头,值可包含 `${env:KEY}`(环境变量)、 -/// `${cmd:command}`(命令输出)、`${param:name:type}`(参数值) -/// * `body` — POST/PUT 的 JSON body 模板,同样支持变量替换 -/// * `parameters` — 显式参数声明(可选,未声明时自动推导) -/// * `extract` — JSON 提取路径,如 "data.items" -/// * `fields` — 筛选要保留的字段 -/// * `timeout` — 超时秒数,默认 30 -#[derive(Debug, Clone, serde::Deserialize)] -pub struct ToolDef { - /// 工具名称,用作 call_capability 的 name - pub name: String, - /// 工具描述 - pub description: String, - /// URL 模板,如 "https://api.example.com/stock/{{code}}" - /// 支持 {{var}} 和 ${param:name:type} 两种传参形式 - pub url: String, - /// HTTP 方法,默认 GET - #[serde(default)] - pub method: HttpMethod, - /// HTTP 头。值可包含 ${env:KEY}、${cmd:command}、${param:name:type} - #[serde(default, skip_serializing_if = "Option::is_none")] - pub headers: Option>, - /// POST/PUT 的 JSON body 模板。字符串值可包含 {{var}} 和 ${param:name:type} - #[serde(default, skip_serializing_if = "Option::is_none")] - pub body: Option, - /// 显式参数声明(可选) - /// 格式:{ "name": { "type": "string", "description": "..." } } - /// 未声明的参数自动从 ${param:name:type} 和 {{var}} 推导 - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parameters: Option, - /// JSON 提取路径(点路径),如 "data.items" - #[serde(default, skip_serializing_if = "Option::is_none")] - pub extract: Option, - /// 筛选字段 - #[serde(default, skip_serializing_if = "Option::is_none")] - pub fields: Option>, - /// 超时秒数,默认 30 - #[serde(default = "default_timeout")] - pub timeout: u64, -} - -const fn default_timeout() -> u64 { - 30 -} - -impl ToolDef { - /// 从文件加载 - pub fn from_file(path: &std::path::Path) -> Result { - let text = std::fs::read_to_string(path) - .map_err(|e| format!("读取 {} 失败: {}", path.display(), e))?; - serde_json::from_str(&text) - .map_err(|e| format!("解析 {} 失败: {}", path.display(), e)) - } - - /// 生成 SkillSpec - /// - /// 参数推导优先级: - /// 1. 显式 parameters 字段(精确控制类型和描述) - /// 2. 从 URL/Body/Headers 中扫描 `${param:name:type}` 推导 - /// 3. 从 {{var}} 推导(全视为 string 类型,兼容旧格式) - pub fn to_spec(&self) -> super::types::SkillSpec { - let params = self.infer_parameters(); - super::types::SkillSpec { - name: self.name.clone(), - description: self.description.clone(), - risk_level: super::types::RiskLevel::Low, - parameters: params, - timeout_secs: self.timeout, - } - } - - /// 推导参数定义 - fn infer_parameters(&self) -> serde_json::Value { - // 1. 显式 parameters 优先 - if let Some(ref p) = self.parameters { - let p = p.clone(); - // 如果已经是完整 schema(含 type/properties),直接返回 - if p.get("type").is_some() { - return p; - } - // 否则认为是属性定义 { name: { type, description }, ... } - // 包装为完整 schema - let required: Vec = p.as_object() - .map(|o| o.keys().cloned().collect()) - .unwrap_or_default(); - return serde_json::json!({ - "type": "object", - "properties": p, - "required": required - }); - } - - // 2. 从模板中扫描 ${param:name:type} 和 {{var}} - let mut param_map: serde_json::Map = serde_json::Map::new(); - - Self::collect_params(&self.url, &mut param_map); - if let Some(ref body) = self.body { - Self::collect_params_from_json(body, &mut param_map); - } - if let Some(ref headers) = self.headers { - for v in headers.values() { - Self::collect_params(v, &mut param_map); - } - } - - let required: Vec = param_map.keys().cloned().collect(); - - serde_json::json!({ - "type": "object", - "properties": param_map, - "required": required - }) - } - - /// 从字符串中收集参数(识别 ${param:name:type} 和 {{var}}) - fn collect_params(s: &str, map: &mut serde_json::Map) { - // 匹配 ${param:name:type} 和 ${param:name:type=default} - let mut pos = 0; - while let Some(start) = s[pos..].find("${param:") { - let rel = pos + start + 8; // skip "${param:" - let rest = &s[rel..]; - if let Some(end) = rest.find('}') { - let segment = &rest[..end]; - let parts: Vec<&str> = segment.split(':').collect(); - let name = parts[0].trim().to_string(); - if name.is_empty() { - pos = rel + end + 1; - continue; - } - if !map.contains_key(&name) { - let param_type = parts.get(1).copied().unwrap_or("string"); - let description = parts.get(2).copied().unwrap_or(name.as_str()); - let entry = serde_json::json!({ - "type": param_type, - "description": description - }); - map.insert(name.clone(), entry); - } - pos = rel + end + 1; - continue; - } - pos = rel; - } - - // 兼容旧格式 {{var}} - Self::extract_vars(s, map); - } - - /// 从 JSON 值中递归收集参数 - fn collect_params_from_json( - v: &serde_json::Value, - map: &mut serde_json::Map, - ) { - match v { - serde_json::Value::String(s) => Self::collect_params(s, map), - serde_json::Value::Object(obj) => { - for val in obj.values() { - Self::collect_params_from_json(val, map); - } - } - serde_json::Value::Array(arr) => { - for item in arr { - Self::collect_params_from_json(item, map); - } - } - _ => {} - } - } - - /// 兼容旧格式 {{var}} — 全视为 string 类型 - fn extract_vars(s: &str, map: &mut serde_json::Map) { - let mut remaining = s; - while let Some(start) = remaining.find("{{") { - remaining = &remaining[start + 2..]; - if let Some(end) = remaining.find("}}") { - let name = remaining[..end].trim().to_string(); - if !name.is_empty() && !name.starts_with('#') && !map.contains_key(&name) { - map.insert( - name.clone(), - serde_json::json!({"type": "string", "description": name}), - ); - } - remaining = &remaining[end + 2..]; - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_minimal() { - let json = r#"{ - "name": "ping", - "description": "Ping test", - "url": "https://httpbin.org/get" - }"#; - let def: ToolDef = serde_json::from_str(json).unwrap(); - assert_eq!(def.name, "ping"); - assert_eq!(def.method, HttpMethod::Get); - assert_eq!(def.timeout, 30); - } - - #[test] - fn test_parse_post() { - let json = r#"{ - "name": "translate", - "description": "翻译", - "url": "https://api.example.com/translate?text={{content}}", - "method": "POST", - "headers": { "Authorization": "${env:TR_KEY}" }, - "body": { "q": "{{content}}", "target": "zh" }, - "extract": "data.result", - "fields": ["text"], - "timeout": 60 - }"#; - let def: ToolDef = serde_json::from_str(json).unwrap(); - assert_eq!(def.method, HttpMethod::Post); - assert_eq!(def.timeout, 60); - assert_eq!(def.extract.as_deref(), Some("data.result")); - } - - #[test] - fn test_infer_parameters_from_url() { - let json = r#"{ - "name": "test", - "description": "test", - "url": "https://api.example.com/{{city}}/weather?days={{days}}" - }"#; - let def: ToolDef = serde_json::from_str(json).unwrap(); - let params = def.infer_parameters(); - let props = params["properties"].as_object().unwrap(); - assert!(props.contains_key("city")); - assert!(props.contains_key("days")); - let required = params["required"].as_array().unwrap(); - assert!(required.iter().any(|v| v == "city")); - assert!(required.iter().any(|v| v == "days")); - } - - #[test] - fn test_infer_parameters_from_body() { - let json = r#"{ - "name": "test", - "description": "test", - "url": "https://api.example.com/x", - "body": { "q": "{{text}}", "target": "{{lang}}" } - }"#; - let def: ToolDef = serde_json::from_str(json).unwrap(); - let params = def.infer_parameters(); - let props = params["properties"].as_object().unwrap(); - assert!(props.contains_key("text")); - assert!(props.contains_key("lang")); - } - - #[test] - fn test_method_uppercase_flexibility() { - let json = r#"{"name":"t","description":"t","url":"u","method":"post"}"#; - let def: ToolDef = serde_json::from_str(json).unwrap(); - assert_eq!(def.method, HttpMethod::Post); - } - - #[test] - fn test_infer_parameters_from_param_syntax() { - let json = r#"{ - "name": "weather", - "description": "天气查询", - "url": "https://api.example.com/weather/${param:city:string}", - "headers": { "X-Key": "${param:api_key:string}" } - }"#; - let def: ToolDef = serde_json::from_str(json).unwrap(); - let params = def.infer_parameters(); - let props = params["properties"].as_object().unwrap(); - - assert!(props.contains_key("city"), "应有 city 参数"); - assert_eq!(props["city"]["type"], "string", "city 类型应为 string"); - - assert!(props.contains_key("api_key"), "应有 api_key 参数"); - assert_eq!(props["api_key"]["type"], "string", "api_key 类型应为 string"); - - let required = params["required"].as_array().unwrap(); - assert!(required.iter().any(|v| v == "city")); - } - - #[test] - fn test_infer_parameters_explicit_priority() { - let json = r#"{ - "name": "my_tool", - "description": "test", - "url": "https://api.example.com/${param:city:string}", - "parameters": { - "city": { "type": "string", "description": "城市名称" }, - "days": { "type": "integer", "description": "天数" } - } - }"#; - let def: ToolDef = serde_json::from_str(json).unwrap(); - let params = def.infer_parameters(); - - // 显式 parameters 覆盖了自动推导 - let props = params["properties"].as_object().unwrap(); - assert!(props.contains_key("days"), "显式声明的 days 应在"); - assert_eq!(props["days"]["type"], "integer"); - - // 且自动 wrap 为完整 schema - assert_eq!(params["type"], "object"); - } -} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 218a41c..619618e 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -13,7 +13,6 @@ //! //! ### 工具类型 //! * **内置工具** (builtins/) — Rust 代码实现的工具(天气、搜索、高德地图等) -//! * **JSON 工具** (definitions.rs) — 从 `~/.ias/tools/*.json` 加载,由 ToolDef 定义 //! * **上下文工具** — read_memories, write_memory, read_summaries(直接在 daemon 中处理) //! //! ### 工具执行流程 @@ -27,8 +26,6 @@ pub mod approval; pub mod builtin; pub mod builtins; -pub mod definitions; -pub mod subprocess; pub mod types; /// ## 从 `call_capability` 的 `{name, prompt}` JSON 中提取目标工具的真实参数 diff --git a/src/tools/subprocess.rs b/src/tools/subprocess.rs deleted file mode 100644 index f9fd084..0000000 --- a/src/tools/subprocess.rs +++ /dev/null @@ -1,959 +0,0 @@ -#![allow(dead_code)] -//! ## JSON 工具子进程执行器 -//! -//! ### 设计理念:"放 JSON 即用" -//! -//! 从 `~/.ias/tools/*.json` 加载 `ToolDef`,运行时通过 HTTP 执行。 -//! 不需要修改 Rust 代码、不需要重新编译,放一个 JSON 文件就能注册新工具。 -//! -//! ### 执行流程 -//! -//! ```text -//! 1. 模板替换 -//! {{var}} → args.var(URL 编码) -//! ${param:name:type} → args.name -//! ${env:KEY} → 环境变量 -//! ${cmd:command} → shell 命令输出(有缓存,TTL=300s) -//! -//! 2. HTTP 请求(GET/POST/PUT/DELETE) -//! -//! 3. 返回处理 -//! ├─ 不是 JSON → 直接返回纯文本 -//! ├─ 有 extract → json_select() 按点路径取子节点 -//! ├─ 有 fields → filter_fields() 只保留指定字段 -//! └─ 格式化输出 → 可读文本 -//! ``` -//! -//! ### 缓存 -//! `${cmd:...}` 的结果会被缓存 300 秒,适合 JWT token 等有时效的值。 -//! -//! ### 注意 -//! 此模块当前未被 daemon 主线直接调用(通过 `BuiltinRegistry` 走内置工具路径), -//! 但作为可选功能保留:用户将 JSON 工具定义放入 `~/.ias/tools/` 即可使用。 -//! 所以函数标记为 `#![allow(dead_code)]` 避免编译警告。 - -use crate::tools::definitions::{HttpMethod, ToolDef}; -use crate::tools::types::{SkillResult, SkillSpec}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::LazyLock; -use std::sync::Mutex; -use std::sync::RwLock; -use std::time::Instant; - -/// 全局单例子进程执行器 -static RUNNER: LazyLock = LazyLock::new(|| { - let dir = tools_dir(); - SubprocessRunner::load_all(&dir) -}); - -/// 工具目录路径 -fn tools_dir() -> std::path::PathBuf { - // 优先环境变量 - if let Ok(dir) = std::env::var("IAS_TOOLS_DIR") { - return PathBuf::from(dir); - } - // 默认 ~/.ias/tools/ - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".ias") - .join("tools") -} - -/// 命令结果缓存 -/// -/// 避免 `${cmd:...}` 每次执行都重跑 shell 命令。 -/// 默认 TTL 300 秒,适合 JWT token 等有时效的值。 -struct CmdCache { - entries: Mutex>, - ttl_secs: u64, -} - -impl CmdCache { - fn new(ttl_secs: u64) -> Self { - Self { - entries: Mutex::new(HashMap::new()), - ttl_secs, - } - } - - fn get_or_exec(&self, cmd: &str) -> Result { - // 检查缓存 - { - let cache = self.entries.lock().unwrap(); - if let Some((val, time)) = cache.get(cmd) { - if time.elapsed().as_secs() < self.ttl_secs { - return Ok(val.clone()); - } - } - } - - // 执行命令 - let output = std::process::Command::new("bash") - .arg("-c") - .arg(cmd) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .output() - .map_err(|e| format!("执行命令失败: {}", e))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - return Err(format!("命令退出码 {}: {}", output.status.code().unwrap_or(-1), stderr)); - } - - let value = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if value.is_empty() { - return Err(format!("命令输出为空: {}", cmd)); - } - - // 写入缓存 - { - let mut cache = self.entries.lock().unwrap(); - cache.insert(cmd.to_string(), (value.clone(), Instant::now())); - } - - Ok(value) - } -} - -static CMD_CACHE: LazyLock = LazyLock::new(|| CmdCache::new(300)); - -/// 获取全局 runner 引用 -pub fn runner() -> &'static SubprocessRunner { - &RUNNER -} - -/// ## 子进程执行器 —— 管理所有从 JSON 文件加载的工具 -/// -/// ### 全局单例 -/// 通过 `LazyLock` 在首次访问时初始化,从 `~/ias/tools/*.json` 加载全部工具。 -/// -/// ### 运行时热加载 -/// `reload()` 方法可以重新扫描工具目录(目前未在运行时调用, -/// 但为后续热更新保留了接口)。 -pub struct SubprocessRunner { - tools: RwLock>, -} - -impl SubprocessRunner { - /// 加载目录下所有 *.json 文件 - pub fn load_all(dir: &Path) -> Self { - let mut tools = HashMap::new(); - if dir.exists() { - if let Ok(entries) = std::fs::read_dir(dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().map_or(false, |e| e == "json") { - match ToolDef::from_file(&path) { - Ok(def) => { - tracing::info!("加载工具: {} ← {}", def.name, path.display()); - tools.insert(def.name.clone(), def); - } - Err(e) => { - tracing::warn!("跳过 {}: {}", path.display(), e); - } - } - } - } - } - } else { - tracing::info!("工具目录不存在,创建: {}", dir.display()); - let _ = std::fs::create_dir_all(dir); - } - Self { - tools: RwLock::new(tools), - } - } - - /// 重载全部工具(运行时热加载) - pub fn reload(&self) { - let dir = tools_dir(); - let mut new_tools = HashMap::new(); - if dir.exists() { - if let Ok(entries) = std::fs::read_dir(&dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().map_or(false, |e| e == "json") { - if let Ok(def) = ToolDef::from_file(&path) { - new_tools.insert(def.name.clone(), def); - } - } - } - } - } - if let Ok(mut w) = self.tools.write() { - *w = new_tools; - } - } - - /// 是否有该工具 - pub fn has_tool(&self, name: &str) -> bool { - self.tools.read().map_or(false, |m| m.contains_key(name)) - } - - /// 获取工具 spec(参数从 URL/Body 模板推导) - pub fn spec(&self, name: &str) -> Option { - self.tools.read().ok().and_then(|m| m.get(name).map(|d| d.to_spec())) - } - - /// 获取所有工具 specs - pub fn all_specs(&self) -> Vec { - self.tools - .read() - .map(|m| m.values().map(|d| d.to_spec()).collect()) - .unwrap_or_default() - } - - /// 执行工具 - pub async fn execute(&self, name: &str, args_json: &str) -> Option { - let def = self.tools.read().ok()?.get(name)?.clone(); - let args: serde_json::Value = serde_json::from_str(args_json).unwrap_or_default(); - Some(self.execute_def(&def, &args).await) - } - - /// 执行一个 ToolDef - async fn execute_def(&self, def: &ToolDef, args: &serde_json::Value) -> SkillResult { - // 1. 模板替换 - let url = render_url(&def.url, args); - let headers = resolve_headers_with_args(def.headers.as_ref(), args); - let body = def.body.as_ref().map(|b| render_body(b, args)); - - // 2. HTTP 请求 - let resp_text = match http_request(def.method.clone(), &url, &headers, body.as_deref()).await - { - Ok(t) => t, - Err(e) => return SkillResult::error(format!("请求失败: {}", e)), - }; - - // 3. JSON 判定 - let json: serde_json::Value = match serde_json::from_str(&resp_text) { - Ok(j) => j, - Err(_) => { - // 纯文本,直接返回全文 - return SkillResult::ok(resp_text); - } - }; - - // 4. 提取子节点 - let extracted = match &def.extract { - Some(path) => match json_select(&json, path) { - Some(v) => v.clone(), - None => { - return SkillResult::error(format!("路径 '{}' 未找到", path)); - } - }, - None => json, - }; - - // 5. 筛选字段 - let filtered = match &def.fields { - Some(fields) => filter_fields(&extracted, fields), - None => extracted, - }; - - // 6. 格式化输出 - let output = format_output(&filtered); - - SkillResult::ok_with_data(output, filtered) - } -} - -// ═══════════════════════════════════════════════ -// 模板渲染 -// ═══════════════════════════════════════════════ - -/// 渲染 URL 模板:{{var}} / ${param:name:type} → args.name(URL 编码) -fn render_url(template: &str, args: &serde_json::Value) -> String { - // 先处理 ${param:name:type} - let after_param = replace_param_refs(template, args, true); - // 再处理 {{var}} - replace_mustache(&after_param, args) -} - -/// 渲染普通文本(非 URL 编码),支持三种引用: -/// ${param:name:type} — 参数值 -/// ${env:KEY} — 环境变量 -/// ${cmd:command} — shell 命令输出 -fn render_text(template: &str, args: &serde_json::Value) -> String { - // 先处理 ${param:name:type} - let after_param = replace_param_refs(template, args, false); - // 再处理 {{var}}(不编码) - replace_mustache_plain(&after_param, args) -} - -/// 替换 ${param:name:type} 引用 -fn replace_param_refs(template: &str, args: &serde_json::Value, url_encode: bool) -> String { - let mut result = String::new(); - let mut remaining = template; - - while let Some(start) = remaining.find("${param:") { - result.push_str(&remaining[..start]); - let rest = &remaining[start + 8..]; - if let Some(end) = rest.find('}') { - let segment = &rest[..end]; - let name = segment.split(':').next().map(|s| s.trim()).unwrap_or(""); - if !name.is_empty() { - let value = args - .get(name) - .and_then(|v| v.as_str()) - .unwrap_or(""); - if url_encode { - result.push_str(&urlencoding(value)); - } else { - result.push_str(value); - } - } - remaining = &rest[end + 1..]; - } else { - result.push_str(&remaining[start..]); - remaining = ""; - } - } - result.push_str(remaining); - result -} - -/// 替换 {{var}} — URL 编码版 -fn replace_mustache(template: &str, args: &serde_json::Value) -> String { - let mut result = String::new(); - let mut remaining = template; - while let Some(start) = remaining.find("{{") { - result.push_str(&remaining[..start]); - remaining = &remaining[start + 2..]; - if let Some(end) = remaining.find("}}") { - let key = remaining[..end].trim(); - if !key.starts_with('#') { - let value = args - .get(key) - .and_then(|v| v.as_str()) - .map(|s| urlencoding(s)) - .unwrap_or_else(|| key.to_string()); - result.push_str(&value); - } - remaining = &remaining[end + 2..]; - } else { - result.push_str(&remaining[..]); - remaining = ""; - } - } - result.push_str(remaining); - result -} - -/// 替换 {{var}} — 纯文本版(不编码) -fn replace_mustache_plain(template: &str, args: &serde_json::Value) -> String { - let mut result = String::new(); - let mut remaining = template; - while let Some(start) = remaining.find("{{") { - result.push_str(&remaining[..start]); - remaining = &remaining[start + 2..]; - if let Some(end) = remaining.find("}}") { - let key = remaining[..end].trim(); - if !key.starts_with('#') { - let value = args - .get(key) - .and_then(|v| v.as_str()) - .unwrap_or(key); - result.push_str(value); - } - remaining = &remaining[end + 2..]; - } else { - result.push_str(&remaining[..]); - remaining = ""; - } - } - result.push_str(remaining); - result -} - -/// URL 编码 -fn urlencoding(s: &str) -> String { - // 只编码需要编码的字符 - let mut result = String::with_capacity(s.len()); - for c in s.chars() { - match c { - 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c), - _ => { - for byte in c.to_string().as_bytes() { - result.push_str(&format!("%{:02X}", byte)); - } - } - } - } - result -} - -/// 解析模板值(无 args 上下文,主要用于测试) -fn resolve_template_value(template: &str) -> String { - if let Some(env_key) = template.strip_prefix("${env:").and_then(|s| s.strip_suffix('}')) { - return std::env::var(env_key).unwrap_or_default(); - } - if let Some(cmd) = template.strip_prefix("${cmd:").and_then(|s| s.strip_suffix('}')) { - return CMD_CACHE.get_or_exec(cmd).unwrap_or_else(|e| { - tracing::warn!("执行命令失败: {}", e); - String::new() - }); - } - // ${param:name:type} — 在 resolve_headers 中通过 args 解析, - // 单纯 resolve_template_value 没有 args 上下文的,先保留原样 - if template.contains("${param:") { - return template.to_string(); - } - template.to_string() -} - -/// 渲染 header 值(带 args 上下文,支持 ${param:name:type}) -fn resolve_header_value(template: &str, args: &serde_json::Value) -> String { - // 优先 env/cmd - if let Some(env_key) = template.strip_prefix("${env:").and_then(|s| s.strip_suffix('}')) { - return std::env::var(env_key).unwrap_or_default(); - } - if let Some(cmd) = template.strip_prefix("${cmd:").and_then(|s| s.strip_suffix('}')) { - return CMD_CACHE.get_or_exec(cmd).unwrap_or_else(|e| { - tracing::warn!("执行命令失败: {}", e); - String::new() - }); - } - // ${param:name:type} → 取 args.name - if let Some(param_rest) = template.strip_prefix("${param:") { - if let Some(end) = param_rest.find('}') { - let segment = ¶m_rest[..end]; - let name = segment.split(':').next().map(|s| s.trim()).unwrap_or(""); - if !name.is_empty() { - return args - .get(name) - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - } - } - } - render_text(template, args) -} - -/// 渲染 header 列表(带 args) -fn resolve_headers_with_args( - headers: Option<&HashMap>, - args: &serde_json::Value, -) -> Vec<(String, String)> { - headers - .map(|h| { - h.iter() - .map(|(k, v)| (k.clone(), resolve_header_value(v, args))) - .collect() - }) - .unwrap_or_default() -} - -/// 渲染 header 值(无 args 上下文,主要用于测试) -fn resolve_headers( - headers: Option<&HashMap>, -) -> Vec<(String, String)> { - headers - .map(|h| { - h.iter() - .map(|(k, v)| (k.clone(), resolve_template_value(v))) - .collect() - }) - .unwrap_or_default() -} - -/// 渲染 JSON body 模板:字符串中的 {{var}} 和 ${param:name:type} 被替换 -fn render_body(template: &serde_json::Value, args: &serde_json::Value) -> String { - fn render_value(v: &serde_json::Value, args: &serde_json::Value) -> serde_json::Value { - match v { - serde_json::Value::String(s) => { - if s.contains("{{") || s.contains("${param:") { - let rendered = render_text(s, args); - // 尝试解析为 JSON - if let Ok(j) = serde_json::from_str::(&rendered) { - return j; - } - return serde_json::Value::String(rendered); - } - v.clone() - } - serde_json::Value::Object(obj) => { - let mut m = serde_json::Map::new(); - for (k, val) in obj { - m.insert(k.clone(), render_value(val, args)); - } - serde_json::Value::Object(m) - } - serde_json::Value::Array(arr) => { - let items: Vec<_> = arr.iter().map(|item| render_value(item, args)).collect(); - serde_json::Value::Array(items) - } - _ => v.clone(), - } - } - - let rendered = render_value(template, args); - serde_json::to_string(&rendered).unwrap_or_default() -} - -// ═══════════════════════════════════════════════ -// HTTP 请求 -// ═══════════════════════════════════════════════ - -async fn http_request( - method: HttpMethod, - url: &str, - headers: &[(String, String)], - body: Option<&str>, -) -> Result { - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .user_agent("iAs/1.0 (JSON Tool Runner)") - .build() - .map_err(|e| format!("创建 HTTP client 失败: {}", e))?; - - let mut req = match method { - HttpMethod::Get => client.get(url), - HttpMethod::Post => { - let mut r = client.post(url); - if let Some(b) = body { - r = r.header("Content-Type", "application/json").body(b.to_string()); - } - r - } - HttpMethod::Put => { - let mut r = client.put(url); - if let Some(b) = body { - r = r.header("Content-Type", "application/json").body(b.to_string()); - } - r - } - HttpMethod::Delete => client.delete(url), - }; - - for (k, v) in headers { - if !v.is_empty() { - req = req.header(k.as_str(), v.as_str()); - } - } - - let resp = req.send().await.map_err(|e| format!("HTTP 请求失败: {}", e))?; - - if !resp.status().is_success() { - let status = resp.status(); - let text = resp.text().await.unwrap_or_default(); - return Err(format!("HTTP {}: {}", status, text.chars().take(200).collect::())); - } - - resp.text().await.map_err(|e| format!("读取响应失败: {}", e)) -} - -// ═══════════════════════════════════════════════ -// JSON 节点选择(点路径) -// ═══════════════════════════════════════════════ - -/// 按点路径选择 JSON 节点 -/// -/// 支持格式: -/// "data" → json["data"] -/// "data.items" → json["data"]["items"] -/// "items.0.name" → json["items"][0]["name"] -/// ".data.items" → 自动忽略前导点 -fn json_select<'a>(json: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> { - let path = path.strip_prefix('.').unwrap_or(path); - if path.is_empty() { - return Some(json); - } - - let mut current = json; - for segment in path.split('.') { - if segment.is_empty() { - continue; - } - current = match current { - serde_json::Value::Object(obj) => obj.get(segment)?, - serde_json::Value::Array(arr) => { - let idx: usize = segment.parse().ok()?; - arr.get(idx)? - } - _ => return None, - }; - } - Some(current) -} - -// ═══════════════════════════════════════════════ -// 字段筛选 -// ═══════════════════════════════════════════════ - -/// 只保留指定字段 -/// -/// - object: 只保留 fields 中的 key -/// - array: 对每个元素做同样筛选 -/// - 其他: 保留原值 -fn filter_fields(value: &serde_json::Value, fields: &[String]) -> serde_json::Value { - match value { - serde_json::Value::Object(obj) => { - let mut m = serde_json::Map::new(); - for field in fields { - if let Some(v) = obj.get(field) { - m.insert(field.clone(), v.clone()); - } - } - serde_json::Value::Object(m) - } - serde_json::Value::Array(arr) => { - let items: Vec<_> = arr.iter().map(|item| filter_fields(item, fields)).collect(); - serde_json::Value::Array(items) - } - other => other.clone(), - } -} - -// ═══════════════════════════════════════════════ -// 输出格式化 -// ═══════════════════════════════════════════════ - -/// 将提取/筛选后的结果格式化为可读文本 -fn format_output(value: &serde_json::Value) -> String { - match value { - serde_json::Value::Null => "null".to_string(), - serde_json::Value::Bool(b) => b.to_string(), - serde_json::Value::Number(n) => n.to_string(), - serde_json::Value::String(s) => s.clone(), - serde_json::Value::Array(arr) => format_array(arr), - serde_json::Value::Object(obj) => format_object(obj), - } -} - -fn format_array(arr: &[serde_json::Value]) -> String { - if arr.is_empty() { - return "(空)".to_string(); - } - - let lines: Vec = arr - .iter() - .enumerate() - .map(|(i, item)| { - let prefix = format!("{}. ", i + 1); - let body = match item { - serde_json::Value::Object(_) => { - // 对象数组:每个对象展平成 key: value 行 - let obj_lines = format_object_lines(item) - .into_iter() - .map(|l| format!(" {}", l)) - .collect::>() - .join("\n"); - obj_lines - } - serde_json::Value::Array(a) => format_array(a), - other => format_output(other), - }; - format!("{}{}", prefix, body) - }) - .collect(); - - lines.join("\n---\n") -} - -fn format_object(obj: &serde_json::Map) -> String { - let lines = format_object_lines(&serde_json::Value::Object(obj.clone())); - lines.join("\n") -} - -fn format_object_lines(value: &serde_json::Value) -> Vec { - match value { - serde_json::Value::Object(obj) => { - let mut lines = Vec::new(); - for (k, v) in obj { - let v_str = match v { - serde_json::Value::String(s) => s.clone(), - serde_json::Value::Null => "null".to_string(), - serde_json::Value::Bool(b) => b.to_string(), - serde_json::Value::Number(n) => n.to_string(), - serde_json::Value::Array(arr) => { - if arr.len() <= 5 { - arr.iter() - .map(format_output) - .collect::>() - .join(", ") - } else { - format!("[{} 项]", arr.len()) - } - } - serde_json::Value::Object(o) => { - format!("{{ {} }}", o.keys().cloned().collect::>().join(", ")) - } - }; - lines.push(format!(" {}: {}", k, v_str)); - } - lines - } - _ => vec![format_output(value)], - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // ─── 模板渲染 ─── - - #[test] - fn test_render_url_basic() { - let args = serde_json::json!({"code": "000001"}); - let url = render_url("https://api.example.com/stock/{{code}}", &args); - assert_eq!(url, "https://api.example.com/stock/000001"); - } - - #[test] - fn test_render_url_encode_chinese() { - let args = serde_json::json!({"city": "北京"}); - let url = render_url("https://api.example.com/weather?city={{city}}", &args); - assert!(url.contains("%E5%8C%97%E4%BA%AC")); - } - - #[test] - fn test_render_url_no_vars() { - let args = serde_json::json!({}); - let url = render_url("https://api.example.com/ping", &args); - assert_eq!(url, "https://api.example.com/ping"); - } - - // ─── JSON 选择 ─── - - #[test] - fn test_json_select_simple() { - let json = serde_json::json!({"data": {"price": 42.5}}); - assert_eq!(json_select(&json, "data.price").and_then(|v| v.as_f64()), Some(42.5)); - } - - #[test] - fn test_json_select_dot_prefix() { - let json = serde_json::json!({"data": {"price": 42.5}}); - assert_eq!(json_select(&json, ".data.price").and_then(|v| v.as_f64()), Some(42.5)); - } - - #[test] - fn test_json_select_array_index() { - let json = serde_json::json!({"items": [{"name": "a"}, {"name": "b"}]}); - assert_eq!( - json_select(&json, "items.1.name").and_then(|v| v.as_str()), - Some("b") - ); - } - - #[test] - fn test_json_select_not_found() { - let json = serde_json::json!({"a": 1}); - assert!(json_select(&json, "a.b.c").is_none()); - } - - // ─── 字段筛选 ─── - - #[test] - fn test_filter_fields_object() { - let obj = serde_json::json!({"name": "AAPL", "price": 150, "ignored": true}); - let fields = vec!["name".into(), "price".into()]; - let filtered = filter_fields(&obj, &fields); - assert_eq!(filtered.as_object().unwrap().len(), 2); - assert!(filtered.get("ignored").is_none()); - } - - #[test] - fn test_filter_fields_array() { - let arr = serde_json::json!([ - {"name": "A", "price": 10, "extra": true}, - {"name": "B", "price": 20, "extra": false} - ]); - let fields = vec!["name".into()]; - let filtered = filter_fields(&arr, &fields); - let items = filtered.as_array().unwrap(); - assert_eq!(items.len(), 2); - assert!(items[0].get("price").is_none()); - assert!(items[0].get("name").is_some()); - } - - // ─── 输出格式化 ─── - - #[test] - fn test_format_scalar() { - assert_eq!(format_output(&serde_json::json!(42)), "42"); - assert_eq!(format_output(&serde_json::json!("hello")), "hello"); - assert_eq!(format_output(&serde_json::json!(true)), "true"); - } - - #[test] - fn test_format_object() { - let obj = serde_json::json!({"name": "AAPL", "price": 150.5}); - let out = format_output(&obj); - assert!(out.contains("name: AAPL")); - assert!(out.contains("price: 150.5")); - } - - #[test] - fn test_format_array_of_objects() { - let arr = serde_json::json!([ - {"name": "A", "price": 10}, - {"name": "B", "price": 20} - ]); - let out = format_output(&arr); - assert!(out.starts_with("1.")); - assert!(out.contains("---")); - assert!(out.contains("2.")); - } - - // ─── 头部解析 ─── - - #[test] - fn test_resolve_headers_static() { - let mut h = HashMap::new(); - h.insert("Accept".into(), "application/json".into()); - let resolved = resolve_headers(Some(&h)); - assert_eq!(resolved[0].1, "application/json"); - } - - #[test] - fn test_resolve_cmd_inline() { - let result = resolve_template_value("${cmd:echo hello-world}"); - assert_eq!(result, "hello-world"); - } - - #[test] - fn test_resolve_cmd_cached() { - // 同一命令应缓存,第二次直接返回 - let r1 = resolve_template_value("${cmd:echo cached-test-123}"); - let r2 = resolve_template_value("${cmd:echo cached-test-123}"); - assert_eq!(r1, "cached-test-123"); - assert_eq!(r2, r1); - } - - #[test] - fn test_resolve_env() { - unsafe { std::env::set_var("TEST_API_KEY", "secret123"); } - let mut h = HashMap::new(); - h.insert("X-Key".into(), "${env:TEST_API_KEY}".into()); - let resolved = resolve_headers(Some(&h)); - assert_eq!(resolved[0].1, "secret123"); - } - - // ─── 新格式 ${param:name:type} 渲染 ─── - - #[test] - fn test_render_url_with_param() { - let args = serde_json::json!({"city": "北京", "days": "3"}); - let url = render_url( - "https://api.example.com/weather/${param:city:string}/days/${param:days:integer}", - &args, - ); - assert!(url.contains("%E5%8C%97%E4%BA%AC")); - assert!(url.contains("3")); - } - - #[test] - fn test_render_url_mixed() { - let args = serde_json::json!({"code": "000001"}); - let url = render_url( - "https://api.example.com/${param:code:string}?type={{#comment}}", - &args, - ); - // {{#comment}} 被跳过(注释语法) - assert_eq!(url, "https://api.example.com/000001?type="); - } - - #[test] - fn test_render_text_param() { - let args = serde_json::json!({"name": "hello"}); - let result = render_text("prefix_${param:name:string}_suffix", &args); - assert_eq!(result, "prefix_hello_suffix"); - } - - #[test] - fn test_resolve_header_param() { - let args = serde_json::json!({"token": "abc123"}); - let mut h = HashMap::new(); - h.insert("Authorization".into(), "Bearer ${param:token:string}".into()); - let resolved = resolve_headers_with_args(Some(&h), &args); - assert_eq!(resolved[0].1, "Bearer abc123"); - } - - #[test] - fn test_param_in_body() { - let args = serde_json::json!({"q": "你好", "lang": "zh"}); - let body = serde_json::json!({ - "query": "${param:q:string}", - "language": "${param:lang:string}" - }); - let rendered = render_body(&body, &args); - assert!(rendered.contains("你好")); - assert!(rendered.contains("zh")); - } - - #[test] - fn test_load_tools_from_disk() { - let dir = std::path::Path::new("/home/xiao/.ias/tools"); - if !dir.exists() { - eprintln!("跳过磁盘加载测试: 目录不存在"); - return; - } - let runner = SubprocessRunner::load_all(dir); - assert!(runner.has_tool("weather"), "应加载 weather 工具"); - assert!(runner.has_tool("github_repo"), "应加载 github_repo 工具"); - - let specs = runner.all_specs(); - assert!(specs.len() >= 2, "至少加载 2 个工具, 实际: {}", specs.len()); - - // 验证 weather 工具的参数从模板推导 - let weather_spec = runner.spec("weather").unwrap(); - let params = &weather_spec.parameters; - let props = params["properties"].as_object().unwrap(); - assert!(props.contains_key("city"), "weather 应有 city 参数"); - } - - // ─── 完整端到端 ─── - - #[tokio::test] - async fn test_execute_json_weather() { - let dir = std::path::Path::new("/home/xiao/.ias/tools"); - if !dir.exists() { - eprintln!("跳过: 工具目录不存在"); - return; - } - let runner = SubprocessRunner::load_all(dir); - let result = runner.execute("weather", r#"{"city":"北京"}"#).await.unwrap(); - assert!(result.success, "天气查询应成功: {}", result.output); - println!("🌤️ 天气结果: {}", result.output); - } - - #[tokio::test] - async fn test_execute_json_github() { - let dir = std::path::Path::new("/home/xiao/.ias/tools"); - if !dir.exists() { - eprintln!("跳过: 工具目录不存在"); - return; - } - let runner = SubprocessRunner::load_all(dir); - let result = runner.execute("github_repo", r#"{"owner":"rust-lang","repo":"rust"}"#).await.unwrap(); - assert!(result.success, "GitHub 查询应成功: {}", result.output); - assert!(result.output.contains("rust"), "应有仓库名: {}", result.output); - println!("🐙 GitHub: {}", result.output); - } - - #[tokio::test] - async fn test_execute_httpbin_get() { - let def = ToolDef { - name: "httpbin".into(), - description: "test".into(), - url: "https://httpbin.org/get".into(), - method: HttpMethod::Get, - headers: None, - body: None, - parameters: None, - extract: Some("url".into()), - fields: None, - timeout: 30, - }; - let args = serde_json::json!({}); - let runner = SubprocessRunner::load_all(Path::new("/nonexistent")); - let result = runner.execute_def(&def, &args).await; - assert!(result.success, "HTTP 请求应成功: {}", result.output); - assert!(result.output.contains("httpbin.org/get"), "应提取 url 字段: {}", result.output); - } -}