feat: 新增 ExecutionCompleted 信号修复发送按钮状态
- 后端 OutboundEventKind/WsOutbound 新增 ExecutionCompleted 变体 - processor 在 handle_message 完成后发送该信号 - 飞书/微信通道过滤该信号(不发送) - 前端 useChat 移除 stream_delta/assistant_response 的 isLoading=false - 改由 execution_completed 事件统一设置 isLoading=false,确保智能体迭代期间按钮保持停止态
This commit is contained in:
parent
76abdbd1de
commit
994db87f11
@ -425,6 +425,8 @@ pub enum OutboundEventKind {
|
|||||||
StreamDelta,
|
StreamDelta,
|
||||||
/// 流式结束信号
|
/// 流式结束信号
|
||||||
StreamEnd,
|
StreamEnd,
|
||||||
|
/// 智能体执行完全结束(不再有后续工具调用或 LLM 迭代)
|
||||||
|
ExecutionCompleted,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutboundMessage {
|
impl OutboundMessage {
|
||||||
@ -630,6 +632,31 @@ impl OutboundMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 构造执行完成信号
|
||||||
|
pub fn execution_completed(
|
||||||
|
channel: impl Into<String>,
|
||||||
|
chat_id: impl Into<String>,
|
||||||
|
session_id: Option<String>,
|
||||||
|
metadata: HashMap<String, String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
channel: channel.into(),
|
||||||
|
chat_id: chat_id.into(),
|
||||||
|
session_id,
|
||||||
|
content: String::new(),
|
||||||
|
reply_to: None,
|
||||||
|
media: Vec::new(),
|
||||||
|
metadata,
|
||||||
|
event_kind: OutboundEventKind::ExecutionCompleted,
|
||||||
|
role: "assistant".to_string(),
|
||||||
|
tool_call_id: None,
|
||||||
|
tool_name: None,
|
||||||
|
tool_arguments: None,
|
||||||
|
reasoning_content: None,
|
||||||
|
message_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_chat_message(
|
pub fn from_chat_message(
|
||||||
channel: &str,
|
channel: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
|
|||||||
@ -2461,7 +2461,7 @@ impl Channel for FeishuChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn send(&self, msg: OutboundMessage) -> Result<(), ChannelError> {
|
async fn send(&self, msg: OutboundMessage) -> Result<(), ChannelError> {
|
||||||
if matches!(msg.event_kind, OutboundEventKind::ToolResult | OutboundEventKind::ToolPending | OutboundEventKind::StreamDelta | OutboundEventKind::StreamEnd)
|
if matches!(msg.event_kind, OutboundEventKind::ToolResult | OutboundEventKind::ToolPending | OutboundEventKind::StreamDelta | OutboundEventKind::StreamEnd | OutboundEventKind::ExecutionCompleted)
|
||||||
|| msg.metadata.get("is_subagent_event").map(|v| v == "true").unwrap_or(false)
|
|| msg.metadata.get("is_subagent_event").map(|v| v == "true").unwrap_or(false)
|
||||||
{
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@ -315,6 +315,7 @@ impl Channel for WechatChannel {
|
|||||||
| OutboundEventKind::ToolCall
|
| OutboundEventKind::ToolCall
|
||||||
| OutboundEventKind::StreamDelta
|
| OutboundEventKind::StreamDelta
|
||||||
| OutboundEventKind::StreamEnd
|
| OutboundEventKind::StreamEnd
|
||||||
|
| OutboundEventKind::ExecutionCompleted
|
||||||
) || msg.metadata.get("is_subagent_event").map(|v| v == "true").unwrap_or(false)
|
) || msg.metadata.get("is_subagent_event").map(|v| v == "true").unwrap_or(false)
|
||||||
{
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@ -387,6 +387,25 @@ impl InboundProcessor {
|
|||||||
self.cancel_manager.remove_by_topic(topic_id).await;
|
self.cancel_manager.remove_by_topic(topic_id).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发送执行完成信号,通知前端可以停止 loading 状态
|
||||||
|
// 无论成功还是失败都发送,确保前端状态正确
|
||||||
|
let mut completion_metadata = inbound.forwarded_metadata.clone();
|
||||||
|
if let Some(ref topic_id) = current_topic {
|
||||||
|
completion_metadata.insert("topic_id".to_string(), topic_id.clone());
|
||||||
|
}
|
||||||
|
if let Err(error) = self
|
||||||
|
.bus
|
||||||
|
.publish_outbound(OutboundMessage::execution_completed(
|
||||||
|
channel,
|
||||||
|
chat_id,
|
||||||
|
Some(session_id),
|
||||||
|
completion_metadata,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!(error = %error, "Failed to publish execution_completed");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -295,6 +295,13 @@ pub enum WsOutbound {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
topic_id: Option<String>,
|
topic_id: Option<String>,
|
||||||
},
|
},
|
||||||
|
#[serde(rename = "execution_completed")]
|
||||||
|
ExecutionCompleted {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
topic_id: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
timestamp: Option<i64>,
|
||||||
|
},
|
||||||
#[serde(rename = "todo_list")]
|
#[serde(rename = "todo_list")]
|
||||||
TodoList {
|
TodoList {
|
||||||
todos: Vec<TodoItemSummary>,
|
todos: Vec<TodoItemSummary>,
|
||||||
|
|||||||
@ -194,6 +194,10 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
|
|||||||
subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
|
subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
|
||||||
topic_id: message.metadata.get("topic_id").cloned(),
|
topic_id: message.metadata.get("topic_id").cloned(),
|
||||||
}],
|
}],
|
||||||
|
OutboundEventKind::ExecutionCompleted => vec![WsOutbound::ExecutionCompleted {
|
||||||
|
topic_id: message.metadata.get("topic_id").cloned(),
|
||||||
|
timestamp: Some(crate::protocol::now_timestamp()),
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import type {
|
|||||||
ChannelList,
|
ChannelList,
|
||||||
StreamDelta,
|
StreamDelta,
|
||||||
StreamEnd,
|
StreamEnd,
|
||||||
|
ExecutionCompleted,
|
||||||
WsInbound,
|
WsInbound,
|
||||||
} from '../types/protocol'
|
} from '../types/protocol'
|
||||||
|
|
||||||
@ -658,7 +659,7 @@ export function useChat(): UseChatReturn {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
setIsLoading(false)
|
// 注意:stream_delta 期间不设置 isLoading=false,智能体仍在生成
|
||||||
if (msg.user_message_id) applyUserMessageId(msg.user_message_id)
|
if (msg.user_message_id) applyUserMessageId(msg.user_message_id)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -668,6 +669,15 @@ export function useChat(): UseChatReturn {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'execution_completed': {
|
||||||
|
// 智能体执行完全结束(不再有后续工具调用或 LLM 迭代)
|
||||||
|
const msg = message as ExecutionCompleted
|
||||||
|
// 按 topic_id 隔离:只处理当前话题的完成信号
|
||||||
|
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return
|
||||||
|
setIsLoading(false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'assistant_response': {
|
case 'assistant_response': {
|
||||||
const msg = message as AssistantResponse
|
const msg = message as AssistantResponse
|
||||||
// 按 topic_id 隔离:如果消息属于其他话题则丢弃
|
// 按 topic_id 隔离:如果消息属于其他话题则丢弃
|
||||||
@ -692,7 +702,7 @@ export function useChat(): UseChatReturn {
|
|||||||
}
|
}
|
||||||
return [...prev, newMsg]
|
return [...prev, newMsg]
|
||||||
})
|
})
|
||||||
setIsLoading(false)
|
// 注意:assistant_response 不设置 isLoading=false,智能体可能还会调用工具继续迭代
|
||||||
|
|
||||||
// 当前话题无描述时,可能刚触发了异步生成,标记需要刷新
|
// 当前话题无描述时,可能刚触发了异步生成,标记需要刷新
|
||||||
const currentTopic = topicsRef.current.find(t => t.id === selectedTopicRef.current)
|
const currentTopic = topicsRef.current.find(t => t.id === selectedTopicRef.current)
|
||||||
|
|||||||
@ -278,6 +278,12 @@ export interface StreamEnd {
|
|||||||
topic_id?: string
|
topic_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExecutionCompleted {
|
||||||
|
type: 'execution_completed'
|
||||||
|
topic_id?: string
|
||||||
|
timestamp?: number
|
||||||
|
}
|
||||||
|
|
||||||
export type WsOutbound =
|
export type WsOutbound =
|
||||||
| AssistantResponse
|
| AssistantResponse
|
||||||
| ToolCall
|
| ToolCall
|
||||||
@ -287,6 +293,7 @@ export type WsOutbound =
|
|||||||
| TaskStarted
|
| TaskStarted
|
||||||
| StreamDelta
|
| StreamDelta
|
||||||
| StreamEnd
|
| StreamEnd
|
||||||
|
| ExecutionCompleted
|
||||||
| SessionEstablished
|
| SessionEstablished
|
||||||
| SessionCreated
|
| SessionCreated
|
||||||
| SessionList
|
| SessionList
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user