diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/DirectAnswerNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/DirectAnswerNode.java index 550cbf3c..167e1a25 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/DirectAnswerNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/DirectAnswerNode.java @@ -3,19 +3,37 @@ package vip.mate.agent.graph.plan.node; import com.alibaba.cloud.ai.graph.OverAllState; import com.alibaba.cloud.ai.graph.action.NodeAction; import vip.mate.agent.graph.plan.state.PlanStateKeys; -import vip.mate.agent.graph.state.MateClawStateKeys; import java.util.Map; /** * 直接回答节点 *

- * 当 PlanGenerationNode 判定用户消息是简单问答时, - * 将 direct_answer 透传为 final_summary,直接结束图执行。 + * When PlanGenerationNode classifies the user's message as a simple + * question, this node propagates {@code direct_answer} into + * {@code FINAL_SUMMARY} so the graph terminates with the answer in the + * canonical place every downstream consumer reads from. *

- * 如果 PlanGenerationNode 已通过 broadcastContent() 推送了内容 - * (contentStreamed=true),则不再复制到 FINAL_SUMMARY, - * 避免 StreamAccumulator 重复收集导致持久化内容翻倍。 + * Earlier versions skipped writing FINAL_SUMMARY when + * {@code CONTENT_STREAMED=true}, on the theory that broadcastContent had + * already pushed the text and a second copy in FINAL_SUMMARY would cause + * double persistence. That was wrong: broadcastContent goes directly to + * the SSE side-channel via {@code streamTracker.broadcastDelta} and does + * NOT participate in the DB segment that ChatController accumulates from + * the structured stream. Skipping FINAL_SUMMARY left + * {@code AgentService.chat()} (the sync entry used by every IM channel) + * with an empty reply, which silently dropped DingTalk / Slack / + * Telegram replies on the direct-answer path. It also left + * {@code mate_message.content} empty on the web channel — the SSE + * client saw the answer in real time but reopening the conversation + * showed a blank assistant turn. + *

+ * Re-broadcast suppression is the responsibility of the stream layer, + * not this node: + * {@link vip.mate.agent.graph.plan.StateGraphPlanExecuteAgent#chatStructuredStream} + * tags the FINAL_SUMMARY delta as {@code persistOnly} when + * CONTENT_STREAMED is true, and {@code ChatController} respects that flag + * to persist without re-pushing. * * @author MateClaw Team */ @@ -23,11 +41,6 @@ public class DirectAnswerNode implements NodeAction { @Override public Map apply(OverAllState state) { - boolean alreadyStreamed = state.value(MateClawStateKeys.CONTENT_STREAMED, false); - if (alreadyStreamed) { - // broadcastContent 已推送并被 accumulator 收集,不重复写入 FINAL_SUMMARY - return Map.of(); - } String directAnswer = state.value(PlanStateKeys.DIRECT_ANSWER, ""); return Map.of(PlanStateKeys.FINAL_SUMMARY, directAnswer); }