fix(feishu): handle card.action.trigger callbacks (openclaw#17863)

Co-authored-by: Kai <clawborn@users.noreply.github.com>
This commit is contained in:
Clawborn
2026-02-28 13:24:11 +08:00
committed by GitHub
parent 60bf56517f
commit 49cf2bceb6
3 changed files with 162 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
import { describe, it, expect, vi } from "vitest";
import { handleFeishuCardAction, type FeishuCardActionEvent } from "./card-action.js";
// Mock resolveFeishuAccount
vi.mock("./accounts.js", () => ({
resolveFeishuAccount: vi.fn().mockReturnValue({ accountId: "mock-account" }),
}));
// Mock bot.js to verify handleFeishuMessage call
vi.mock("./bot.js", () => ({
handleFeishuMessage: vi.fn(),
}));
import { handleFeishuMessage } from "./bot.js";
describe("Feishu Card Action Handler", () => {
const cfg = {} as any; // Minimal mock
const runtime = { log: vi.fn(), error: vi.fn() } as any;
it("handles card action with text payload", async () => {
const event: FeishuCardActionEvent = {
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
token: "tok1",
action: { value: { text: "/ping" }, tag: "button" },
context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
};
await handleFeishuCardAction({ cfg, event, runtime });
expect(handleFeishuMessage).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
message: expect.objectContaining({
content: '{"text":"/ping"}',
chat_id: "chat1",
}),
}),
}),
);
});
it("handles card action with JSON object payload", async () => {
const event: FeishuCardActionEvent = {
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
token: "tok2",
action: { value: { key: "val" }, tag: "button" },
context: { open_id: "u123", user_id: "uid1", chat_id: "" },
};
await handleFeishuCardAction({ cfg, event, runtime });
expect(handleFeishuMessage).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
message: expect.objectContaining({
content: '{"text":"{\\"key\\":\\"val\\"}"}',
chat_id: "u123", // Fallback to open_id
}),
}),
}),
);
});
});

View File

@@ -0,0 +1,77 @@
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
import { resolveFeishuAccount } from "./accounts.js";
import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
export type FeishuCardActionEvent = {
operator: {
open_id: string;
user_id: string;
union_id: string;
};
token: string;
action: {
value: Record<string, unknown>;
tag: string;
};
context: {
open_id: string;
user_id: string;
chat_id: string;
};
};
export async function handleFeishuCardAction(params: {
cfg: ClawdbotConfig;
event: FeishuCardActionEvent;
botOpenId?: string;
runtime?: RuntimeEnv;
accountId?: string;
}): Promise<void> {
const { cfg, event, runtime, accountId } = params;
const account = resolveFeishuAccount({ cfg, accountId });
const log = runtime?.log ?? console.log;
// Extract action value
const actionValue = event.action.value;
let content = "";
if (typeof actionValue === "object" && actionValue !== null) {
if ("text" in actionValue && typeof actionValue.text === "string") {
content = actionValue.text;
} else if ("command" in actionValue && typeof actionValue.command === "string") {
content = actionValue.command;
} else {
content = JSON.stringify(actionValue);
}
} else {
content = String(actionValue);
}
// Construct a synthetic message event
const messageEvent: FeishuMessageEvent = {
sender: {
sender_id: {
open_id: event.operator.open_id,
user_id: event.operator.user_id,
union_id: event.operator.union_id,
},
},
message: {
message_id: `card-action-${event.token}`,
chat_id: event.context.chat_id || event.operator.open_id,
chat_type: event.context.chat_id ? "group" : "p2p",
message_type: "text",
content: JSON.stringify({ text: content }),
},
};
log(`feishu[${account.accountId}]: handling card action from ${event.operator.open_id}: ${content}`);
// Dispatch as normal message
await handleFeishuMessage({
cfg,
event: messageEvent,
botOpenId: params.botOpenId,
runtime,
accountId,
});
}

View File

@@ -9,6 +9,7 @@ import {
} from "openclaw/plugin-sdk";
import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
import { handleFeishuMessage, type FeishuMessageEvent, type FeishuBotAddedEvent } from "./bot.js";
import { handleFeishuCardAction, type FeishuCardActionEvent } from "./card-action.js";
import { createFeishuWSClient, createEventDispatcher } from "./client.js";
import { probeFeishu } from "./probe.js";
import { getMessageFeishu } from "./send.js";
@@ -350,6 +351,27 @@ function registerEventHandlers(
"im.message.reaction.deleted_v1": async () => {
// Ignore reaction removals
},
"card.action.trigger": async (data) => {
try {
const event = data as unknown as FeishuCardActionEvent;
const promise = handleFeishuCardAction({
cfg,
event,
botOpenId: botOpenIds.get(accountId),
runtime,
accountId,
});
if (fireAndForget) {
promise.catch((err) => {
error(`feishu[${accountId}]: error handling card action: ${String(err)}`);
});
} else {
await promise;
}
} catch (err) {
error(`feishu[${accountId}]: error handling card action: ${String(err)}`);
}
},
});
}