Skip to content

Commit 49cf2bc

Browse files
ClawbornKai
andauthored
fix(feishu): handle card.action.trigger callbacks (#17863)
Co-authored-by: Kai <[email protected]>
1 parent 60bf565 commit 49cf2bc

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { handleFeishuCardAction, type FeishuCardActionEvent } from "./card-action.js";
3+
4+
// Mock resolveFeishuAccount
5+
vi.mock("./accounts.js", () => ({
6+
resolveFeishuAccount: vi.fn().mockReturnValue({ accountId: "mock-account" }),
7+
}));
8+
9+
// Mock bot.js to verify handleFeishuMessage call
10+
vi.mock("./bot.js", () => ({
11+
handleFeishuMessage: vi.fn(),
12+
}));
13+
14+
import { handleFeishuMessage } from "./bot.js";
15+
16+
describe("Feishu Card Action Handler", () => {
17+
const cfg = {} as any; // Minimal mock
18+
const runtime = { log: vi.fn(), error: vi.fn() } as any;
19+
20+
it("handles card action with text payload", async () => {
21+
const event: FeishuCardActionEvent = {
22+
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
23+
token: "tok1",
24+
action: { value: { text: "/ping" }, tag: "button" },
25+
context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
26+
};
27+
28+
await handleFeishuCardAction({ cfg, event, runtime });
29+
30+
expect(handleFeishuMessage).toHaveBeenCalledWith(
31+
expect.objectContaining({
32+
event: expect.objectContaining({
33+
message: expect.objectContaining({
34+
content: '{"text":"/ping"}',
35+
chat_id: "chat1",
36+
}),
37+
}),
38+
}),
39+
);
40+
});
41+
42+
it("handles card action with JSON object payload", async () => {
43+
const event: FeishuCardActionEvent = {
44+
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
45+
token: "tok2",
46+
action: { value: { key: "val" }, tag: "button" },
47+
context: { open_id: "u123", user_id: "uid1", chat_id: "" },
48+
};
49+
50+
await handleFeishuCardAction({ cfg, event, runtime });
51+
52+
expect(handleFeishuMessage).toHaveBeenCalledWith(
53+
expect.objectContaining({
54+
event: expect.objectContaining({
55+
message: expect.objectContaining({
56+
content: '{"text":"{\\"key\\":\\"val\\"}"}',
57+
chat_id: "u123", // Fallback to open_id
58+
}),
59+
}),
60+
}),
61+
);
62+
});
63+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2+
import { resolveFeishuAccount } from "./accounts.js";
3+
import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
4+
5+
export type FeishuCardActionEvent = {
6+
operator: {
7+
open_id: string;
8+
user_id: string;
9+
union_id: string;
10+
};
11+
token: string;
12+
action: {
13+
value: Record<string, unknown>;
14+
tag: string;
15+
};
16+
context: {
17+
open_id: string;
18+
user_id: string;
19+
chat_id: string;
20+
};
21+
};
22+
23+
export async function handleFeishuCardAction(params: {
24+
cfg: ClawdbotConfig;
25+
event: FeishuCardActionEvent;
26+
botOpenId?: string;
27+
runtime?: RuntimeEnv;
28+
accountId?: string;
29+
}): Promise<void> {
30+
const { cfg, event, runtime, accountId } = params;
31+
const account = resolveFeishuAccount({ cfg, accountId });
32+
const log = runtime?.log ?? console.log;
33+
34+
// Extract action value
35+
const actionValue = event.action.value;
36+
let content = "";
37+
if (typeof actionValue === "object" && actionValue !== null) {
38+
if ("text" in actionValue && typeof actionValue.text === "string") {
39+
content = actionValue.text;
40+
} else if ("command" in actionValue && typeof actionValue.command === "string") {
41+
content = actionValue.command;
42+
} else {
43+
content = JSON.stringify(actionValue);
44+
}
45+
} else {
46+
content = String(actionValue);
47+
}
48+
49+
// Construct a synthetic message event
50+
const messageEvent: FeishuMessageEvent = {
51+
sender: {
52+
sender_id: {
53+
open_id: event.operator.open_id,
54+
user_id: event.operator.user_id,
55+
union_id: event.operator.union_id,
56+
},
57+
},
58+
message: {
59+
message_id: `card-action-${event.token}`,
60+
chat_id: event.context.chat_id || event.operator.open_id,
61+
chat_type: event.context.chat_id ? "group" : "p2p",
62+
message_type: "text",
63+
content: JSON.stringify({ text: content }),
64+
},
65+
};
66+
67+
log(`feishu[${account.accountId}]: handling card action from ${event.operator.open_id}: ${content}`);
68+
69+
// Dispatch as normal message
70+
await handleFeishuMessage({
71+
cfg,
72+
event: messageEvent,
73+
botOpenId: params.botOpenId,
74+
runtime,
75+
accountId,
76+
});
77+
}

extensions/feishu/src/monitor.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "openclaw/plugin-sdk";
1010
import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
1111
import { handleFeishuMessage, type FeishuMessageEvent, type FeishuBotAddedEvent } from "./bot.js";
12+
import { handleFeishuCardAction, type FeishuCardActionEvent } from "./card-action.js";
1213
import { createFeishuWSClient, createEventDispatcher } from "./client.js";
1314
import { probeFeishu } from "./probe.js";
1415
import { getMessageFeishu } from "./send.js";
@@ -350,6 +351,27 @@ function registerEventHandlers(
350351
"im.message.reaction.deleted_v1": async () => {
351352
// Ignore reaction removals
352353
},
354+
"card.action.trigger": async (data) => {
355+
try {
356+
const event = data as unknown as FeishuCardActionEvent;
357+
const promise = handleFeishuCardAction({
358+
cfg,
359+
event,
360+
botOpenId: botOpenIds.get(accountId),
361+
runtime,
362+
accountId,
363+
});
364+
if (fireAndForget) {
365+
promise.catch((err) => {
366+
error(`feishu[${accountId}]: error handling card action: ${String(err)}`);
367+
});
368+
} else {
369+
await promise;
370+
}
371+
} catch (err) {
372+
error(`feishu[${accountId}]: error handling card action: ${String(err)}`);
373+
}
374+
},
353375
});
354376
}
355377

0 commit comments

Comments
 (0)