From 42eb9f85d561cc3c717509a2cf2db0ba7ef4c96a Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Wed, 6 May 2026 17:22:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=AB=98=E4=BB=B7=E5=80=BC=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E8=A6=81=E6=B1=82=EF=BC=9B=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=8A=80=E8=83=BD=E7=B4=A2=E5=BC=95=E6=8F=90=E7=A4=BA=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E6=94=AF=E6=8C=81=20XML=20=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot --- src/agent/memory_tool_usage_system_prompt.md | 2 +- src/channels/wechat.rs | 8 ++++ src/skills/mod.rs | 47 +++++++++++++++++--- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/agent/memory_tool_usage_system_prompt.md b/src/agent/memory_tool_usage_system_prompt.md index e0691cf..7d42a2f 100644 --- a/src/agent/memory_tool_usage_system_prompt.md +++ b/src/agent/memory_tool_usage_system_prompt.md @@ -43,7 +43,7 @@ - 优先调用 memory_manage(action='put');同一 namespace/key 可直接覆盖更新。 -### 以下场景视为高价值加分 +### 以下场景视为高价值加分,必须记录记忆 - 用户多次跟你交互去优化输出 - 用户对你的纠正 - 确定的事实,路径/地址/网址等 diff --git a/src/channels/wechat.rs b/src/channels/wechat.rs index cc6ba01..404cebf 100644 --- a/src/channels/wechat.rs +++ b/src/channels/wechat.rs @@ -160,6 +160,12 @@ impl WechatChannel { .map(ToOwned::to_owned); Ok(vec![media_item]) } + + async fn send_typing_indicator(bot: Arc, chat_id: &str) { + if let Err(error) = bot.send_typing(chat_id).await { + tracing::debug!(chat_id = %chat_id, error = %error, "Failed to send WeChat typing indicator"); + } + } } #[async_trait] @@ -202,6 +208,8 @@ impl Channel for WechatChannel { let bot = bot_for_handler.clone(); let channel_name_for_publish = channel_name.clone(); tokio::spawn(async move { + Self::send_typing_indicator(bot.clone(), &sender_id).await; + let media = match Self::download_inbound_media(bot, msg.clone()).await { Ok(media) => media, Err(error) => { diff --git a/src/skills/mod.rs b/src/skills/mod.rs index d243878..e0b7381 100644 --- a/src/skills/mod.rs +++ b/src/skills/mod.rs @@ -429,18 +429,25 @@ impl SkillCatalog { } let mut prompt = String::from( - "You have access to skills discovered from local skill directories. Use a skill only when the user's request clearly matches the skill description.\nSkills are not tools. Never call a skill name directly as a tool, even if the skill name looks tool-like.\nIf a skill is needed, you must call tool skill_activate with {\"name\": \"\"} before using the skill instructions.\nDo not call directly. Always call skill_activate first.\nAvailable skills:\n", + "技能为特定任务提供专用说明和工作流。\n当任务匹配其描述时,使用 skill_activate 工具加载技能。\n技能不是工具名,即使技能名看起来像工具,也不能直接调用技能名。\n如果需要某个技能,必须先调用 tool skill_activate,并传入 {\"name\": \"\"},再根据返回的技能说明执行。\n\n\n", ); for skill in self.skills.iter().take(self.max_listed_skills) { - let line = format!("- {}: {}\n", skill.name, skill.description); - if prompt.len() + line.len() > self.max_index_chars { - prompt.push_str("- ... (truncated)\n"); + let entry = format!( + " \n {}\n {}\n {}\n \n", + xml_escape(&skill.name), + xml_escape(&skill.description), + xml_escape(&format!("file://{}", skill.path.display())), + ); + if prompt.len() + entry.len() + "\n".len() > self.max_index_chars { + prompt.push_str(" true\n"); break; } - prompt.push_str(&line); + prompt.push_str(&entry); } + prompt.push_str("\n"); + Some(prompt) } @@ -827,6 +834,13 @@ fn split_frontmatter(content: &str) -> Option<(&str, &str)> { Some((frontmatter, body)) } +fn xml_escape(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + #[cfg(test)] mod tests { use super::*; @@ -925,6 +939,29 @@ mod tests { assert!(payload.contains("Step A")); } + #[test] + fn test_system_index_prompt_uses_available_skills_markup() { + let catalog = SkillCatalog { + skills: vec![Skill { + name: "demo-skill".to_string(), + description: "demo & usage".to_string(), + body: String::new(), + source: SkillSource::Project, + path: PathBuf::from("/tmp/demo-skill/SKILL.md"), + }], + max_index_chars: 4000, + max_listed_skills: 32, + }; + + let prompt = catalog.system_index_prompt().unwrap(); + assert!(prompt.contains("")); + assert!(prompt.contains("技能为特定任务提供专用说明和工作流。")); + assert!(prompt.contains("demo-skill")); + assert!(prompt.contains("demo <skill> & usage")); + assert!(prompt.contains("file:///tmp/demo-skill/SKILL.md")); + assert!(prompt.contains("")); + } + #[test] fn test_runtime_create_update_delete_reload() { let _lock = acquire_test_lock();