diff --git a/extensions/feishu/src/reply-dispatcher.test.ts b/extensions/feishu/src/reply-dispatcher.test.ts index a4a58d7759d..190bc007a4a 100644 --- a/extensions/feishu/src/reply-dispatcher.test.ts +++ b/extensions/feishu/src/reply-dispatcher.test.ts @@ -364,6 +364,27 @@ describe("createFeishuReplyDispatcher streaming behavior", () => { expect(sendMessageFeishuMock).not.toHaveBeenCalled(); expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled(); }); + + it("skips final text already closed by idle streaming", async () => { + const { result, options } = createDispatcherHarness({ + runtime: createRuntimeLogger(), + }); + + await options.onReplyStart?.(); + result.replyOptions.onPartialReply?.({ text: "```md\nidle streamed reply\n```" }); + await options.onIdle?.(); + await options.deliver({ text: "```md\nidle streamed reply\n```" }, { kind: "final" }); + + expect(streamingInstances).toHaveLength(1); + expect(streamingInstances[0].close).toHaveBeenCalledTimes(1); + expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\nidle streamed reply\n```", { + note: "Agent: agent", + }); + expect(sendMessageFeishuMock).not.toHaveBeenCalled(); + expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled(); + expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled(); + }); + it("suppresses duplicate final text while still sending media", async () => { const options = setupNonStreamingAutoDispatcher(); await options.deliver({ text: "plain final" }, { kind: "final" }); diff --git a/extensions/feishu/src/reply-dispatcher.ts b/extensions/feishu/src/reply-dispatcher.ts index fc2e18eb07a..b4fa4f909d9 100644 --- a/extensions/feishu/src/reply-dispatcher.ts +++ b/extensions/feishu/src/reply-dispatcher.ts @@ -311,6 +311,12 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP } const finalNote = resolveCardNote(agentId, identity, prefixContext.prefixContext); await streaming.close(text, { note: finalNote }); + // Track the raw streamed text so the duplicate-final check in deliver() + // can skip the redundant text delivery that arrives after onIdle closes + // the streaming card. + if (streamText) { + deliveredFinalTexts.add(streamText); + } } streaming = null; streamingStartPromise = null;