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
This commit is contained in:
+17
-13
@@ -3,7 +3,7 @@ use crate::{
|
||||
tools::http_api::call_http_api,
|
||||
tools::tool_instructions::query_tool_call_instruction,
|
||||
workflow::{
|
||||
core::{call_flow, load_configs},
|
||||
core::{call_flow, load_configs_with_warnings},
|
||||
types::{FlowConfig, FlowParam},
|
||||
},
|
||||
};
|
||||
@@ -135,7 +135,7 @@ async fn query_capabilities(channel: &ChannelMessage, tool_path: &str) -> String
|
||||
let mut result = serde_json::json!({
|
||||
"api_capabilities": capability_briefs(&inventory.api_tools),
|
||||
"http_capabilities": capability_briefs(&inventory.http_tools),
|
||||
"tool_call_instructions": tool_call_instructions.unwrap_or_else(|| "暂无工具调用说明。".to_string()),
|
||||
"tool_call_instructions": tool_call_instructions.unwrap_or_else(|| "固定工具调用约束已在系统提示词中生效;当前没有额外动态工具调用说明。".to_string()),
|
||||
"total": inventory.api_tools.len() + inventory.http_tools.len(),
|
||||
"usage": "先用 query_capabilities 查看名称、描述和工具调用说明;需要参数时调用 query_capability_detail;执行时调用 call_capability。内部 HTTP API 统一通过 call_http_api 执行,先用 query_api_docs 查询 endpoint_id、参数和请求体。",
|
||||
});
|
||||
@@ -293,17 +293,21 @@ pub async fn load_capability_inventory(tool_path: &str) -> CapabilityInventory {
|
||||
}
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
let http_tools = match load_configs(tool_path.to_string()).await {
|
||||
Ok(configs) => configs
|
||||
.into_iter()
|
||||
.filter_map(|config| {
|
||||
if seen.insert(config.name.clone()) {
|
||||
Some(capability_from_flow_config(config, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
let http_tools = match load_configs_with_warnings(tool_path.to_string()).await {
|
||||
Ok(loaded) => {
|
||||
warnings.extend(loaded.warnings);
|
||||
loaded
|
||||
.configs
|
||||
.into_iter()
|
||||
.filter_map(|config| {
|
||||
if seen.insert(config.name.clone()) {
|
||||
Some(capability_from_flow_config(config, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Err(error) => {
|
||||
warnings.push(format!("加载 HTTP 能力失败: {error}"));
|
||||
Vec::new()
|
||||
|
||||
@@ -4,6 +4,12 @@ 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,
|
||||
@@ -26,20 +32,27 @@ pub async fn call_flow(path: String, args: String) -> String {
|
||||
}
|
||||
|
||||
pub async fn load_tools_define(path: String) -> anyhow::Result<String> {
|
||||
let configs = load_configs(path).await?;
|
||||
Ok(serde_json::to_string(&configs).unwrap_or_default())
|
||||
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 {
|
||||
let config = load_yaml(file).await?;
|
||||
configs.push(config);
|
||||
match load_yaml(file.clone()).await {
|
||||
Ok(config) => configs.push(config),
|
||||
Err(error) => warnings.push(format!("跳过工具配置 {file}: {error}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(configs)
|
||||
Ok(LoadedFlowConfigs { configs, warnings })
|
||||
}
|
||||
|
||||
pub async fn load_yaml(path: String) -> anyhow::Result<FlowConfig> {
|
||||
@@ -82,3 +95,55 @@ async fn list_file() {
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user