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,
|
||||
/// 流式结束信号
|
||||
StreamEnd,
|
||||
/// 智能体执行完全结束(不再有后续工具调用或 LLM 迭代)
|
||||
ExecutionCompleted,
|
||||
}
|
||||
|
||||
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(
|
||||
channel: &str,
|
||||
chat_id: &str,
|
||||
|
||||
@ -2461,7 +2461,7 @@ impl Channel for FeishuChannel {
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return Ok(());
|
||||
|
||||
@ -315,6 +315,7 @@ impl Channel for WechatChannel {
|
||||
| OutboundEventKind::ToolCall
|
||||
| OutboundEventKind::StreamDelta
|
||||
| OutboundEventKind::StreamEnd
|
||||
| OutboundEventKind::ExecutionCompleted
|
||||
) || msg.metadata.get("is_subagent_event").map(|v| v == "true").unwrap_or(false)
|
||||
{
|
||||
return Ok(());
|
||||
|
||||
@ -387,6 +387,25 @@ impl InboundProcessor {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -295,6 +295,13 @@ pub enum WsOutbound {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
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")]
|
||||
TodoList {
|
||||
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(),
|
||||
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,
|
||||
StreamDelta,
|
||||
StreamEnd,
|
||||
ExecutionCompleted,
|
||||
WsInbound,
|
||||
} 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)
|
||||
break
|
||||
}
|
||||
@ -668,6 +669,15 @@ export function useChat(): UseChatReturn {
|
||||
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': {
|
||||
const msg = message as AssistantResponse
|
||||
// 按 topic_id 隔离:如果消息属于其他话题则丢弃
|
||||
@ -692,7 +702,7 @@ export function useChat(): UseChatReturn {
|
||||
}
|
||||
return [...prev, newMsg]
|
||||
})
|
||||
setIsLoading(false)
|
||||
// 注意:assistant_response 不设置 isLoading=false,智能体可能还会调用工具继续迭代
|
||||
|
||||
// 当前话题无描述时,可能刚触发了异步生成,标记需要刷新
|
||||
const currentTopic = topicsRef.current.find(t => t.id === selectedTopicRef.current)
|
||||
|
||||
@ -278,6 +278,12 @@ export interface StreamEnd {
|
||||
topic_id?: string
|
||||
}
|
||||
|
||||
export interface ExecutionCompleted {
|
||||
type: 'execution_completed'
|
||||
topic_id?: string
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
export type WsOutbound =
|
||||
| AssistantResponse
|
||||
| ToolCall
|
||||
@ -287,6 +293,7 @@ export type WsOutbound =
|
||||
| TaskStarted
|
||||
| StreamDelta
|
||||
| StreamEnd
|
||||
| ExecutionCompleted
|
||||
| SessionEstablished
|
||||
| SessionCreated
|
||||
| SessionList
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user