From 78b10170834b60e5f1f08d341d100035113fdf3f Mon Sep 17 00:00:00 2001 From: "zhangwei.justin" Date: Mon, 11 May 2026 17:32:47 +0800 Subject: [PATCH 1/2] fix: cap Feishu card elements Prevent Feishu card creation failures when long agent runs produce more tool-step elements than the platform accepts. Co-Authored-By: Claude Opus 4.7 --- .../feishu/messaging/message-renderer.ts | 16 ++++++++-- .../feishu/messaging/message-renderer.test.ts | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 tests/community/feishu/messaging/message-renderer.test.ts diff --git a/src/community/feishu/messaging/message-renderer.ts b/src/community/feishu/messaging/message-renderer.ts index 9761a1b..b799a03 100644 --- a/src/community/feishu/messaging/message-renderer.ts +++ b/src/community/feishu/messaging/message-renderer.ts @@ -23,6 +23,9 @@ import type { MarkdownElement, } from "./types"; +const MAX_CARD_ELEMENTS = 20; +const MAX_PANEL_ELEMENTS = 20; + /** * Render assistant message content as a Feishu interactive card. * @param messageContent - Array of content blocks (thinking, tool_use, text). @@ -107,10 +110,16 @@ export async function renderMessageCard( } } - const stepCount = stepPanel.elements.length; - if (stepCount > 0) { + const totalStepCount = stepPanel.elements.length; + if (totalStepCount > MAX_PANEL_ELEMENTS) { + stepPanel.elements = stepPanel.elements.slice( + totalStepCount - MAX_PANEL_ELEMENTS, + ); + } + + if (totalStepCount > 0) { const stepCountText = - stepCount + " " + (stepCount === 1 ? "step" : "steps"); + totalStepCount + " " + (totalStepCount === 1 ? "step" : "steps"); if (streaming) { stepPanel.header.title.content = `Working on it (${stepCountText})`; card.config!.summary.content = `Working on it (${stepCountText})`; @@ -142,6 +151,7 @@ export async function renderMessageCard( }, }); } + card.body.elements = card.body.elements.slice(-MAX_CARD_ELEMENTS); return card; } diff --git a/tests/community/feishu/messaging/message-renderer.test.ts b/tests/community/feishu/messaging/message-renderer.test.ts new file mode 100644 index 0000000..d35a7f8 --- /dev/null +++ b/tests/community/feishu/messaging/message-renderer.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "bun:test"; + +import { renderMessageCard } from "@/community/feishu/messaging/message-renderer"; + +describe("renderMessageCard", () => { + test("keeps final cards within Feishu's element limit", async () => { + const card = await renderMessageCard( + [ + ...Array.from({ length: 50 }, (_, i) => ({ + type: "tool_use" as const, + id: `tool-${i}`, + name: "Bash", + input: { command: `echo ${i}` }, + })), + { type: "text", text: "done" } as const, + ], + { + streaming: false, + uploadImage: async () => "image-key", + }, + ); + + const stepPanel = card.body.elements.find( + (element) => element.tag === "collapsible_panel", + ); + + expect(card.body.elements.length).toBeLessThanOrEqual(20); + expect(stepPanel?.tag).toBe("collapsible_panel"); + expect(stepPanel?.elements.length).toBeLessThanOrEqual(20); + }); +}); From c26be20fb87019dd85f5a8a8d46a548f62caa7e8 Mon Sep 17 00:00:00 2001 From: "zhangwei.justin" Date: Mon, 11 May 2026 18:55:01 +0800 Subject: [PATCH 2/2] fix: align Feishu card limit with official cap Trim old step entries by total card element count so rendered cards stay under Feishu's documented 200-element limit while preserving as much context as possible. Co-Authored-By: Claude Opus 4.7 --- .../feishu/messaging/message-renderer.ts | 48 +++++++++++++++---- .../feishu/messaging/message-renderer.test.ts | 25 ++++++++-- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/community/feishu/messaging/message-renderer.ts b/src/community/feishu/messaging/message-renderer.ts index b799a03..39a4c06 100644 --- a/src/community/feishu/messaging/message-renderer.ts +++ b/src/community/feishu/messaging/message-renderer.ts @@ -23,8 +23,8 @@ import type { MarkdownElement, } from "./types"; -const MAX_CARD_ELEMENTS = 20; -const MAX_PANEL_ELEMENTS = 20; +/** Maximum elements or components in one Feishu card. */ +const MAX_FEISHU_CARD_ELEMENTS = 200; /** * Render assistant message content as a Feishu interactive card. @@ -111,12 +111,6 @@ export async function renderMessageCard( } const totalStepCount = stepPanel.elements.length; - if (totalStepCount > MAX_PANEL_ELEMENTS) { - stepPanel.elements = stepPanel.elements.slice( - totalStepCount - MAX_PANEL_ELEMENTS, - ); - } - if (totalStepCount > 0) { const stepCountText = totalStepCount + " " + (totalStepCount === 1 ? "step" : "steps"); @@ -151,10 +145,46 @@ export async function renderMessageCard( }, }); } - card.body.elements = card.body.elements.slice(-MAX_CARD_ELEMENTS); + _trimCardElements(card); return card; } +/** Trim old step elements until the card fits Feishu's card limit. */ +function _trimCardElements(card: Card) { + let elementCount = _countElements(card); + const stepPanel = card.body.elements.find( + (element): element is CollapsiblePanel => element.tag === "collapsible_panel", + ); + if (!stepPanel || elementCount <= MAX_FEISHU_CARD_ELEMENTS) { + return; + } + + while ( + stepPanel.elements.length > 0 && + elementCount > MAX_FEISHU_CARD_ELEMENTS + ) { + const removedElement = stepPanel.elements.shift(); + elementCount -= _countElements(removedElement); + } +} + +function _countElements(value: unknown): number { + if (!value || typeof value !== "object") { + return 0; + } + + const item = value as Record; + const self = typeof item.tag === "string" ? 1 : 0; + return Object.values(item).reduce( + (count, child) => + count + + (Array.isArray(child) + ? child.reduce((sum, entry) => sum + _countElements(entry), 0) + : _countElements(child)), + self, + ); +} + async function _uploadMessageResource( text: string, { diff --git a/tests/community/feishu/messaging/message-renderer.test.ts b/tests/community/feishu/messaging/message-renderer.test.ts index d35a7f8..013b005 100644 --- a/tests/community/feishu/messaging/message-renderer.test.ts +++ b/tests/community/feishu/messaging/message-renderer.test.ts @@ -2,11 +2,28 @@ import { describe, expect, test } from "bun:test"; import { renderMessageCard } from "@/community/feishu/messaging/message-renderer"; +function countElements(value: unknown): number { + if (!value || typeof value !== "object") { + return 0; + } + + const item = value as Record; + const self = typeof item.tag === "string" ? 1 : 0; + return Object.values(item).reduce( + (count, child) => + count + + (Array.isArray(child) + ? child.reduce((sum, entry) => sum + countElements(entry), 0) + : countElements(child)), + self, + ); +} + describe("renderMessageCard", () => { - test("keeps final cards within Feishu's element limit", async () => { + test("keeps final cards within Feishu's official 200 element limit", async () => { const card = await renderMessageCard( [ - ...Array.from({ length: 50 }, (_, i) => ({ + ...Array.from({ length: 100 }, (_, i) => ({ type: "tool_use" as const, id: `tool-${i}`, name: "Bash", @@ -24,8 +41,8 @@ describe("renderMessageCard", () => { (element) => element.tag === "collapsible_panel", ); - expect(card.body.elements.length).toBeLessThanOrEqual(20); + expect(countElements(card)).toBeLessThanOrEqual(200); expect(stepPanel?.tag).toBe("collapsible_panel"); - expect(stepPanel?.elements.length).toBeLessThanOrEqual(20); + expect(stepPanel?.elements.length).toBe(65); }); });