fix(feishu): suppress reasoning/thinking block payloads from delivery (#31723)

* fix(extensions/feishu/src/reply-dispatcher.ts): missing privacy check / data leak

Pattern from PR #24969

The fix addresses the critical race condition by placing the 'block' filter check at the very top of the `deliver` function. This ensures that for internal 'block' reasoning chunks, the function returns immediately, preventing any text processing (lines 195-203) and, crucially, preventing the initialization of the streaming state for these payloads (lines 212-216). This ensures that the `streaming` object is not initialized with empty data, and subsequent 'final' payloads will correctly initialize and stream only the final content. The fix also addresses the 'incomplete' validation issue by using `info?.kind !== 'block'`. While the contract likely ensures `info` is present, this defensive approach ensures that if `info` is missing (and the payload is unrelated to internal blocking), the message is still delivered to the user, preventing a 'silent failure' bug. The validation logic at line 205 (`!hasText && !hasMedia`) ensures we do not send empty messages.

* Fix indentation: remove extra 4 spaces from deliver function body

The deliver function is inside the createReplyDispatcherWithTyping call,
so it should be indented at 2 levels (8 spaces), not 3 levels (12 spaces).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(feishu): cover block payload suppression in reply dispatcher

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Maple778
2026-03-02 17:15:45 -05:00
committed by GitHub
parent bd4a082b73
commit 477de545f9
2 changed files with 24 additions and 1 deletions

View File

@@ -185,6 +185,23 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
});
it("suppresses internal block payload delivery", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.deliver({ text: "internal reasoning chunk" }, { kind: "block" });
expect(streamingInstances).toHaveLength(0);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
expect(sendMediaFeishuMock).not.toHaveBeenCalled();
});
it("uses streaming session for auto mode markdown payloads", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,

View File

@@ -192,6 +192,12 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
void typingCallbacks.onReplyStart?.();
},
deliver: async (payload: ReplyPayload, info) => {
// FIX: Filter out internal 'block' reasoning chunks immediately to prevent
// data leak and race conditions with streaming state initialization.
if (info?.kind === "block") {
return;
}
const text = payload.text ?? "";
const mediaList =
payload.mediaUrls && payload.mediaUrls.length > 0
@@ -209,7 +215,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
if (hasText) {
const useCard = renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
if ((info?.kind === "block" || info?.kind === "final") && streamingEnabled && useCard) {
if (info?.kind === "final" && streamingEnabled && useCard) {
startStreaming();
if (streamingStartPromise) {
await streamingStartPromise;