refactor: worker进程 → tokio task 架构 + 多渠道消息队列 + JSON工具系统

核心变更:
- daemon: 废弃 worker 子进程模型,替换为 LLM/Tool/Send 三消费者 tokio task
- queue: 新增公平轮转 MessageQueue + QueueRunner 路由,严格 ChannelId 渠道隔离
- channel: ChannelId(platform + user_id) 统一渠道标识,替代散列 user_id+platform
- tools: 全部工具统一经由 execute_tool() → Tool Consumer 单一执行路径
- llm: chat_with_tools 返回 ChatResult,支持异步工具调用 → 保留/恢复 session
- tools: 新增 JSON 工具定义系统 (ToolDef) 及 SubprocessRunner (HTTP 模板引擎)
- bin: ias-auth 独立 JWT 生成器,ias-fetch headless Chrome 网页抓取
- 所有出站消息通过 Send Consumer 队列转发,无直接 send_text 调用
- Session TTL 清理 (5分钟),QueueRunner 通道容错
- cargo check 零警告,cargo test 23/23 通过
This commit is contained in:
2026-06-09 16:32:49 +08:00
parent 8dc232ebab
commit 937556917c
17 changed files with 2937 additions and 729 deletions
+364
View File
@@ -0,0 +1,364 @@
//! JSON 工具定义 — 像 curl + jq 一样描述一个工具
//!
//! 三要素:URL → 取节点 → 选字段
//!
//! ```json
//! {
//! "name": "stocks",
//! "description": "股票行情",
//! "url": "https://api.example.com/stocks",
//! "extract": "data.items",
//! "fields": ["name", "price"]
//! }
//! ```
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 工具定义
#[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");
}
}