mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:30:44 +00:00
fix(feishu): suppress late streaming card finals
This commit is contained in:
@@ -399,6 +399,46 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
||||
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips distinct late final text after streaming card close", async () => {
|
||||
resolveFeishuAccountMock.mockReturnValue({
|
||||
accountId: "main",
|
||||
appId: "app_id",
|
||||
appSecret: "app_secret",
|
||||
domain: "feishu",
|
||||
config: {
|
||||
renderMode: "card",
|
||||
streaming: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { options } = createDispatcherHarness({
|
||||
runtime: createRuntimeLogger(),
|
||||
});
|
||||
|
||||
await options.deliver({ text: "First complete answer" }, { kind: "final" });
|
||||
await options.onIdle?.();
|
||||
await options.deliver(
|
||||
{ text: "Late tool-result final", mediaUrl: "https://example.com/a.png" },
|
||||
{ kind: "final" },
|
||||
);
|
||||
await options.onIdle?.();
|
||||
|
||||
expect(streamingInstances).toHaveLength(1);
|
||||
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
||||
expect(streamingInstances[0].close).toHaveBeenCalledWith("First complete answer", {
|
||||
note: "Agent: agent",
|
||||
});
|
||||
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
||||
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
||||
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
|
||||
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mediaUrl: "https://example.com/a.png",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses duplicate final text while still sending media", async () => {
|
||||
const options = setupNonStreamingAutoDispatcher();
|
||||
await options.deliver({ text: "plain final" }, { kind: "final" });
|
||||
|
||||
@@ -228,6 +228,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
const deliveredFinalTexts = new Set<string>();
|
||||
let partialUpdateQueue: Promise<void> = Promise.resolve();
|
||||
let streamingStartPromise: Promise<void> | null = null;
|
||||
let streamingClosedForReply = false;
|
||||
type StreamTextUpdateMode = "snapshot" | "delta";
|
||||
|
||||
const formatReasoningPrefix = (thinking: string): string => {
|
||||
@@ -378,6 +379,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
// the streaming card.
|
||||
if (streamText) {
|
||||
deliveredFinalTexts.add(streamText);
|
||||
streamingClosedForReply = true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -451,6 +453,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
||||
onReplyStart: async () => {
|
||||
deliveredFinalTexts.clear();
|
||||
streamingClosedForReply = false;
|
||||
if (streamingEnabled && renderMode === "card") {
|
||||
startStreaming();
|
||||
}
|
||||
@@ -461,17 +464,24 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
const text = reply.text;
|
||||
const hasText = reply.hasText;
|
||||
const hasMedia = reply.hasMedia;
|
||||
const useCard =
|
||||
hasText && (renderMode === "card" || (renderMode === "auto" && shouldUseCard(text)));
|
||||
const skipTextForDuplicateFinal =
|
||||
info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
|
||||
const shouldDeliverText = hasText && !skipTextForDuplicateFinal;
|
||||
const skipTextForClosedStreamingFinal =
|
||||
info?.kind === "final" &&
|
||||
hasText &&
|
||||
streamingClosedForReply &&
|
||||
streamingEnabled &&
|
||||
useCard;
|
||||
const shouldDeliverText =
|
||||
hasText && !skipTextForDuplicateFinal && !skipTextForClosedStreamingFinal;
|
||||
|
||||
if (!shouldDeliverText && !hasMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldDeliverText) {
|
||||
const useCard = renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
|
||||
|
||||
if (info?.kind === "block") {
|
||||
// Drop internal block chunks unless we can safely consume them as
|
||||
// streaming-card fallback content.
|
||||
|
||||
Reference in New Issue
Block a user