feat: 高德地图工具 + 多轮 bug 修复
新增: - 高德地图 6 个内置工具: amap_poi_search, amap_geocode, amap_reverse_geocode, amap_route_plan, amap_travel_plan, amap_map_link - CLI 子命令: ias tool amap (poi-search/geocode/route-plan/travel-plan/map-link) - 工具优先级指南: build_capability_guide() 含 amap > weather > web_search 优先级 - DEFAULT_SYSTEM_PROMPT 内置工具优先级说明(不再依赖环境变量) Bug 修复: 1. (HIGH) call_capability 参数契约修复 — 新增 unpack_call_params() 统一解包 prompt 字段 2. (MEDIUM) 天气 hours=0 不再调用逐时 API 3. (MEDIUM) 长期记忆 daemon 双写缓存+DB,无 DB 不丢失 4. (HIGH) 审批流程状态语义修复 — handle_reply 返回 ApprovalDecision,拒绝/过期不再当作通过 5. (HIGH) 审批有效期基于 expires_at 时间戳而非 receiver drop 6. (MEDIUM) 摘要保存改为 current_user_id 与读取一致 7. (MEDIUM) worker 审批通过后删除孤立 tool result,仅保留 approved_tool 状态放行 架构: - daemon + worker IPC 架构 (Unix Domain Socket) - build_capability_guide 共享函数消除 main.rs/worker.rs 重复
This commit is contained in:
+174
-27
@@ -1,15 +1,18 @@
|
||||
mod cli;
|
||||
mod context;
|
||||
mod daemon;
|
||||
mod db;
|
||||
mod ipc;
|
||||
mod llm;
|
||||
mod logger;
|
||||
mod scheduler;
|
||||
mod state;
|
||||
mod tools;
|
||||
mod wechat;
|
||||
mod worker;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Commands};
|
||||
use cli::{AmapAction, Cli, Commands, MemosAction, ToolCommand};
|
||||
use context::MemoryStore;
|
||||
use db::Database;
|
||||
use llm::{Conversation, ConversationConfig, DEFAULT_SYSTEM_PROMPT, ToolExecutor, Usage};
|
||||
@@ -42,6 +45,17 @@ async fn main() {
|
||||
}
|
||||
|
||||
let file_state = state::StateManager::new(".data/weixin-ilink");
|
||||
|
||||
// 工具命令无需登录/数据库,提前处理
|
||||
let tool_cmd = match &cli.command {
|
||||
Commands::Tool(cmd) => Some(cmd),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(cmd) = tool_cmd {
|
||||
cmd_tool(cmd.clone()).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let database = match Database::connect().await {
|
||||
Ok(db) => {
|
||||
info!("✅ 数据库连接成功");
|
||||
@@ -87,11 +101,29 @@ async fn main() {
|
||||
cmd_usage(&database, since, until, model).await;
|
||||
}
|
||||
Commands::Service => cmd_listen(true, false, &database, &file_state, &memory_store).await,
|
||||
Commands::Daemon { sock } => {
|
||||
cmd_daemon(sock, &database, &file_state, &memory_store).await;
|
||||
}
|
||||
Commands::Worker { sock } => {
|
||||
if let Err(e) = worker::run(&sock).await {
|
||||
error!("Worker 失败: {}", e);
|
||||
}
|
||||
}
|
||||
Commands::Tool(..) => unreachable!("Tool 命令已提前处理"),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 命令实现 ───
|
||||
|
||||
async fn cmd_daemon(
|
||||
sock_path: String,
|
||||
database: &Option<Arc<Database>>,
|
||||
file_state: &state::StateManager,
|
||||
memory_store: &Arc<MemoryStore>,
|
||||
) {
|
||||
daemon::run(sock_path, database, file_state, memory_store).await;
|
||||
}
|
||||
|
||||
async fn cmd_login(
|
||||
timeout_secs: u64,
|
||||
database: &Option<Arc<Database>>,
|
||||
@@ -339,10 +371,11 @@ async fn cmd_listen(
|
||||
// query_capabilities: 列出所有内置工具
|
||||
if n == "query_capabilities" {
|
||||
let specs = tools::builtin::BuiltinRegistry::specs();
|
||||
return Ok(build_capability_list(&specs));
|
||||
return Ok(tools::build_capability_guide(&specs));
|
||||
}
|
||||
|
||||
// 确定目标工具名和参数(call_capability 提取 name + 透传完整参数)
|
||||
// 确定目标工具名和参数
|
||||
// call_capability: 从 {name, prompt} 中提取 name,从 prompt 中解包真实参数
|
||||
let (target_name, target_args) = if n == "call_capability" {
|
||||
let cp: serde_json::Value = serde_json::from_str(&args).unwrap_or_default();
|
||||
let name = cp
|
||||
@@ -350,7 +383,8 @@ async fn cmd_listen(
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default();
|
||||
(name, serde_json::to_string(&cp).unwrap_or_default())
|
||||
let unpacked = tools::unpack_call_params(&cp);
|
||||
(name, serde_json::to_string(&unpacked).unwrap_or_default())
|
||||
} else {
|
||||
(n.clone(), args.clone())
|
||||
};
|
||||
@@ -841,30 +875,143 @@ async fn builtin_approve(ctx: &ExecutionContext, name: &str) -> Result<bool, Str
|
||||
}
|
||||
}
|
||||
|
||||
fn build_capability_list(specs: &[tools::types::SkillSpec]) -> String {
|
||||
let mut lines = vec!["📋 可用工具:".to_string()];
|
||||
for spec in specs {
|
||||
let risk = if spec.risk_level == tools::types::RiskLevel::High {
|
||||
" ⚠️需确认"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
lines.push(format!(
|
||||
"\n🔹 {} {} — {}",
|
||||
spec.name, risk, spec.description
|
||||
));
|
||||
if !spec
|
||||
.parameters
|
||||
.get("properties")
|
||||
.and_then(|v| v.as_object())
|
||||
.map_or(true, |o| o.is_empty())
|
||||
{
|
||||
lines.push(format!(
|
||||
" 参数: {}",
|
||||
serde_json::to_string(&spec.parameters).unwrap_or_default()
|
||||
));
|
||||
|
||||
// ─── 工具 CLI 命令 ───
|
||||
|
||||
async fn cmd_tool(cmd: ToolCommand) {
|
||||
match cmd {
|
||||
ToolCommand::Datetime => {
|
||||
let result = tools::builtins::datetime::execute();
|
||||
println!("{}", result.output);
|
||||
}
|
||||
ToolCommand::Memos(action) => {
|
||||
let args = match action {
|
||||
MemosAction::List => serde_json::json!({"action": "list"}),
|
||||
MemosAction::Add { content } => {
|
||||
serde_json::json!({"action": "add", "content": content})
|
||||
}
|
||||
MemosAction::Delete { id } => {
|
||||
serde_json::json!({"action": "delete", "id": id})
|
||||
}
|
||||
};
|
||||
let args_str = serde_json::to_string(&args).unwrap_or_default();
|
||||
let result = tools::builtins::memos::execute(&args_str).await;
|
||||
println!("{}", result.output);
|
||||
}
|
||||
ToolCommand::Weather { location, days } => {
|
||||
let params = serde_json::json!({"location": location, "days": days});
|
||||
let result = tools::builtins::weather::execute(params).await;
|
||||
println!("{}", result.output);
|
||||
}
|
||||
ToolCommand::Search {
|
||||
query,
|
||||
max_results,
|
||||
no_answer,
|
||||
topic,
|
||||
time_range,
|
||||
} => {
|
||||
let mut params = serde_json::json!({
|
||||
"query": query,
|
||||
"max_results": max_results,
|
||||
});
|
||||
if no_answer {
|
||||
params["include_answer"] = serde_json::json!(false);
|
||||
}
|
||||
if let Some(t) = topic {
|
||||
params["topic"] = serde_json::json!(t);
|
||||
}
|
||||
if let Some(tr) = time_range {
|
||||
params["time_range"] = serde_json::json!(tr);
|
||||
}
|
||||
let result = tools::builtins::web_search::execute(params).await;
|
||||
println!("{}", result.output);
|
||||
}
|
||||
ToolCommand::Fetch { url } => {
|
||||
let params = serde_json::json!({"url": url});
|
||||
let result = tools::builtins::fetch_page::execute(params).await;
|
||||
println!("{}", result.output);
|
||||
}
|
||||
ToolCommand::Amap(action) => {
|
||||
cmd_amap(action).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cmd_amap(action: AmapAction) {
|
||||
match action {
|
||||
AmapAction::PoiSearch {
|
||||
keywords,
|
||||
city,
|
||||
location,
|
||||
radius,
|
||||
page,
|
||||
offset,
|
||||
} => {
|
||||
let mut params = serde_json::json!({
|
||||
"keywords": keywords,
|
||||
"page": page,
|
||||
"offset": offset,
|
||||
});
|
||||
if let Some(c) = city { params["city"] = serde_json::json!(c); }
|
||||
if let Some(l) = location { params["location"] = serde_json::json!(l); }
|
||||
if let Some(r) = radius { params["radius"] = serde_json::json!(r); }
|
||||
let result = tools::builtins::amap::execute("amap_poi_search", params).await;
|
||||
if let Some(r) = result { println!("{}", r.output); }
|
||||
}
|
||||
AmapAction::Geocode { address, city } => {
|
||||
let mut params = serde_json::json!({"address": address});
|
||||
if let Some(c) = city { params["city"] = serde_json::json!(c); }
|
||||
let result = tools::builtins::amap::execute("amap_geocode", params).await;
|
||||
if let Some(r) = result { println!("{}", r.output); }
|
||||
}
|
||||
AmapAction::ReverseGeocode { location } => {
|
||||
let params = serde_json::json!({"location": location});
|
||||
let result = tools::builtins::amap::execute("amap_reverse_geocode", params).await;
|
||||
if let Some(r) = result { println!("{}", r.output); }
|
||||
}
|
||||
AmapAction::RoutePlan {
|
||||
r#type,
|
||||
origin,
|
||||
destination,
|
||||
city,
|
||||
waypoints,
|
||||
strategy,
|
||||
} => {
|
||||
let mut params = serde_json::json!({
|
||||
"type": r#type,
|
||||
"origin": origin,
|
||||
"destination": destination,
|
||||
});
|
||||
if let Some(c) = city { params["city"] = serde_json::json!(c); }
|
||||
if let Some(w) = waypoints { params["waypoints"] = serde_json::json!(w); }
|
||||
if let Some(s) = strategy { params["strategy"] = serde_json::json!(s); }
|
||||
let result = tools::builtins::amap::execute("amap_route_plan", params).await;
|
||||
if let Some(r) = result { println!("{}", r.output); }
|
||||
}
|
||||
AmapAction::TravelPlan {
|
||||
city,
|
||||
interests,
|
||||
route_type,
|
||||
} => {
|
||||
let interests_arr: Vec<serde_json::Value> = interests
|
||||
.split(',')
|
||||
.map(|s| serde_json::json!(s.trim()))
|
||||
.collect();
|
||||
let params = serde_json::json!({
|
||||
"city": city,
|
||||
"interests": interests_arr,
|
||||
"route_type": route_type,
|
||||
});
|
||||
let result = tools::builtins::amap::execute("amap_travel_plan", params).await;
|
||||
if let Some(r) = result { println!("{}", r.output); }
|
||||
}
|
||||
AmapAction::MapLink { data } => {
|
||||
let map_data: serde_json::Value =
|
||||
serde_json::from_str(&data).unwrap_or(serde_json::json!([]));
|
||||
let params = serde_json::json!({"map_data": map_data});
|
||||
let result = tools::builtins::amap::execute("amap_map_link", params).await;
|
||||
if let Some(r) = result { println!("{}", r.output); }
|
||||
}
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user