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:
2026-06-03 17:01:04 +08:00
parent e379e498b5
commit b2fe054eaf
23 changed files with 4367 additions and 263 deletions
+30 -34
View File
@@ -74,10 +74,7 @@ impl WeChatClient {
qrcode: &str,
base_url: &str,
) -> Result<StatusResponse, String> {
let url = format!(
"{}/ilink/bot/get_qrcode_status?qrcode={}",
base_url, qrcode
);
let url = format!("{}/ilink/bot/get_qrcode_status?qrcode={}", base_url, qrcode);
let resp_result = self
.http
@@ -89,7 +86,9 @@ impl WeChatClient {
match resp_result {
Ok(response) => {
let text = response.text().await
let text = response
.text()
.await
.map_err(|e| format!("读取响应体失败: {}", e))?;
serde_json::from_str::<StatusResponse>(&text)
.map_err(|e| format!("解析状态响应失败: {}", e))
@@ -134,7 +133,8 @@ impl WeChatClient {
// 生成二维码终端显示
if let Ok(code) = qrcode::QrCode::new(qrcode_img_content.as_bytes()) {
let svg = code.render::<qrcode::render::unicode::Dense1x2>()
let svg = code
.render::<qrcode::render::unicode::Dense1x2>()
.dark_color(qrcode::render::unicode::Dense1x2::Dark)
.light_color(qrcode::render::unicode::Dense1x2::Light)
.build();
@@ -177,7 +177,9 @@ impl WeChatClient {
.ilink_bot_id
.ok_or_else(|| "登录成功但未返回 ilink_bot_id".to_string())?;
let user_id = status.ilink_user_id.unwrap_or_default();
let base_url = status.baseurl.unwrap_or_else(|| Self::DEFAULT_BASE_URL.to_string());
let base_url = status
.baseurl
.unwrap_or_else(|| Self::DEFAULT_BASE_URL.to_string());
// 保存到内存状态
*self.token.lock().await = token.clone();
@@ -220,7 +222,10 @@ impl WeChatClient {
.map_err(|e| format!("解析 notifyStart 响应失败: {}", e))?;
if _resp.ret != 0 {
return Err(format!("notifyStart 失败: ret={} errmsg={:?}", _resp.ret, _resp.errmsg));
return Err(format!(
"notifyStart 失败: ret={} errmsg={:?}",
_resp.ret, _resp.errmsg
));
}
info!("监听器已注册");
@@ -257,15 +262,19 @@ impl WeChatClient {
let url = format!("{}/ilink/bot/getupdates", self.base_url);
let token = self.token.lock().await.clone();
let resp: GetUpdatesResp = match self
.post_json(&url, &body, Some(&token))
.await
{
let resp: GetUpdatesResp = match self.post_json(&url, &body, Some(&token)).await {
Ok(resp) => {
let resp: GetUpdatesResp = resp.json().await.map_err(|e| format!("解析 getUpdates 响应失败: {}", e))?;
let resp: GetUpdatesResp = resp
.json()
.await
.map_err(|e| format!("解析 getUpdates 响应失败: {}", e))?;
if resp.ret != 0 || resp.errcode.unwrap_or(0) != 0 {
tracing::warn!("getUpdates 返回错误: ret={} errcode={:?} errmsg={:?}",
resp.ret, resp.errcode, resp.errmsg);
tracing::warn!(
"getUpdates 返回错误: ret={} errcode={:?} errmsg={:?}",
resp.ret,
resp.errcode,
resp.errmsg
);
}
resp
}
@@ -337,16 +346,6 @@ impl WeChatClient {
}
}
/// 获取当前 token(用于持久化)
pub async fn get_token(&self) -> String {
self.token.lock().await.clone()
}
/// 获取当前 account_id(用于持久化)
pub async fn get_account_id(&self) -> String {
self.account_id.lock().await.clone()
}
/// 获取当前 updates_buf(用于持久化)
pub async fn get_updates_buf(&self) -> String {
self.updates_buf.lock().await.clone()
@@ -375,14 +374,12 @@ impl WeChatClient {
headers.insert(
"iLink-App-ClientVersion",
reqwest::header::HeaderValue::from_str(&Self::CLIENT_VERSION.to_string())
.unwrap(),
reqwest::header::HeaderValue::from_str(&Self::CLIENT_VERSION.to_string()).unwrap(),
);
// X-WECHAT-UIN: random uint32 -> decimal -> base64
let uin = rand::random::<u32>();
let uin_b64 = base64::engine::general_purpose::STANDARD
.encode(uin.to_string());
let uin_b64 = base64::engine::general_purpose::STANDARD.encode(uin.to_string());
headers.insert(
"X-WECHAT-UIN",
reqwest::header::HeaderValue::from_str(&uin_b64).unwrap(),
@@ -396,8 +393,7 @@ impl WeChatClient {
if !token.is_empty() {
headers.insert(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", token))
.unwrap(),
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(),
);
headers.insert(
"AuthorizationType",
@@ -413,7 +409,8 @@ impl WeChatClient {
body: &impl serde::Serialize,
token: Option<&str>,
) -> Result<reqwest::Response, String> {
let body_str = serde_json::to_string(body).map_err(|e| format!("序列化请求体失败: {}", e))?;
let body_str =
serde_json::to_string(body).map_err(|e| format!("序列化请求体失败: {}", e))?;
debug!("POST {} body_len={}", url, body_str.len());
@@ -426,8 +423,7 @@ impl WeChatClient {
// 设置 Content-Length
headers.insert(
"Content-Length",
reqwest::header::HeaderValue::from_str(&body_str.len().to_string())
.unwrap(),
reqwest::header::HeaderValue::from_str(&body_str.len().to_string()).unwrap(),
);
self.http