Files
iAs/ias-ai/src/workflow/core.rs
T
wunianxiao 7deae286ca feat: 0.2.27 - 抽离数据库基础设施模块
- 新增 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
2026-07-08 17:44:21 +08:00

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()))
}