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