diff --git a/src/gateway/session.rs b/src/gateway/session.rs index 880d238..16308ba 100644 --- a/src/gateway/session.rs +++ b/src/gateway/session.rs @@ -881,7 +881,7 @@ impl Session { /// 纯文本格式: `running, task_id=xxx. ...` /// /// 返回 (task_id, is_json)。仅当 status=="running" 时才提取(只对账 running 占位)。 -fn extract_task_id_from_content(content: &str) -> Option<(String, bool)> { +pub(crate) fn extract_task_id_from_content(content: &str) -> Option<(String, bool)> { // 尝试 JSON 格式 let json_start = content.find('{')?; if let Ok(parsed) = serde_json::from_str::(&content[json_start..]) { @@ -912,7 +912,7 @@ fn extract_task_id_from_content(content: &str) -> Option<(String, bool)> { /// /// JSON 格式:更新 JSON 中的 status 字段(保持前端 parseTaskResult 兼容)。 /// 纯文本格式:替换为描述性文本(向后兼容)。 -fn format_reconciled_content(task_id: &str, status: &str, is_json: bool) -> String { +pub(crate) fn format_reconciled_content(task_id: &str, status: &str, is_json: bool) -> String { if is_json { // 更新 JSON 中的 status 字段,保持前端 parseTaskResult 能正确解析 let placeholder = match status { diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index b428baa..88e28a9 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -806,7 +806,13 @@ async fn send_topic_history( task_repository: &Arc, ) -> Result<(), Box> { // 加载话题消息,按 session_id 过滤,避免混入子智能体消息 - let messages = store.load_messages_for_topic_full(topic_id, Some(session_id))?; + let mut messages = store.load_messages_for_topic_full(topic_id, Some(session_id))?; + + // 对账 running 占位:DB 中的 task tool_result 永远保持 spawn 时的 running 状态 + // (实时完成信号只更新前端内存与 pending_subagents 表),若不替换, + // 刷新/切话题后前端卡片会永远显示"运行中"。与 Session::reconcile_running_placeholders + // 同语义,仅改发送副本,不落库。 + reconcile_running_in_messages(&mut messages, store, topic_id); tracing::info!(topic_id = %topic_id, message_count = messages.len(), "Sending topic history"); @@ -877,6 +883,52 @@ async fn send_topic_history( Ok(()) } +/// 发送前对账消息列表中的 task "running" 占位。 +/// +/// DB messages 表中的 task tool_result 行固化在 spawn 时刻的 running 状态, +/// 完成信号只更新 pending_subagents 表;此处按该表的实际状态替换发送副本, +/// 避免前端刷新/切话题后卡片永远显示"运行中"。仍为 running 的保持原样。 +fn reconcile_running_in_messages( + messages: &mut [crate::bus::ChatMessage], + store: &Arc, + topic_id: &str, +) { + let has_running_placeholder = messages.iter().any(|m| { + m.role == "tool" && crate::gateway::session::extract_task_id_from_content(&m.content).is_some() + }); + if !has_running_placeholder { + return; // 无需查询 DB + } + + let pending = match store.list_pending_subagents(topic_id, None) { + Ok(records) => records, + Err(e) => { + tracing::warn!(error = %e, %topic_id, "Failed to query pending_subagents for history reconciliation"); + return; + } + }; + let status_map: std::collections::HashMap<&str, &str> = pending + .iter() + .map(|r| (r.task_id.as_str(), r.status.as_str())) + .collect(); + + for msg in messages.iter_mut() { + if msg.role != "tool" { + continue; + } + let Some((task_id, is_json)) = crate::gateway::session::extract_task_id_from_content(&msg.content) + else { + continue; + }; + match status_map.get(task_id.as_str()) { + Some(&status) if status != "running" => { + msg.content = crate::gateway::session::format_reconciled_content(&task_id, status, is_json); + } + _ => {} // 不存在(已清理)或仍在运行:保留原占位 + } + } +} + /// 加载并发送子智能体任务的历史消息 async fn send_task_messages( store: &Arc,