Skip to content

Commit bf6f954

Browse files
committed
fix(feishu): support text-tag fallback card parsing
1 parent 0fb8582 commit bf6f954

3 files changed

Lines changed: 55 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
3535
- Channels/Telegram: persist native command metadata on target sessions so topic, helper, and ACP-bound slash commands keep their session metadata attached to the routed conversation. (#57548) Thanks @GaosCode.
3636
- Channels/native commands: keep validated native slash command replies visible in group chats while preserving explicit owner allowlists for command authorization. (#73672) Thanks @obviyus.
3737
- Auto-reply/session: carry the tail of user/assistant turns into the freshly-rotated transcript on silent in-reply session resets (compaction failure, role-ordering conflict) so direct-chat continuity survives the rebind. Fixes #70853. (#70898) Thanks @neeravmakwana.
38+
- Feishu: extract text-tag and nested-array text from replied interactive cards, and fall back to direct post-format card content when element arrays are empty. Fixes #60380; supersedes #60383; carries forward #73203. Thanks @taozengabc and @lskun.
3839

3940
## 2026.4.27
4041

extensions/feishu/src/send.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,14 @@ describe("getMessageFeishu", () => {
180180
body: {
181181
content: JSON.stringify({
182182
elements: [
183-
[{ tag: "text", text: "Service Name" }, { tag: "text", text: "eip-admin-api" }],
184-
[{ tag: "text", text: "Alert" }, { tag: "text", text: "Health check failed 3 times" }],
183+
[
184+
{ tag: "text", text: "Service Name" },
185+
{ tag: "text", text: "eip-admin-api" },
186+
],
187+
[
188+
{ tag: "text", text: "Alert" },
189+
{ tag: "text", text: "Health check failed 3 times" },
190+
],
185191
],
186192
}),
187193
},
@@ -331,6 +337,42 @@ describe("getMessageFeishu", () => {
331337
);
332338
});
333339

340+
it("falls back to direct post-format content when interactive card elements are empty", async () => {
341+
mockClientGet.mockResolvedValueOnce({
342+
code: 0,
343+
data: {
344+
items: [
345+
{
346+
message_id: "om_direct_post_card",
347+
chat_id: "oc_direct_post_card",
348+
msg_type: "interactive",
349+
body: {
350+
content: JSON.stringify({
351+
elements: [],
352+
title: "Direct card summary",
353+
content: [[{ tag: "text", text: "fallback body" }]],
354+
}),
355+
},
356+
},
357+
],
358+
},
359+
});
360+
361+
const result = await getMessageFeishu({
362+
cfg: {} as ClawdbotConfig,
363+
messageId: "om_direct_post_card",
364+
});
365+
366+
expect(result).toEqual(
367+
expect.objectContaining({
368+
messageId: "om_direct_post_card",
369+
chatId: "oc_direct_post_card",
370+
contentType: "interactive",
371+
content: "Direct card summary\n\nfallback body",
372+
}),
373+
);
374+
});
375+
334376
it("extracts text content from post messages", async () => {
335377
mockClientGet.mockResolvedValueOnce({
336378
code: 0,

extensions/feishu/src/send.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -252,23 +252,25 @@ function extractInteractiveElementsText(
252252
variables: Map<string, string>,
253253
): string {
254254
const texts: string[] = [];
255-
for (const element of elements) {
256-
// Some cards use nested arrays (e.g. [[row1_col1, row1_col2], [row2_col1, ...]])
257-
// for table-like layouts. Flatten one level and extract text from each cell.
255+
256+
function collectText(element: unknown): void {
258257
if (Array.isArray(element)) {
259258
for (const cell of element) {
260-
const text = extractInteractiveElementText(cell, variables);
261-
if (text !== undefined) {
262-
texts.push(text);
263-
}
259+
collectText(cell);
264260
}
265-
continue;
261+
return;
266262
}
263+
267264
const text = extractInteractiveElementText(element, variables);
268265
if (text !== undefined) {
269266
texts.push(text);
270267
}
271268
}
269+
270+
for (const element of elements) {
271+
collectText(element);
272+
}
273+
272274
return texts.join("\n").trim();
273275
}
274276

0 commit comments

Comments
 (0)