feat(gateway): 添加 show_tool_results 配置以控制工具结果显示

feat(session): 更新 BusToolCallEmitter 以支持工具结果显示控制
feat(ws): 更新 WsToolCallEmitter 以支持工具结果显示控制
This commit is contained in:
ooodc 2026-04-22 16:14:27 +08:00
parent 65abf017a1
commit 302e6ef6b9
4 changed files with 129 additions and 5 deletions

View File

@ -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");

View File

@ -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,

View File

@ -40,6 +40,7 @@ pub struct BusToolCallEmitter {
channel_name: String,
chat_id: String,
metadata: HashMap<String, String>,
show_tool_results: bool,
}
impl BusToolCallEmitter {
@ -48,12 +49,14 @@ impl BusToolCallEmitter {
channel_name: impl Into<String>,
chat_id: impl Into<String>,
metadata: HashMap<String, String>,
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<SkillRuntime> {
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));

View File

@ -13,11 +13,16 @@ use super::{GatewayState, session::{Session, handle_in_chat_command}};
struct WsToolCallEmitter {
sender: mpsc::Sender<WsOutbound>,
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<WsOutbound> {
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<WsOutbound> {
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());
}
}