7deae286ca
- 新增 ias-db crate,集中管理 PostgreSQL 连接池和 SQLx 迁移
- 迁移 ias-service/migrations 到 ias-db/migrations
- ias-main 改用 ias_db::connect_db(),ias-service 只接 PgPool
- 新增操作日志 HTTP 查询接口 (GET /v1/logs, GET /v1/logs/{id})
- 新增操作日志 Web 前端页面,支持按级别/模块/关键词筛选
- 消息队列关键节点增加操作日志写入
- ias-mail 新增 mark_remote_message_seen,IMAP 已读同步
- 工作流配置加载改为文件级容错 (load_configs_with_warnings)
- ias-context 工具调用说明更新为 query_api_docs + call_http_api
- 新增高德地图地理编码 HTTP 能力配置
模块版本: ias-db 0.1.0, ias-ai 0.1.14, ias-context 0.1.6, ias-http 0.1.15, ias-mail 0.1.16, ias-service 0.1.25, ias-main 0.2.27
150 lines
4.2 KiB
Rust
150 lines
4.2 KiB
Rust
use serde_json::Value;
|
|
use tokio::fs;
|
|
|
|
use crate::workflow::flow::start_flow;
|
|
use crate::workflow::types::FlowConfig;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct LoadedFlowConfigs {
|
|
pub configs: Vec<FlowConfig>,
|
|
pub warnings: Vec<String>,
|
|
}
|
|
|
|
pub async fn call_flow(path: String, args: String) -> String {
|
|
let all_flow = match load_configs(path).await {
|
|
Ok(flows) => flows,
|
|
Err(err) => return format!("工具调用失败:{:?}", err),
|
|
};
|
|
let args_json: Value = match serde_json::from_str(&args) {
|
|
Ok(json) => json,
|
|
Err(err) => return format!("工具调用失败:{:?}", err),
|
|
};
|
|
let flow_name = args_json["name"].as_str().unwrap_or_default();
|
|
if let Some(config) = all_flow.iter().find(|e| flow_name == e.name) {
|
|
let flow_result = start_flow(config, args_json["prompt"].clone()).await;
|
|
match flow_result {
|
|
Ok(value) => value,
|
|
Err(err) => format!("工具调用失败:{:?}", err),
|
|
}
|
|
} else {
|
|
"工具不存在,请检查工具名称".to_string()
|
|
}
|
|
}
|
|
|
|
pub async fn load_tools_define(path: String) -> anyhow::Result<String> {
|
|
let loaded = load_configs_with_warnings(path).await?;
|
|
Ok(serde_json::to_string(&loaded.configs).unwrap_or_default())
|
|
}
|
|
|
|
pub async fn load_configs(path: String) -> anyhow::Result<Vec<FlowConfig>> {
|
|
Ok(load_configs_with_warnings(path).await?.configs)
|
|
}
|
|
|
|
pub async fn load_configs_with_warnings(path: String) -> anyhow::Result<LoadedFlowConfigs> {
|
|
let mut configs: Vec<FlowConfig> = vec![];
|
|
let mut warnings: Vec<String> = vec![];
|
|
let config_files = scan_config_dir(path).await?;
|
|
if !config_files.is_empty() {
|
|
for file in config_files {
|
|
match load_yaml(file.clone()).await {
|
|
Ok(config) => configs.push(config),
|
|
Err(error) => warnings.push(format!("跳过工具配置 {file}: {error}")),
|
|
}
|
|
}
|
|
}
|
|
Ok(LoadedFlowConfigs { configs, warnings })
|
|
}
|
|
|
|
pub async fn load_yaml(path: String) -> anyhow::Result<FlowConfig> {
|
|
let content: String = fs::read_to_string(path).await?;
|
|
let config = serde_yaml::from_str(&content)?;
|
|
Ok(config)
|
|
}
|
|
|
|
pub async fn scan_config_dir(path: String) -> anyhow::Result<Vec<String>> {
|
|
let mut dir = fs::read_dir(path).await?;
|
|
let mut files: Vec<String> = vec![];
|
|
|
|
while let Some(entry) = dir.next_entry().await? {
|
|
let path = entry.path();
|
|
if !is_yaml_file(&path) {
|
|
continue;
|
|
}
|
|
let abs = fs::canonicalize(&path).await?;
|
|
files.push(abs.display().to_string());
|
|
}
|
|
|
|
files.sort();
|
|
Ok(files)
|
|
}
|
|
|
|
fn is_yaml_file(path: &std::path::Path) -> bool {
|
|
path.extension()
|
|
.and_then(|value| value.to_str())
|
|
.map(|value| matches!(value.to_ascii_lowercase().as_str(), "yaml" | "yml"))
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn list_file() {
|
|
println!();
|
|
|
|
let tools = load_tools_define("../tools".to_string()).await.unwrap();
|
|
|
|
println!("{}", tools);
|
|
|
|
println!();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn load_configs_skips_invalid_yaml_files() {
|
|
let dir = unique_test_dir("load_configs_skips_invalid_yaml_files");
|
|
tokio::fs::create_dir_all(&dir).await.unwrap();
|
|
|
|
tokio::fs::write(
|
|
dir.join("good.yaml"),
|
|
r#"
|
|
name: good_tool
|
|
desc: good config
|
|
params: []
|
|
steps: []
|
|
"#,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
tokio::fs::write(
|
|
dir.join("bad.yaml"),
|
|
r#"
|
|
name: bad_tool
|
|
steps:
|
|
- query:
|
|
- key: value
|
|
"#,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let loaded = load_configs_with_warnings(dir.display().to_string())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(loaded.configs.len(), 1);
|
|
assert_eq!(loaded.configs[0].name, "good_tool");
|
|
assert_eq!(loaded.warnings.len(), 1);
|
|
assert!(loaded.warnings[0].contains("bad.yaml"));
|
|
|
|
let configs = load_configs(dir.display().to_string()).await.unwrap();
|
|
assert_eq!(configs.len(), 1);
|
|
assert_eq!(configs[0].name, "good_tool");
|
|
|
|
let _ = tokio::fs::remove_dir_all(&dir).await;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn unique_test_dir(name: &str) -> std::path::PathBuf {
|
|
let nanos = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
std::env::temp_dir().join(format!("ias-ai-{name}-{}-{nanos}", std::process::id()))
|
|
}
|