fix(ws): 历史加载时对账 task running 占位,修复刷新/切话题后子代理卡片永远显示运行中

DB messages 表的 task tool_result 固化在 spawn 时刻的 running 状态,完成信号只更新
pending_subagents 表;send_topic_history 原样发送导致刷新后卡片退回黄色运行中。
新增 reconcile_running_in_messages:发送前按 pending_subagents 实际状态替换发送副本,
复用 session.rs 提取/格式化函数(改为 pub(crate)),不落库且幂等,无占位时零查询。
This commit is contained in:
oudecheng 2026-08-16 17:27:55 +08:00
parent 867b7df409
commit 692cc93075
2 changed files with 55 additions and 3 deletions

View File

@ -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::<serde_json::Value>(&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 {

View File

@ -806,7 +806,13 @@ async fn send_topic_history(
task_repository: &Arc<dyn TaskRepository>,
) -> Result<(), Box<dyn std::error::Error>> {
// 加载话题消息,按 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<crate::storage::SessionStore>,
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<crate::storage::SessionStore>,