fix(feishu): prevent duplicate message after streaming card close (#67791) (#68491)

* fix(feishu): prevent duplicate message after streaming card close (#67791)

When onIdle closed the streaming card before the final delivery arrived, the streamed text was not tracked in deliveredFinalTexts. The subsequent final payload bypassed the streaming?.isActive() guard (already closed) and fell through to the non-streaming path, sending the same content as a redundant text/card message. Track raw streamText in deliveredFinalTexts when closeStreaming finalizes the card so the duplicate-final check catches it.

* test(feishu): cover idle streaming final dedupe

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
ToToKr
2026-04-25 15:18:10 +09:00
committed by GitHub
parent 36eae5a2c7
commit 2aa313cd90
2 changed files with 27 additions and 0 deletions

View File

@@ -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" });

View File

@@ -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;