From 5050ce4ff830f7df1c2ff90f061f4a0d9c33eaa0 Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 24 Apr 2026 12:17:17 +0800 Subject: [PATCH] fix: delay creation agent reply until final session --- ...RLD_AGENT_FINAL_REPLY_TIMING_2026-04-24.md | 23 +++++++++ .../crates/api-server/src/custom_world.rs | 48 +++++-------------- .../creation-agent/CreationAgentWorkspace.tsx | 3 +- 3 files changed, 37 insertions(+), 37 deletions(-) create mode 100644 docs/technical/CUSTOM_WORLD_AGENT_FINAL_REPLY_TIMING_2026-04-24.md diff --git a/docs/technical/CUSTOM_WORLD_AGENT_FINAL_REPLY_TIMING_2026-04-24.md b/docs/technical/CUSTOM_WORLD_AGENT_FINAL_REPLY_TIMING_2026-04-24.md new file mode 100644 index 00000000..d559062e --- /dev/null +++ b/docs/technical/CUSTOM_WORLD_AGENT_FINAL_REPLY_TIMING_2026-04-24.md @@ -0,0 +1,23 @@ +# 世界共创聊天最终回复时机调整(2026-04-24) + +## 背景 + +创作聊天页顶部进度条来自后端会话快照 `progressPercent`,而助手文本此前通过 SSE `reply_delta` 在模型生成过程中提前展示。这样会导致玩家先看到完整或接近完整的文本回复,但进度、锚点、阶段与推荐操作仍停留在上一轮状态。 + +## 本次约束 + +- 玩家可继续看到自己的输入被乐观插入聊天线程。 +- 助手回复不再通过中途 `reply_delta` 展示。 +- 本轮模型输出解析、SpacetimeDB finalize、最终 session 快照读取全部完成后,助手回复才随最终 session 一次性显示。 +- 进度条、阶段、锚点内容、推荐动作和助手回复在同一次 session 替换中同步刷新。 + +## 落地方案 + +1. `server-rs/crates/api-server/src/custom_world.rs` 的 `stream_custom_world_agent_message` 保留 SSE 响应格式,但不再发送 `reply_delta` 事件。 +2. 同一接口仍等待 `run_custom_world_agent_turn` 完成,再调用 `finalize_custom_world_agent_message` 写入 SpacetimeDB。 +3. finalize 成功后读取最终 session,并通过 `session` 事件一次性返回给前端。 +4. `src/components/creation-agent/CreationAgentWorkspace.tsx` 仅在确实存在流式文本时显示临时助手气泡,避免无 `reply_delta` 时出现空回复。 + +## 预期体验 + +发送消息后,玩家会先看到自己的消息和忙碌状态;助手文本会在进度、阶段与会话数据全部更新后一次性出现,避免“回复已经到了但进度还没动”的错位感。 diff --git a/server-rs/crates/api-server/src/custom_world.rs b/server-rs/crates/api-server/src/custom_world.rs index c23dfb5d..d5ca1d2c 100644 --- a/server-rs/crates/api-server/src/custom_world.rs +++ b/server-rs/crates/api-server/src/custom_world.rs @@ -730,42 +730,18 @@ pub async fn stream_custom_world_agent_message( let owner_user_id_for_stream = owner_user_id.clone(); let operation_id = operation.operation_id.clone(); let stream = async_stream::stream! { - let (reply_tx, mut reply_rx) = tokio::sync::mpsc::unbounded_channel::(); - let turn_result = { - let run_turn = run_custom_world_agent_turn( - CustomWorldAgentTurnRequest { - llm_client: state.llm_client(), - session: &session, - quick_fill_requested, - focus_card_id, - }, - move |text| { - let _ = reply_tx.send(text.to_string()); - }, - ); - tokio::pin!(run_turn); - - loop { - tokio::select! { - result = &mut run_turn => break result, - maybe_text = reply_rx.recv() => { - if let Some(text) = maybe_text { - yield Ok::(custom_world_sse_json_event_or_error( - "reply_delta", - json!({ "text": text }), - )); - } - } - } - } - }; - - while let Some(text) = reply_rx.recv().await { - yield Ok::(custom_world_sse_json_event_or_error( - "reply_delta", - json!({ "text": text }), - )); - } + // 聊天回复必须等本轮模型解析、进度与会话快照全部落库后, + // 再随最终 session 一次性返回,避免玩家先看到回复而进度仍停在旧状态。 + let turn_result = run_custom_world_agent_turn( + CustomWorldAgentTurnRequest { + llm_client: state.llm_client(), + session: &session, + quick_fill_requested, + focus_card_id, + }, + |_| {}, + ) + .await; let finalize_input = match turn_result { Ok(turn_result) => build_finalize_record_input( diff --git a/src/components/creation-agent/CreationAgentWorkspace.tsx b/src/components/creation-agent/CreationAgentWorkspace.tsx index 6bf65c18..ccd54771 100644 --- a/src/components/creation-agent/CreationAgentWorkspace.tsx +++ b/src/components/creation-agent/CreationAgentWorkspace.tsx @@ -317,7 +317,8 @@ export function CreationAgentWorkspace({ shouldShowQuickAction(action, session, progress), ); const streamingMessageId = `streaming-assistant-${session.sessionId}`; - const displayedMessages = isStreamingReply + const shouldShowStreamingReply = isStreamingReply && streamingReplyText.trim(); + const displayedMessages = shouldShowStreamingReply ? [ ...session.messages, {