Skip to content

Commit 2aa313c

Browse files
MoerAIvincentkoc
andauthored
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 <[email protected]>
1 parent 36eae5a commit 2aa313c

2 files changed

Lines changed: 27 additions & 0 deletions

File tree

extensions/feishu/src/reply-dispatcher.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,27 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
364364
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
365365
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
366366
});
367+
368+
it("skips final text already closed by idle streaming", async () => {
369+
const { result, options } = createDispatcherHarness({
370+
runtime: createRuntimeLogger(),
371+
});
372+
373+
await options.onReplyStart?.();
374+
result.replyOptions.onPartialReply?.({ text: "```md\nidle streamed reply\n```" });
375+
await options.onIdle?.();
376+
await options.deliver({ text: "```md\nidle streamed reply\n```" }, { kind: "final" });
377+
378+
expect(streamingInstances).toHaveLength(1);
379+
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
380+
expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\nidle streamed reply\n```", {
381+
note: "Agent: agent",
382+
});
383+
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
384+
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
385+
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
386+
});
387+
367388
it("suppresses duplicate final text while still sending media", async () => {
368389
const options = setupNonStreamingAutoDispatcher();
369390
await options.deliver({ text: "plain final" }, { kind: "final" });

extensions/feishu/src/reply-dispatcher.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,12 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
311311
}
312312
const finalNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
313313
await streaming.close(text, { note: finalNote });
314+
// Track the raw streamed text so the duplicate-final check in deliver()
315+
// can skip the redundant text delivery that arrives after onIdle closes
316+
// the streaming card.
317+
if (streamText) {
318+
deliveredFinalTexts.add(streamText);
319+
}
314320
}
315321
streaming = null;
316322
streamingStartPromise = null;

0 commit comments

Comments
 (0)