Skip to content

Commit 71fb093

Browse files
committed
fix(feishu): render native presentation buttons
1 parent 62b51a6 commit 71fb093

5 files changed

Lines changed: 329 additions & 167 deletions

File tree

extensions/feishu/src/channel.test.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ describe("feishuPlugin actions", () => {
395395
expect(details.chatId).toBe("oc_group_1");
396396
});
397397

398-
it("renders presentation button labels into the card fallback", async () => {
398+
it("renders presentation buttons as native Feishu card actions", async () => {
399399
sendCardFeishuMock.mockResolvedValueOnce({ messageId: "om_card", chatId: "oc_group_1" });
400400

401401
await feishuPlugin.actions?.handleAction?.({
@@ -421,10 +421,56 @@ describe("feishuPlugin actions", () => {
421421
"send card args",
422422
);
423423
const card = requireRecord(sendCardArgs.card, "card");
424+
expect(requireRecord(card.body, "card body").elements).toEqual([
425+
{
426+
tag: "action",
427+
actions: [
428+
{
429+
tag: "button",
430+
text: { tag: "plain_text", content: "Run help" },
431+
type: "default",
432+
value: {
433+
oc: "ocf1",
434+
k: "quick",
435+
a: "feishu.payload.button",
436+
q: "feishu.quick_actions.help",
437+
},
438+
},
439+
],
440+
},
441+
]);
442+
});
443+
444+
it("does not duplicate title-only presentation cards in the body fallback", async () => {
445+
sendCardFeishuMock.mockResolvedValueOnce({ messageId: "om_card", chatId: "oc_group_1" });
446+
447+
await feishuPlugin.actions?.handleAction?.({
448+
action: "send",
449+
params: {
450+
to: "chat:oc_group_1",
451+
presentation: {
452+
title: "Status",
453+
blocks: [],
454+
},
455+
},
456+
cfg,
457+
accountId: undefined,
458+
toolContext: {},
459+
} as never);
460+
461+
const sendCardArgs = requireRecord(
462+
mockCallArg(sendCardFeishuMock, 0, 0, "sendCardFeishu"),
463+
"send card args",
464+
);
465+
const card = requireRecord(sendCardArgs.card, "card");
466+
expect(card.header).toEqual({
467+
title: { tag: "plain_text", content: "Status" },
468+
template: "blue",
469+
});
424470
expect(requireRecord(card.body, "card body").elements).toEqual([
425471
{
426472
tag: "markdown",
427-
content: "- Run help",
473+
content: "",
428474
},
429475
]);
430476
});

extensions/feishu/src/channel.ts

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ import {
2525
createChannelDirectoryAdapter,
2626
createRuntimeDirectoryLiveAdapter,
2727
} from "openclaw/plugin-sdk/directory-runtime";
28-
import {
29-
normalizeMessagePresentation,
30-
renderMessagePresentationFallbackText,
31-
} from "openclaw/plugin-sdk/interactive-runtime";
28+
import { normalizeMessagePresentation } from "openclaw/plugin-sdk/interactive-runtime";
3229
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
3330
import { createRuntimeOutboundDelegates } from "openclaw/plugin-sdk/outbound-runtime";
3431
import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
@@ -70,6 +67,7 @@ import {
7067
import { listFeishuDirectoryGroups, listFeishuDirectoryPeers } from "./directory.static.js";
7168
import { messageActionTargetAliases } from "./message-action-contract.js";
7269
import { resolveFeishuGroupToolPolicy } from "./policy.js";
70+
import { buildFeishuPresentationCard } from "./presentation-card.js";
7371
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
7472
import { collectFeishuSecurityAuditFindings } from "./security-audit.js";
7573
import { createFeishuSendReceipt } from "./send-result.js";
@@ -183,41 +181,6 @@ const feishuMessageAdapter = defineChannelMessageAdapter({
183181
},
184182
});
185183

186-
function buildFeishuPresentationCard(params: {
187-
presentation: NonNullable<ReturnType<typeof normalizeMessagePresentation>>;
188-
fallbackText?: string;
189-
}): Record<string, unknown> {
190-
const fallbackPresentation: NonNullable<ReturnType<typeof normalizeMessagePresentation>> = {
191-
...(params.presentation.tone ? { tone: params.presentation.tone } : {}),
192-
blocks: params.presentation.blocks,
193-
};
194-
return {
195-
schema: "2.0",
196-
config: {
197-
width_mode: "fill",
198-
},
199-
...(params.presentation.title
200-
? {
201-
header: {
202-
title: { tag: "plain_text", content: params.presentation.title },
203-
template: "blue",
204-
},
205-
}
206-
: {}),
207-
body: {
208-
elements: [
209-
{
210-
tag: "markdown",
211-
content: renderMessagePresentationFallbackText({
212-
text: params.fallbackText,
213-
presentation: fallbackPresentation,
214-
}),
215-
},
216-
],
217-
},
218-
};
219-
}
220-
221184
async function createFeishuActionClient(account: ResolvedFeishuAccount) {
222185
const { createFeishuClient } = await import("./client.js");
223186
return createFeishuClient(account);

extensions/feishu/src/outbound.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,43 @@ describe("feishuOutbound.sendPayload native cards", () => {
468468
expectFeishuResult(result, "native_card_msg");
469469
});
470470

471+
it("does not duplicate title-only presentation cards in outbound fallbacks", async () => {
472+
const presentation: MessagePresentation = {
473+
title: "Status",
474+
blocks: [],
475+
};
476+
const payload = { presentation };
477+
const rendered = await feishuOutbound.renderPresentation?.({
478+
payload,
479+
presentation,
480+
ctx: {
481+
cfg: emptyConfig,
482+
to: "chat_1",
483+
text: "",
484+
accountId: "main",
485+
payload,
486+
},
487+
});
488+
489+
if (!rendered) {
490+
throw new Error("expected Feishu presentation renderer to return a payload");
491+
}
492+
const renderedChannelData = rendered.channelData as
493+
| { feishu?: { card?: Record<string, any> } }
494+
| undefined;
495+
const renderedCard = renderedChannelData?.feishu?.card;
496+
expect(renderedCard?.header).toEqual({
497+
title: { tag: "plain_text", content: "Status" },
498+
template: "blue",
499+
});
500+
expect(renderedCard?.body?.elements).toEqual([
501+
{
502+
tag: "markdown",
503+
content: "",
504+
},
505+
]);
506+
});
507+
471508
it("sends interactive button payloads as native Feishu cards", async () => {
472509
const result = await feishuOutbound.sendPayload?.({
473510
cfg: emptyConfig,
@@ -581,6 +618,12 @@ describe("feishuOutbound.sendPayload native cards", () => {
581618
{
582619
tag: "action",
583620
actions: [
621+
{
622+
tag: "button",
623+
text: { tag: "plain_text", content: "Promote" },
624+
type: "success",
625+
url: "https://example.com/promote",
626+
},
584627
{
585628
tag: "button",
586629
text: { tag: "plain_text", content: "Bad link" },
@@ -608,6 +651,12 @@ describe("feishuOutbound.sendPayload native cards", () => {
608651
{
609652
tag: "action",
610653
actions: [
654+
{
655+
tag: "button",
656+
text: { tag: "plain_text", content: "Promote" },
657+
type: "primary",
658+
url: "https://example.com/promote",
659+
},
611660
{
612661
tag: "button",
613662
text: { tag: "plain_text", content: "Good link" },

0 commit comments

Comments
 (0)