23c7fbac62
为整个项目添加了详尽的中文文档注释,覆盖所有模块: 入口与架构: - main.rs — 架构图、数据流、4 个关键设计决策 - cli.rs — 所有 CLI 子命令中文化 - Cargo.toml — 每个依赖的作用说明 - channel/mod.rs — 渠道标识设计意图 核心守护进程与消息队列: - daemon.rs — 三消费者架构图、审批流 - queue/* — 公平轮转算法、路由调度架构 LLM 层: - types.rs — Role/Message/ToolCall 等每个字段含义 - provider.rs — Provider trait 设计 + SSE 解析 - conversation.rs — 工具循环流程图、摘要机制 - deepseek.rs — API 请求格式 上下文管理: - context/types.rs — ChatSession 消息生命周期 - context/builder.rs — Token 预算管理策略 - context/tools.rs — MemoryStore 双写策略 工具系统: - tools/mod.rs — 两层元工具架构图 - tools/approval.rs — 审批流程 - tools/definitions.rs — 声明式工具三要素 - tools/subprocess.rs — 执行流程 + 缓存策略 数据库与状态: - db/* — 5 张表用途、回退策略 - state.rs — 文件存储回退方案 - scheduler.rs — SKIP LOCKED 防重复 已废弃模块: - ipc.rs — 旧 UDS 协议 - worker.rs — 旧 vs 新架构对比
394 lines
14 KiB
Rust
394 lines
14 KiB
Rust
//! ## 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");
|
||
}
|
||
}
|