diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java index 1dff350c..e47bd461 100644 --- a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java +++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java @@ -930,7 +930,7 @@ public class AgentRunService { buildAssistantRuntimeMessage(chatContext, finalAnswer, assistantAccumulator, citations)); chatRuntimeManager.recordCompleted(chatContext); } - sendDone(chatSseEmitter); + sendDone(chatSseEmitter, finalAnswer); } private void handleRuntimeError(Throwable error, @@ -1184,10 +1184,30 @@ public class AgentRunService { return chatSseEmitter.send(envelope); } + /** + * 发送不携带最终正文的完成事件。 + * + * @param chatSseEmitter SSE 发送器 + * @return 发送成功时为 {@code true} + */ private boolean sendDone(ChatSseEmitter chatSseEmitter) { + return sendDone(chatSseEmitter, null); + } + + /** + * 发送完成事件,并提供最终正文供前端校正流式增量。 + * + * @param chatSseEmitter SSE 发送器 + * @param finalText 最终完整正文;取消场景可为空 + * @return 发送成功时为 {@code true} + */ + private boolean sendDone(ChatSseEmitter chatSseEmitter, String finalText) { ChatEnvelope> envelope = new ChatEnvelope<>(); envelope.setDomain(ChatDomain.SYSTEM); envelope.setType(ChatType.DONE); + if (finalText != null) { + envelope.setPayload(Map.of("finalText", finalText)); + } return chatSseEmitter.sendDone(envelope); } diff --git a/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceDraftAndHitlTest.java b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceDraftAndHitlTest.java index 4ddbb57e..fa99cade 100644 --- a/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceDraftAndHitlTest.java +++ b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceDraftAndHitlTest.java @@ -229,8 +229,14 @@ public class AgentRunServiceDraftAndHitlTest { Assert.assertEquals("先分析", reasoning.toString()); Assert.assertEquals("\n最终回答", content.toString()); Assert.assertEquals("\n最终回答", answer.toString()); - Assert.assertTrue(emitter.envelopes.stream().anyMatch(envelope -> - envelope.getDomain() == ChatDomain.SYSTEM && envelope.getType() == ChatType.DONE)); + ChatEnvelope done = emitter.envelopes.stream() + .filter(envelope -> envelope.getDomain() == ChatDomain.SYSTEM + && envelope.getType() == ChatType.DONE) + .findFirst() + .orElseThrow(); + @SuppressWarnings("unchecked") + Map donePayload = (Map) done.getPayload(); + Assert.assertEquals("\n最终回答", donePayload.get("finalText")); } /** diff --git a/easyflow-ui-admin/app/src/views/ai/agent-chat/adapters/agentTimelineAdapter.test.ts b/easyflow-ui-admin/app/src/views/ai/agent-chat/adapters/agentTimelineAdapter.test.ts index 32af49e8..9097871a 100644 --- a/easyflow-ui-admin/app/src/views/ai/agent-chat/adapters/agentTimelineAdapter.test.ts +++ b/easyflow-ui-admin/app/src/views/ai/agent-chat/adapters/agentTimelineAdapter.test.ts @@ -298,6 +298,38 @@ describe('agentTimelineAdapter', () => { }); }); + it('reconciles streamed text with the canonical final answer', () => { + const items: any[] = []; + + for (const delta of [ + 'http://127.0.0.1:39', + '0', + '/easyflow/file.docx', + ]) { + applyAgentSseEnvelope(items, { + domain: 'LLM', + type: 'MESSAGE', + payload: { delta }, + }); + } + applyAgentSseEnvelope(items, { + domain: 'SYSTEM', + type: 'DONE', + payload: { + finalText: 'http://127.0.0.1:39000/easyflow/file.docx', + }, + }); + + const assistant = items.find( + (item): item is ChatTimelineMessageItem => + item.type === 'message' && item.role === 'assistant', + ); + expect(assistant?.parts[0]?.content).toBe( + 'http://127.0.0.1:39000/easyflow/file.docx', + ); + expect(assistant?.status).toBe('done'); + }); + it('applies streaming text, HITL approval and error envelopes', () => { const items: any[] = []; diff --git a/easyflow-ui-admin/app/src/views/ai/agent-chat/adapters/agentTimelineAdapter.ts b/easyflow-ui-admin/app/src/views/ai/agent-chat/adapters/agentTimelineAdapter.ts index c67d3af9..4a6bd5e1 100644 --- a/easyflow-ui-admin/app/src/views/ai/agent-chat/adapters/agentTimelineAdapter.ts +++ b/easyflow-ui-admin/app/src/views/ai/agent-chat/adapters/agentTimelineAdapter.ts @@ -534,6 +534,10 @@ export function applyAgentSseEnvelope( return; } if (domain === 'SYSTEM' && type === 'DONE') { + const finalText = asText(payload.finalText ?? payload.text); + if (finalText) { + ChatTimelineBuilder.replaceMessageContent(items, finalText); + } ChatTimelineBuilder.finalize(items); return; }