chore: 删除死代码 subprocess.rs 和 definitions.rs

- subprocess.rs(JSON 工具子进程执行器)未被任何代码引用
- definitions.rs(ToolDef 定义)仅被 subprocess.rs 引用
- 删除后消除全部 3 个 dead_code warning
- 更新 tools/mod.rs 注释和模块声明
This commit is contained in:
2026-06-09 21:49:17 +08:00
parent ee5c9b733f
commit 8ed608f01b
3 changed files with 0 additions and 1355 deletions
-393
View File
@@ -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<D>(deserializer: D) -> Result<Self, D::Error>
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<HashMap<String, String>>,
/// POST/PUT 的 JSON body 模板。字符串值可包含 {{var}} 和 ${param:name:type}
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
/// 显式参数声明(可选)
/// 格式:{ "name": { "type": "string", "description": "..." } }
/// 未声明的参数自动从 ${param:name:type} 和 {{var}} 推导
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parameters: Option<serde_json::Value>,
/// JSON 提取路径(点路径),如 "data.items"
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extract: Option<String>,
/// 筛选字段
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fields: Option<Vec<String>>,
/// 超时秒数,默认 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<Self, String> {
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<String> = 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<String, serde_json::Value> = 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<String> = 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<String, serde_json::Value>) {
// 匹配 ${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<String, serde_json::Value>,
) {
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<String, serde_json::Value>) {
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");
}
}
-3
View File
@@ -13,7 +13,6 @@
//! //!
//! ### 工具类型 //! ### 工具类型
//! * **内置工具** (builtins/) — Rust 代码实现的工具(天气、搜索、高德地图等) //! * **内置工具** (builtins/) — Rust 代码实现的工具(天气、搜索、高德地图等)
//! * **JSON 工具** (definitions.rs) — 从 `~/.ias/tools/*.json` 加载,由 ToolDef 定义
//! * **上下文工具** — read_memories, write_memory, read_summaries(直接在 daemon 中处理) //! * **上下文工具** — read_memories, write_memory, read_summaries(直接在 daemon 中处理)
//! //!
//! ### 工具执行流程 //! ### 工具执行流程
@@ -27,8 +26,6 @@
pub mod approval; pub mod approval;
pub mod builtin; pub mod builtin;
pub mod builtins; pub mod builtins;
pub mod definitions;
pub mod subprocess;
pub mod types; pub mod types;
/// ## 从 `call_capability` 的 `{name, prompt}` JSON 中提取目标工具的真实参数 /// ## 从 `call_capability` 的 `{name, prompt}` JSON 中提取目标工具的真实参数
-959
View File
@@ -1,959 +0,0 @@
#![allow(dead_code)]
//! ## JSON 工具子进程执行器
//!
//! ### 设计理念:"放 JSON 即用"
//!
//! 从 `~/.ias/tools/*.json` 加载 `ToolDef`,运行时通过 HTTP 执行。
//! 不需要修改 Rust 代码、不需要重新编译,放一个 JSON 文件就能注册新工具。
//!
//! ### 执行流程
//!
//! ```text
//! 1. 模板替换
//! {{var}} → args.varURL 编码)
//! ${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<SubprocessRunner> = 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<HashMap<String, (String, Instant)>>,
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<String, String> {
// 检查缓存
{
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<CmdCache> = LazyLock::new(|| CmdCache::new(300));
/// 获取全局 runner 引用
pub fn runner() -> &'static SubprocessRunner {
&RUNNER
}
/// ## 子进程执行器 —— 管理所有从 JSON 文件加载的工具
///
/// ### 全局单例
/// 通过 `LazyLock` 在首次访问时初始化,从 `~/ias/tools/*.json` 加载全部工具。
///
/// ### 运行时热加载
/// `reload()` 方法可以重新扫描工具目录(目前未在运行时调用,
/// 但为后续热更新保留了接口)。
pub struct SubprocessRunner {
tools: RwLock<HashMap<String, ToolDef>>,
}
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<SkillSpec> {
self.tools.read().ok().and_then(|m| m.get(name).map(|d| d.to_spec()))
}
/// 获取所有工具 specs
pub fn all_specs(&self) -> Vec<SkillSpec> {
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<SkillResult> {
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.nameURL 编码)
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 = &param_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<String, String>>,
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<String, String>>,
) -> 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::<serde_json::Value>(&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<String, String> {
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::<String>()));
}
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<String> = 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::<Vec<_>>()
.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, serde_json::Value>) -> String {
let lines = format_object_lines(&serde_json::Value::Object(obj.clone()));
lines.join("\n")
}
fn format_object_lines(value: &serde_json::Value) -> Vec<String> {
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::<Vec<_>>()
.join(", ")
} else {
format!("[{} 项]", arr.len())
}
}
serde_json::Value::Object(o) => {
format!("{{ {} }}", o.keys().cloned().collect::<Vec<_>>().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);
}
}