From 302e6ef6b9da80c52fb2a591b34e781bda83a956 Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Wed, 22 Apr 2026 16:14:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(gateway):=20=E6=B7=BB=E5=8A=A0=20show=5Fto?= =?UTF-8?q?ol=5Fresults=20=E9=85=8D=E7=BD=AE=E4=BB=A5=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=BB=93=E6=9E=9C=E6=98=BE=E7=A4=BA=20feat(s?= =?UTF-8?q?ession):=20=E6=9B=B4=E6=96=B0=20BusToolCallEmitter=20=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B7=A5=E5=85=B7=E7=BB=93=E6=9E=9C=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=8E=A7=E5=88=B6=20feat(ws):=20=E6=9B=B4=E6=96=B0=20?= =?UTF-8?q?WsToolCallEmitter=20=E4=BB=A5=E6=94=AF=E6=8C=81=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=BB=93=E6=9E=9C=E6=98=BE=E7=A4=BA=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bus/message.rs | 42 ++++++++++++++++++++++++++++-- src/gateway/mod.rs | 1 + src/gateway/session.rs | 32 +++++++++++++++++++++++ src/gateway/ws.rs | 59 +++++++++++++++++++++++++++++++++++++++--- 4 files changed, 129 insertions(+), 5 deletions(-) diff --git a/src/bus/message.rs b/src/bus/message.rs index 31be986..2c8a373 100644 --- a/src/bus/message.rs +++ b/src/bus/message.rs @@ -356,7 +356,18 @@ impl OutboundMessage { match message.role.as_str() { "assistant" => { if let Some(tool_calls) = &message.tool_calls { - tool_calls + let mut outbound = Vec::new(); + if !message.content.trim().is_empty() { + outbound.push(Self::assistant( + channel.to_string(), + chat_id.to_string(), + message.content.clone(), + reply_to.clone(), + metadata.clone(), + )); + } + + outbound.extend(tool_calls .iter() .map(|tool_call| { Self::tool_call( @@ -369,7 +380,8 @@ impl OutboundMessage { metadata.clone(), ) }) - .collect() + ); + outbound } else { vec![Self::assistant( channel.to_string(), @@ -488,6 +500,32 @@ mod tests { assert_eq!(outbound[1].content, "### file_read\n- path: README.md"); } + #[test] + fn test_from_chat_message_keeps_assistant_content_when_tool_calls_exist() { + let message = ChatMessage::assistant_with_tool_calls( + "日报已整理完成。", + vec![ToolCall { + id: "call-1".to_string(), + name: "memory_manage".to_string(), + arguments: json!({"action": "put"}), + }], + ); + + let outbound = OutboundMessage::from_chat_message( + "feishu", + "chat-1", + None, + &HashMap::new(), + &message, + ); + + assert_eq!(outbound.len(), 2); + assert_eq!(outbound[0].event_kind, OutboundEventKind::AssistantResponse); + assert_eq!(outbound[0].content, "日报已整理完成。"); + assert_eq!(outbound[1].event_kind, OutboundEventKind::ToolCall); + assert_eq!(outbound[1].tool_name.as_deref(), Some("memory_manage")); + } + #[test] fn test_from_chat_message_includes_tool_result() { let message = ChatMessage::tool("call-9", "calculator", "2"); diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 76e3f77..fb99e23 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -87,6 +87,7 @@ impl GatewayState { inbound.channel.clone(), inbound.chat_id.clone(), inbound.forwarded_metadata.clone(), + session_manager.show_tool_results(), )); match session_manager.handle_message( &inbound.channel, diff --git a/src/gateway/session.rs b/src/gateway/session.rs index 28e61ca..ea38da8 100644 --- a/src/gateway/session.rs +++ b/src/gateway/session.rs @@ -40,6 +40,7 @@ pub struct BusToolCallEmitter { channel_name: String, chat_id: String, metadata: HashMap, + show_tool_results: bool, } impl BusToolCallEmitter { @@ -48,12 +49,14 @@ impl BusToolCallEmitter { channel_name: impl Into, chat_id: impl Into, metadata: HashMap, + show_tool_results: bool, ) -> Self { Self { bus, channel_name: channel_name.into(), chat_id: chat_id.into(), metadata, + show_tool_results, } } } @@ -61,6 +64,10 @@ impl BusToolCallEmitter { #[async_trait] impl EmittedMessageHandler for BusToolCallEmitter { async fn handle(&self, message: ChatMessage) { + if !should_display_message_to_user(self.show_tool_results, &message) { + return; + } + for outbound in OutboundMessage::from_chat_message( &self.channel_name, &self.chat_id, @@ -453,6 +460,10 @@ impl SessionManager { self.store.clone() } + pub fn show_tool_results(&self) -> bool { + self.show_tool_results + } + pub fn skills(&self) -> Arc { self.skills.clone() } @@ -699,6 +710,7 @@ fn should_display_message_to_user(show_tool_results: bool, message: &ChatMessage #[cfg(test)] mod tests { use super::*; + use crate::bus::MessageBus; use std::collections::HashMap; use tokio::sync::mpsc; @@ -733,6 +745,26 @@ mod tests { assert!(should_display_message_to_user(true, &completed)); } + #[tokio::test] + async fn test_bus_tool_call_emitter_hides_completed_tool_results_when_disabled() { + let bus = MessageBus::new(4); + let emitter = BusToolCallEmitter::new( + bus.clone(), + "feishu", + "chat-1", + HashMap::new(), + false, + ); + + emitter + .handle(ChatMessage::tool("call-1", "calculator", "2")) + .await; + + assert!(tokio::time::timeout(std::time::Duration::from_millis(50), bus.consume_outbound()) + .await + .is_err()); + } + #[test] fn test_parse_in_chat_command_aliases() { assert_eq!(parse_in_chat_command("/new"), Some(InChatCommand::FreshConversation)); diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index b7da7e9..3b8bc22 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -13,11 +13,16 @@ use super::{GatewayState, session::{Session, handle_in_chat_command}}; struct WsToolCallEmitter { sender: mpsc::Sender, + show_tool_results: bool, } #[async_trait] impl EmittedMessageHandler for WsToolCallEmitter { async fn handle(&self, message: ChatMessage) { + if !should_display_message_to_user(self.show_tool_results, &message) { + return; + } + for outbound in ws_outbound_from_chat_message(&message) { let _ = self.sender.send(outbound).await; } @@ -162,7 +167,16 @@ fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec { match message.role.as_str() { "assistant" => { if let Some(tool_calls) = &message.tool_calls { - tool_calls + let mut outbound = Vec::new(); + if !message.content.trim().is_empty() { + outbound.push(WsOutbound::AssistantResponse { + id: message.id.clone(), + content: message.content.clone(), + role: message.role.clone(), + }); + } + + outbound.extend(tool_calls .iter() .map(|tool_call| WsOutbound::ToolCall { id: message.id.clone(), @@ -172,7 +186,8 @@ fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec { content: format_tool_call_content(&tool_call.name, &tool_call.arguments), role: message.role.clone(), }) - .collect() + ); + outbound } else { vec![WsOutbound::AssistantResponse { id: message.id.clone(), @@ -262,6 +277,7 @@ async fn handle_inbound( let live_emitter = Arc::new(WsToolCallEmitter { sender: session_guard.user_tx.clone(), + show_tool_results: state.config.gateway.show_tool_results, }); let agent = session_guard .create_agent(&chat_id, None, Some(&user_message_id))? @@ -420,12 +436,14 @@ async fn handle_inbound( #[cfg(test)] mod tests { - use super::{should_display_message_to_user, ws_outbound_from_chat_message}; + use crate::agent::EmittedMessageHandler; + use super::{WsToolCallEmitter, should_display_message_to_user, ws_outbound_from_chat_message}; use crate::bus::ChatMessage; use crate::bus::message::ToolMessageState; use crate::providers::ToolCall; use crate::protocol::WsOutbound; use serde_json::json; + use tokio::sync::mpsc; #[test] fn test_ws_outbound_from_chat_message_expands_tool_calls() { @@ -452,6 +470,24 @@ mod tests { } } + #[test] + fn test_ws_outbound_keeps_assistant_content_when_tool_calls_exist() { + let message = ChatMessage::assistant_with_tool_calls( + "日报已整理完成。", + vec![ToolCall { + id: "call-1".to_string(), + name: "memory_manage".to_string(), + arguments: json!({"action": "put"}), + }], + ); + + let outbound = ws_outbound_from_chat_message(&message); + + assert_eq!(outbound.len(), 2); + assert!(matches!(outbound[0], WsOutbound::AssistantResponse { .. })); + assert!(matches!(outbound[1], WsOutbound::ToolCall { .. })); + } + #[test] fn test_ws_outbound_from_chat_message_includes_tool_results() { let message = ChatMessage::tool("call-1", "calculator", "2"); @@ -491,4 +527,21 @@ mod tests { assert!(should_display_message_to_user(false, &pending)); assert!(should_display_message_to_user(true, &completed)); } + + #[tokio::test] + async fn test_ws_tool_call_emitter_hides_completed_tool_results_when_disabled() { + let (sender, mut receiver) = mpsc::channel(4); + let emitter = WsToolCallEmitter { + sender, + show_tool_results: false, + }; + + emitter + .handle(ChatMessage::tool("call-1", "calculator", "2")) + .await; + + assert!(tokio::time::timeout(std::time::Duration::from_millis(50), receiver.recv()) + .await + .is_err()); + } }