feat: add tool manager capability

This commit is contained in:
2026-06-22 10:30:12 +08:00
parent 99c1e9cd6c
commit 32c4fcf3a8
11 changed files with 590 additions and 46 deletions
+1
View File
@@ -1,6 +1,7 @@
# Rust 构建产物
/target
tools/*/target/
dist/
# 运行时数据(状态文件、缓存、聊天记录本地副本等)
.data/
+44 -10
View File
@@ -43,12 +43,13 @@ use crate::context::HistoryEntry;
use crate::context::MemoryStore;
use crate::db::Database;
use crate::llm::{
ASYNC_MARKER, ChatResult, Conversation, ConversationConfig, DEFAULT_SYSTEM_PROMPT, ToolExecutor,
ChatResult, Conversation, ConversationConfig, DEFAULT_SYSTEM_PROMPT, ToolExecution,
ToolExecutor,
};
use crate::queue::{ConsumerChannels, EnqueueHandle, MessageKind, PipelineMessage, QueueRunner};
use crate::tools::approval::ApprovalManager;
use crate::tools::registry::Registry;
use crate::tools::registry::RegistryManager;
use crate::wechat::client::WeChatClient;
use std::collections::HashMap;
use std::sync::Arc;
@@ -71,7 +72,7 @@ struct DaemonCtx {
/// 审批管理器 —— 处理高风险工具的用户确认码流程
approval: Arc<ApprovalManager>,
/// 工具注册器 —— 管理外部工具规范和执行
registry: Arc<Registry>,
registry: Arc<RegistryManager>,
/// 长期记忆管理器 —— 按用户隔离的记忆读写
memory_store: Arc<MemoryStore>,
/// 系统提示词,覆盖默认值
@@ -109,7 +110,7 @@ pub async fn run(database: &Arc<Database>, memory_store: &Arc<MemoryStore>) {
let approval_manager = Arc::new(ApprovalManager::new(Some(Arc::new(
database.pool().clone(),
))));
let registry = Arc::new(Registry::load("tools"));
let registry = Arc::new(RegistryManager::load("tools"));
let system_prompt = std::env::var("WEIXIN_LLM_SYSTEM_PROMPT").unwrap_or_else(|_| String::new());
let model = std::env::var("DEEPSEEK_MODEL").unwrap_or_else(|_| "deepseek-v4-flash".to_string());
@@ -408,7 +409,7 @@ async fn llm_consumer_loop(
context_token,
))
.await;
Ok(format!("{}{}", ASYNC_MARKER, n))
Ok(ToolExecution::Pending)
})
});
conv.set_tool_executor(executor);
@@ -691,7 +692,8 @@ async fn handle_tool_call(
eq.enqueue(msg).await;
})
};
ctx.registry
let output = ctx
.registry
.dispatch(
target,
&sub_params,
@@ -701,7 +703,13 @@ async fn handle_tool_call(
&ctx.memory_store,
&wechat_cb,
)
.await
.await;
if target == "tool_manager" && !is_tool_error(&output) {
let count = ctx.registry.reload().await;
format!("{output}\n\n工具注册表已重载,当前可用工具数: {count}")
} else {
output
}
} else {
let params: serde_json::Value = serde_json::from_str(&arguments).unwrap_or_default();
let eq = enqueue.clone();
@@ -726,7 +734,8 @@ async fn handle_tool_call(
eq.enqueue(msg).await;
})
};
ctx.registry
let output = ctx
.registry
.dispatch(
&tool_name,
&params,
@@ -736,7 +745,13 @@ async fn handle_tool_call(
&ctx.memory_store,
&wechat_cb,
)
.await
.await;
if tool_name == "tool_manager" && !is_tool_error(&output) {
let count = ctx.registry.reload().await;
format!("{output}\n\n工具注册表已重载,当前可用工具数: {count}")
} else {
output
}
};
enqueue
@@ -752,6 +767,25 @@ async fn handle_tool_call(
.await;
}
fn is_tool_error(result: &str) -> bool {
result.starts_with("异常退出")
|| result.starts_with("启动失败")
|| result.starts_with("工具输出非JSON")
|| result.starts_with("HTTP失败")
|| result.starts_with("读取stdout失败")
|| result.starts_with("stdout超时")
|| result.starts_with("进程超时")
|| result.starts_with("进程异常")
|| result.starts_with("未知消息类型")
|| result.starts_with("未知工具")
|| result.starts_with("不支持的方法")
|| result.starts_with("审批失败")
|| result.starts_with("审批超时")
|| result.starts_with("操作已被用户取消")
|| result.starts_with("缺少必填参数")
|| result.starts_with("工具 '")
}
fn unpack_call_capability_params(cp: &serde_json::Value) -> (&str, serde_json::Value) {
let target = cp["name"].as_str().unwrap_or("");
let mut params = serde_json::Map::new();
@@ -804,7 +838,7 @@ async fn execute_builtin(
match name {
"query_capabilities" => {
let tools = ctx.registry.list_tools();
let tools = ctx.registry.list_tools().await;
if tools.is_empty() {
return Some("当前没有注册任何工具".into());
}
+62 -27
View File
@@ -30,18 +30,21 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use tokio::sync::Mutex;
/// ## 工具执行器返回值
///
/// `Ready` 表示工具结果已经可直接写入对话;`Pending` 表示工具调用已交给
/// 外部队列,结果稍后通过 `ToolResult` 回喂。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolExecution {
Ready(String),
Pending,
}
/// ## 工具执行器 —— LLM 调用的工具回调
///
/// 这是一个函数别名:`Fn(工具名, JSON参数) → Future<Result<String>>`。
///
/// 当 LLM 在对话中发起工具调用时,Conversation 会调用这个 executor 来执行工具。
///
/// ### 两种执行模式
/// 1. **同步模式** — executor 直接执行并返回结果字符串
/// 2. **异步模式** — executor 将工具调用入队到消息队列,返回 `ASYNC_MARKER + 工具名`
/// 表示"工具已异步入队,结果稍后通过 ToolResult 消息回喂"
pub type ToolExecutor = Arc<
dyn Fn(&str, &str, &str) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send>>
dyn Fn(&str, &str, &str) -> Pin<Box<dyn Future<Output = Result<ToolExecution, String>> + Send>>
+ Send
+ Sync,
>;
@@ -74,9 +77,6 @@ pub struct ChatResult {
pub has_pending_async: bool,
}
/// 异步工具标记 —— 工具执行器返回此前缀表示工具已异步入队
pub const ASYNC_MARKER: &str = "__ASYNC_PENDING__";
/// ## Conversation —— LLM 对话管理器(核心类)
///
/// 这是整个项目中"与 LLM 对话"的核心抽象,负责:
@@ -262,19 +262,7 @@ impl Conversation {
/// 通知一个异步工具结果已返回,返回 true 表示所有异步工具均已返回
pub fn notify_tool_result(&self) -> bool {
loop {
let current = self.pending_async_count.load(Ordering::SeqCst);
if current == 0 {
return false;
}
if self
.pending_async_count
.compare_exchange(current, current - 1, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
return current == 1;
}
}
decrement_pending_count(&self.pending_async_count)
}
/// 工具循环核心:调 LLM → 处理工具调用 → 循环直到无工具或需要异步等待
@@ -347,17 +335,20 @@ impl Conversation {
Some(exec) => {
match exec(&tc.function.name, &tc.function.arguments, &tc.id).await {
Ok(r) => r,
Err(e) => format!("执行失败: {}", e),
Err(e) => ToolExecution::Ready(format!("执行失败: {}", e)),
}
}
None => "工具执行器未配置".to_string(),
None => ToolExecution::Ready("工具执行器未配置".to_string()),
};
if result.starts_with(ASYNC_MARKER) {
let result = match result {
ToolExecution::Ready(result) => result,
ToolExecution::Pending => {
has_async = true;
async_count += 1;
continue;
}
};
tracing::info!(target: "ias::msg", "[tool] {}: {}", tc.function.name, result);
self.session
@@ -408,6 +399,21 @@ impl Conversation {
}
}
fn decrement_pending_count(count: &AtomicU32) -> bool {
loop {
let current = count.load(Ordering::SeqCst);
if current == 0 {
return false;
}
if count
.compare_exchange(current, current - 1, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
return current == 1;
}
}
}
// ─── ChatHandle ───
#[allow(dead_code)]
@@ -474,3 +480,32 @@ pub const DEFAULT_SYSTEM_PROMPT: &str = "\
\n\
流程:收到消息 → query_capabilities → call_capability → 回复\n\
";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_execution_is_explicit_state() {
assert_eq!(
ToolExecution::Ready("__ASYNC_PENDING__weather".to_string()),
ToolExecution::Ready("__ASYNC_PENDING__weather".to_string())
);
assert_ne!(
ToolExecution::Ready("__ASYNC_PENDING__weather".to_string()),
ToolExecution::Pending
);
}
#[test]
fn notify_tool_result_does_not_underflow() {
let count = AtomicU32::new(0);
assert!(!decrement_pending_count(&count));
count.store(1, Ordering::SeqCst);
assert!(decrement_pending_count(&count));
assert!(!decrement_pending_count(&count));
assert_eq!(count.load(Ordering::SeqCst), 0);
}
}
+1 -1
View File
@@ -22,6 +22,6 @@ pub mod provider;
pub mod types;
pub use conversation::{
ASYNC_MARKER, ChatResult, Conversation, DEFAULT_SYSTEM_PROMPT, ToolExecutor,
ChatResult, Conversation, DEFAULT_SYSTEM_PROMPT, ToolExecution, ToolExecutor,
};
pub use types::ConversationConfig;
+68
View File
@@ -12,6 +12,7 @@ use crate::tools::executor;
use crate::tools::parser;
use crate::tools::spec::ToolSpec;
use serde_json::Value;
use tokio::sync::RwLock;
#[derive(Debug, Clone, serde::Serialize)]
pub struct ToolSummary {
@@ -53,6 +54,11 @@ pub struct Registry {
tools: HashMap<String, ToolSpec>,
}
pub struct RegistryManager {
root: String,
registry: RwLock<Registry>,
}
impl Registry {
/// 扫描工具根目录下各子目录的 `specs/*.tool.yaml`。
pub fn load(dir: &str) -> Self {
@@ -176,6 +182,68 @@ impl Registry {
}
}
impl RegistryManager {
pub fn load(dir: impl Into<String>) -> Self {
let root = dir.into();
Self {
registry: RwLock::new(Registry::load(&root)),
root,
}
}
pub async fn reload(&self) -> usize {
let registry = Registry::load(&self.root);
let len = registry.len();
*self.registry.write().await = registry;
len
}
pub async fn list_tools(&self) -> Vec<ToolSummary> {
self.registry.read().await.list_tools()
}
pub async fn dispatch(
&self,
name: &str,
params: &Value,
user_id: &str,
approved: bool,
approval: &Arc<ApprovalManager>,
memory: &Arc<MemoryStore>,
wechat_send: &(
dyn Fn(&str, &str) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>
+ Send
+ Sync
),
) -> String {
let spec = {
let registry = self.registry.read().await;
match registry.get(name) {
Some(spec) => spec.clone(),
None => {
tracing::warn!(target:"ias::registry","未知: {name}");
return format!("未知工具: {name}");
}
}
};
if let Some(err) = validate_required_params(&spec, params) {
return err;
}
executor::execute_loop(
&spec,
params,
user_id,
approved,
approval,
memory,
wechat_send,
)
.await
}
}
fn validate_required_params(spec: &ToolSpec, params: &Value) -> Option<String> {
let Some(obj) = params.as_object() else {
return Some(format!("工具 '{}' 参数必须是 JSON 对象", spec.name));
+3 -3
View File
@@ -2,10 +2,10 @@
//!
//! 与 `*.tool.yaml` 格式一一对应的 Rust 结构体。
use serde::Deserialize;
use serde::{Deserialize, Serialize};
/// 单个参数的声明
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ParamSpec {
pub name: String,
#[serde(default)]
@@ -30,7 +30,7 @@ pub struct ParamSpec {
/// required: true
/// desc: 城市名称
/// ```
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ToolSpec {
pub name: String,
pub desc: String,
+1 -1
View File
@@ -3,7 +3,7 @@
set -e
cd "$(dirname "$0")"
TOOLS=(datetime weather amap web_search fetch_page memories)
TOOLS=(datetime weather amap web_search fetch_page memories tool_manager)
echo "=== 构建工具 ==="
for tool in "${TOOLS[@]}"; do
+105
View File
@@ -0,0 +1,105 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tool_manager"
version = "0.1.0"
dependencies = [
"serde_json",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "tool_manager"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "tool_manager"
path = "src/main.rs"
[dependencies]
serde_json = "1.0"
@@ -0,0 +1,32 @@
name: tool_manager
desc: 创建、修改和构建本地能力工具。用于给助手新增或更新 tools 目录下的 stdio Rust 工具;高风险操作,执行前需要用户确认。
type: stdio
tool: tool_manager
path: /target/release/tool_manager
risk_level: high
timeout_secs: 120
params:
- name: action
required: true
desc: 操作类型:create_tool、update_tool 或 build_tool
- name: tool_name
required: true
desc: 工具名称,只允许字母、数字、下划线和短横线
- name: description
required: false
desc: 工具说明,创建或更新 spec 时使用
- name: rust_code
required: false
desc: 完整的 src/main.rs Rust 源码;省略时 create_tool 会生成默认模板
- name: params
required: false
desc: 参数列表数组,每项包含 name、required、desc
- name: risk_level
required: false
desc: 新工具风险级别,low 或 high,默认 low
- name: timeout_secs
required: false
desc: 新工具超时时间秒数,默认 30
- name: overwrite
required: false
desc: create_tool 目标已存在时是否覆盖,默认 false
+258
View File
@@ -0,0 +1,258 @@
//! tool_manager — 受控创建、修改、构建本地工具
use serde_json::{Value, json};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
let input: Value = match serde_json::from_reader(std::io::stdin()) {
Ok(v) => v,
Err(e) => return result(&format!("读取输入失败: {e}")),
};
let params = &input["params"];
let action = params["action"].as_str().unwrap_or("");
let tool_name = params["tool_name"].as_str().unwrap_or("");
let output = match run(action, tool_name, params) {
Ok(msg) => msg,
Err(e) => format!("工具管理失败: {e}"),
};
result(&output);
}
fn run(action: &str, tool_name: &str, params: &Value) -> Result<String, String> {
validate_tool_name(tool_name)?;
let tools_root = locate_tools_root()?;
let tool_dir = tools_root.join(tool_name);
match action {
"create_tool" => {
let overwrite = truthy(params.get("overwrite"));
if tool_dir.exists() && !overwrite {
return Err(format!(
"工具 {tool_name} 已存在;如需覆盖,请设置 overwrite=true"
));
}
write_tool_project(&tool_dir, tool_name, params)?;
build_tool(&tool_dir, tool_name)?;
Ok(format!(
"已创建并构建工具 {tool_name}。可通过 query_capabilities 查看并用 call_capability 调用。"
))
}
"update_tool" => {
if !tool_dir.exists() {
return Err(format!("工具 {tool_name} 不存在,无法修改"));
}
update_tool_project(&tool_dir, tool_name, params)?;
build_tool(&tool_dir, tool_name)?;
Ok(format!(
"已修改并构建工具 {tool_name}。注册表重载后即可使用最新版本。"
))
}
"build_tool" => {
if !tool_dir.exists() {
return Err(format!("工具 {tool_name} 不存在,无法构建"));
}
build_tool(&tool_dir, tool_name)?;
Ok(format!("已构建工具 {tool_name}"))
}
_ => Err("action 必须是 create_tool、update_tool 或 build_tool".to_string()),
}
}
fn write_tool_project(tool_dir: &Path, tool_name: &str, params: &Value) -> Result<(), String> {
fs::create_dir_all(tool_dir.join("src")).map_err(|e| e.to_string())?;
fs::create_dir_all(tool_dir.join("specs")).map_err(|e| e.to_string())?;
fs::write(tool_dir.join("Cargo.toml"), cargo_toml(tool_name)).map_err(|e| e.to_string())?;
fs::write(tool_dir.join("src/main.rs"), rust_code(tool_name, params))
.map_err(|e| e.to_string())?;
fs::write(
tool_dir
.join("specs")
.join(format!("{tool_name}.tool.yaml")),
spec_yaml(tool_name, params),
)
.map_err(|e| e.to_string())?;
Ok(())
}
fn update_tool_project(tool_dir: &Path, tool_name: &str, params: &Value) -> Result<(), String> {
if let Some(code) = params["rust_code"].as_str() {
fs::write(tool_dir.join("src/main.rs"), code).map_err(|e| e.to_string())?;
}
let should_update_spec = params.get("description").is_some()
|| params.get("params").is_some()
|| params.get("risk_level").is_some()
|| params.get("timeout_secs").is_some();
if should_update_spec {
fs::create_dir_all(tool_dir.join("specs")).map_err(|e| e.to_string())?;
fs::write(
tool_dir
.join("specs")
.join(format!("{tool_name}.tool.yaml")),
spec_yaml(tool_name, params),
)
.map_err(|e| e.to_string())?;
}
Ok(())
}
fn build_tool(tool_dir: &Path, tool_name: &str) -> Result<(), String> {
let output = Command::new("cargo")
.arg("build")
.arg("--release")
.current_dir(tool_dir)
.output()
.map_err(|e| format!("启动 cargo 失败: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("cargo build 失败: {}", stderr.trim()));
}
let binary = tool_dir.join("target/release").join(tool_name);
if !binary.exists() {
return Err(format!("构建后未找到二进制: {}", binary.display()));
}
Ok(())
}
fn locate_tools_root() -> Result<PathBuf, String> {
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
let tool_dir = exe
.parent()
.and_then(Path::parent)
.and_then(Path::parent)
.ok_or_else(|| "无法定位 tool_manager 目录".to_string())?;
tool_dir
.parent()
.map(Path::to_path_buf)
.ok_or_else(|| "无法定位 tools 根目录".to_string())
}
fn cargo_toml(tool_name: &str) -> String {
format!(
r#"[package]
name = "{tool_name}"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "{tool_name}"
path = "src/main.rs"
[dependencies]
serde_json = "1.0"
"#
)
}
fn rust_code(tool_name: &str, params: &Value) -> String {
params["rust_code"]
.as_str()
.map(str::to_string)
.unwrap_or_else(|| default_rust_code(tool_name))
}
fn default_rust_code(tool_name: &str) -> String {
format!(
r#"//! {tool_name} — generated tool
fn main() {{
let input: serde_json::Value = serde_json::from_reader(std::io::stdin()).unwrap_or_default();
let content = format!("工具 {tool_name} 已收到参数: {{}}", input["params"]);
println!("{{}}", serde_json::json!({{"type":"result","content":content}}));
}}
"#
)
}
fn spec_yaml(tool_name: &str, params: &Value) -> String {
let desc = params["description"]
.as_str()
.unwrap_or("由 tool_manager 创建的本地工具");
let risk_level = match params["risk_level"].as_str() {
Some("high") => "high",
_ => "low",
};
let timeout_secs = params["timeout_secs"].as_u64().unwrap_or(30).clamp(1, 300);
let mut out = format!(
"name: {tool_name}\n\
desc: {}\n\
type: stdio\n\
tool: {tool_name}\n\
path: /target/release/{tool_name}\n\
risk_level: {risk_level}\n\
timeout_secs: {timeout_secs}\n",
yaml_scalar(desc)
);
if let Some(items) = params["params"].as_array() {
out.push_str("params:\n");
for item in items {
let Some(name) = item["name"].as_str() else {
continue;
};
if validate_param_name(name).is_err() {
continue;
}
let required = item["required"].as_bool().unwrap_or(false);
let desc = item["desc"].as_str().unwrap_or("");
out.push_str(&format!(
" - name: {name}\n required: {required}\n desc: {}\n",
yaml_scalar(desc)
));
}
} else {
out.push_str("params: []\n");
}
out
}
fn validate_tool_name(name: &str) -> Result<(), String> {
if name.is_empty() {
return Err("缺少 tool_name".to_string());
}
if matches!(name, "target" | "specs" | "src" | "." | "..") {
return Err("tool_name 使用了保留名称".to_string());
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err("tool_name 只允许字母、数字、下划线和短横线".to_string());
}
Ok(())
}
fn validate_param_name(name: &str) -> Result<(), String> {
if name.is_empty()
|| !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err("参数名非法".to_string());
}
Ok(())
}
fn yaml_scalar(value: &str) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
}
fn truthy(value: Option<&Value>) -> bool {
match value {
Some(Value::Bool(v)) => *v,
Some(Value::String(v)) => matches!(v.as_str(), "true" | "1" | "yes" | ""),
_ => false,
}
}
fn result(content: &str) {
println!("{}", json!({"type":"result","content":content}));
}