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:
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 中提取目标工具的真实参数
|
||||||
|
|||||||
@@ -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<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.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<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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user