diff --git a/CHANGELOG.md b/CHANGELOG.md index 14843551ba4..c288b9565c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -201,6 +201,7 @@ Docs: https://docs.openclaw.ai - Mattermost/slash commands: harden native slash-command callback token validation to use constant-time secret comparison, matching the existing interaction-token path. - Agents/scheduling: route delayed follow-up requests toward cron only when cron is actually available, while keeping background `exec`/`process` guidance scoped to work that starts now. (#60811) Thanks @vincentkoc. - Cron/security: reject unsafe custom `sessionTarget: "session:..."` IDs earlier during cron add, update, and execution so malformed custom session keys fail closed with clear errors. +- Feishu/cards: replace the legacy `wide_screen_mode` schema 1.x config with schema 2.0 `width_mode: "fill"` in interactive approval, launcher, markdown, and structured card builders so Feishu card sends stop failing with parse-card errors while preserving wide-card rendering. (#53395) Thanks @drvoss ## 2026.4.1 diff --git a/extensions/feishu/src/bot.card-action.test.ts b/extensions/feishu/src/bot.card-action.test.ts index 25658b309c9..5ee4f060ed8 100644 --- a/extensions/feishu/src/bot.card-action.test.ts +++ b/extensions/feishu/src/bot.card-action.test.ts @@ -195,6 +195,9 @@ describe("Feishu Card Action Handler", () => { to: "chat:chat1", accountId: "main", card: expect.objectContaining({ + config: expect.objectContaining({ + width_mode: "fill", + }), header: expect.objectContaining({ title: expect.objectContaining({ content: "Confirm action" }), }), @@ -220,6 +223,21 @@ describe("Feishu Card Action Handler", () => { }), }), ); + const firstSendArg = (sendCardFeishuMock.mock.calls as unknown[][]).at(0)?.[0] as + | { + card?: { + config?: { + width_mode?: string; + wide_screen_mode?: boolean; + enable_forward?: boolean; + }; + }; + } + | undefined; + const sentCard = firstSendArg?.card; + expect(sentCard).toBeDefined(); + expect(sentCard?.config?.wide_screen_mode).toBeUndefined(); + expect(sentCard?.config?.enable_forward).toBeUndefined(); expect(handleFeishuMessage).not.toHaveBeenCalled(); }); diff --git a/extensions/feishu/src/card-ux-approval.ts b/extensions/feishu/src/card-ux-approval.ts index 944ace931ea..17ec2ddc872 100644 --- a/extensions/feishu/src/card-ux-approval.ts +++ b/extensions/feishu/src/card-ux-approval.ts @@ -21,7 +21,7 @@ export function createApprovalCard(params: { return { schema: "2.0", config: { - wide_screen_mode: true, + width_mode: "fill", }, header: { title: { diff --git a/extensions/feishu/src/card-ux-launcher.test.ts b/extensions/feishu/src/card-ux-launcher.test.ts index 74fe72514f3..a8513a58f78 100644 --- a/extensions/feishu/src/card-ux-launcher.test.ts +++ b/extensions/feishu/src/card-ux-launcher.test.ts @@ -32,6 +32,11 @@ describe("feishu quick-action launcher", () => { expiresAt: 123, sessionKey: "agent:codex:feishu:chat:chat1", }) as { + config: { + width_mode?: string; + enable_forward?: boolean; + wide_screen_mode?: boolean; + }; body: { elements: Array<{ tag: string; @@ -40,6 +45,9 @@ describe("feishu quick-action launcher", () => { }; }; + expect(card.config.width_mode).toBe("fill"); + expect(card.config.enable_forward).toBeUndefined(); + expect(card.config.wide_screen_mode).toBeUndefined(); const actionBlock = card.body.elements.find((entry) => entry.tag === "action"); expect(actionBlock?.actions).toHaveLength(3); expect(actionBlock?.actions?.[0]?.value?.oc).toBe("ocf1"); @@ -64,6 +72,9 @@ describe("feishu quick-action launcher", () => { to: "user:u123", accountId: "main", card: expect.objectContaining({ + config: expect.objectContaining({ + width_mode: "fill", + }), body: expect.objectContaining({ elements: expect.arrayContaining([ expect.objectContaining({ @@ -83,6 +94,21 @@ describe("feishu quick-action launcher", () => { }), }), ); + const firstSendArg = (sendCardFeishuMock.mock.calls as unknown[][]).at(0)?.[0] as + | { + card?: { + config?: { + width_mode?: string; + wide_screen_mode?: boolean; + enable_forward?: boolean; + }; + }; + } + | undefined; + const sentCard = firstSendArg?.card; + expect(sentCard).toBeDefined(); + expect(sentCard?.config?.wide_screen_mode).toBeUndefined(); + expect(sentCard?.config?.enable_forward).toBeUndefined(); }); it("falls back to legacy menu handling when launcher send fails", async () => { diff --git a/extensions/feishu/src/card-ux-launcher.ts b/extensions/feishu/src/card-ux-launcher.ts index 91f05d8dfe6..e72cea82c05 100644 --- a/extensions/feishu/src/card-ux-launcher.ts +++ b/extensions/feishu/src/card-ux-launcher.ts @@ -23,7 +23,7 @@ export function createQuickActionLauncherCard(params: { return { schema: "2.0", config: { - wide_screen_mode: true, + width_mode: "fill", }, header: { title: { diff --git a/extensions/feishu/src/monitor.bot-menu.test.ts b/extensions/feishu/src/monitor.bot-menu.test.ts index b27ef9a12ff..07f538afbb1 100644 --- a/extensions/feishu/src/monitor.bot-menu.test.ts +++ b/extensions/feishu/src/monitor.bot-menu.test.ts @@ -143,6 +143,9 @@ describe("Feishu bot menu handler", () => { expect.objectContaining({ to: "user:ou_user1", card: expect.objectContaining({ + config: expect.objectContaining({ + width_mode: "fill", + }), header: expect.objectContaining({ title: expect.objectContaining({ content: "Quick actions" }), }), @@ -212,5 +215,20 @@ describe("Feishu bot menu handler", () => { }), ); }); + const firstSendArg = (sendCardFeishuMock.mock.calls as unknown[][]).at(0)?.[0] as + | { + card?: { + config?: { + width_mode?: string; + wide_screen_mode?: boolean; + enable_forward?: boolean; + }; + }; + } + | undefined; + const sentCard = firstSendArg?.card; + expect(sentCard).toBeDefined(); + expect(sentCard?.config?.wide_screen_mode).toBeUndefined(); + expect(sentCard?.config?.enable_forward).toBeUndefined(); }); }); diff --git a/extensions/feishu/src/send.test.ts b/extensions/feishu/src/send.test.ts index d8e4a7c54dd..6f7052c388f 100644 --- a/extensions/feishu/src/send.test.ts +++ b/extensions/feishu/src/send.test.ts @@ -1,5 +1,6 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; +import { buildMarkdownCard } from "./send.js"; const { mockConvertMarkdownTables, @@ -406,6 +407,20 @@ describe("resolveFeishuCardTemplate", () => { }); describe("buildStructuredCard", () => { + it("uses schema-2.0 width config instead of legacy wide screen mode", () => { + const card = buildStructuredCard("hello") as { + config: { + width_mode?: string; + enable_forward?: boolean; + wide_screen_mode?: boolean; + }; + }; + + expect(card.config.width_mode).toBe("fill"); + expect(card.config.enable_forward).toBeUndefined(); + expect(card.config.wide_screen_mode).toBeUndefined(); + }); + it("falls back to blue when the header template is unsupported", () => { const card = buildStructuredCard("hello", { header: { @@ -424,3 +439,19 @@ describe("buildStructuredCard", () => { ); }); }); + +describe("buildMarkdownCard", () => { + it("uses schema-2.0 width config instead of legacy wide screen mode", () => { + const card = buildMarkdownCard("hello") as { + config: { + width_mode?: string; + enable_forward?: boolean; + wide_screen_mode?: boolean; + }; + }; + + expect(card.config.width_mode).toBe("fill"); + expect(card.config.enable_forward).toBeUndefined(); + expect(card.config.wide_screen_mode).toBeUndefined(); + }); +}); diff --git a/extensions/feishu/src/send.ts b/extensions/feishu/src/send.ts index 3791fca739c..95b235b2d4e 100644 --- a/extensions/feishu/src/send.ts +++ b/extensions/feishu/src/send.ts @@ -587,7 +587,7 @@ export function buildMarkdownCard(text: string): Record { return { schema: "2.0", config: { - wide_screen_mode: true, + width_mode: "fill", }, body: { elements: [ @@ -634,7 +634,7 @@ export function buildStructuredCard( } const card: Record = { schema: "2.0", - config: { wide_screen_mode: true }, + config: { width_mode: "fill" }, body: { elements }, }; if (options?.header) {