Skip to content

Commit a1090b6

Browse files
committed
fix(feishu): accept v2 card action callbacks
1 parent 12c1657 commit a1090b6

4 files changed

Lines changed: 102 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ Docs: https://docs.openclaw.ai
6262

6363
### Fixes
6464

65+
- Feishu: accept Schema 2.0 card action callbacks that report
66+
`context.open_chat_id` instead of legacy `context.chat_id`, so button
67+
callbacks no longer drop as malformed. Fixes #71670. Thanks @eddy1068.
6568
- QQ Bot: make `qqbot_remind` schedule, list, and remove Gateway cron jobs
6669
directly for owner-authorized senders instead of returning `cronParams` and
6770
relying on a follow-up generic `cron` tool call. Fixes #70865. (#70937)

extensions/feishu/src/card-action.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ import { sendCardFeishu, sendMessageFeishu } from "./send.js";
1414
export type FeishuCardActionEvent = {
1515
operator: {
1616
open_id: string;
17-
user_id: string;
18-
union_id: string;
17+
user_id?: string;
18+
union_id?: string;
1919
};
2020
token: string;
2121
action: {
2222
value: Record<string, unknown>;
2323
tag: string;
2424
};
2525
context: {
26-
open_id: string;
27-
user_id: string;
28-
chat_id: string;
26+
open_id?: string;
27+
user_id?: string;
28+
chat_id?: string;
2929
};
3030
};
3131

extensions/feishu/src/monitor.account.ts

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -183,53 +183,54 @@ function parseFeishuBotRemovedChatId(value: unknown): string | null {
183183
return readString(value.chat_id) ?? null;
184184
}
185185

186+
function firstString(...values: unknown[]): string | undefined {
187+
for (const value of values) {
188+
const stringValue = readString(value);
189+
const trimmed = stringValue?.trim();
190+
if (trimmed) {
191+
return trimmed;
192+
}
193+
}
194+
return undefined;
195+
}
196+
186197
function parseFeishuCardActionEventPayload(value: unknown): FeishuCardActionEvent | null {
187198
if (!isRecord(value)) {
188199
return null;
189200
}
190-
const operator = value.operator;
201+
const operator = isRecord(value.operator) ? value.operator : {};
191202
const action = value.action;
192-
const context = value.context;
193-
if (!isRecord(operator) || !isRecord(action) || !isRecord(context)) {
203+
const context = isRecord(value.context) ? value.context : {};
204+
if (!isRecord(action)) {
194205
return null;
195206
}
196207
const token = readString(value.token);
197-
const openId = readString(operator.open_id);
198-
const userId = readString(operator.user_id);
199-
const unionId = readString(operator.union_id);
208+
const openId = firstString(operator.open_id, value.open_id, context.open_id);
209+
const userId = firstString(operator.user_id, value.user_id, context.user_id);
210+
const unionId = firstString(operator.union_id);
200211
const tag = readString(action.tag);
201212
const actionValue = action.value;
202-
const contextOpenId = readString(context.open_id);
203-
const contextUserId = readString(context.user_id);
204-
const chatId = readString(context.chat_id);
205-
if (
206-
!token ||
207-
!openId ||
208-
!userId ||
209-
!unionId ||
210-
!tag ||
211-
!isRecord(actionValue) ||
212-
!contextOpenId ||
213-
!contextUserId ||
214-
!chatId
215-
) {
213+
const contextOpenId = firstString(context.open_id, openId);
214+
const contextUserId = firstString(context.user_id, userId);
215+
const chatId = firstString(context.chat_id, context.open_chat_id);
216+
if (!token || !openId || !tag || !isRecord(actionValue)) {
216217
return null;
217218
}
218219
return {
219220
operator: {
220221
open_id: openId,
221-
user_id: userId,
222-
union_id: unionId,
222+
...(userId ? { user_id: userId } : {}),
223+
...(unionId ? { union_id: unionId } : {}),
223224
},
224225
token,
225226
action: {
226227
value: actionValue,
227228
tag,
228229
},
229230
context: {
230-
open_id: contextOpenId,
231-
user_id: contextUserId,
232-
chat_id: chatId,
231+
...(contextOpenId ? { open_id: contextOpenId } : {}),
232+
...(contextUserId ? { user_id: contextUserId } : {}),
233+
...(chatId ? { chat_id: chatId } : {}),
233234
},
234235
};
235236
}

extensions/feishu/src/monitor.card-action.lifecycle.test-support.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,74 @@ describe("Feishu card-action lifecycle", () => {
198198
expect(sendCardFeishuMock).not.toHaveBeenCalled();
199199
});
200200

201+
it("routes v2 callbacks that report open_chat_id instead of chat_id", async () => {
202+
const onCardAction = await setupLifecycleMonitor();
203+
const chatId = "oc_group_v2";
204+
205+
await onCardAction({
206+
operator: {
207+
open_id: "ou_user1",
208+
},
209+
token: "tok-card-v2-context",
210+
action: {
211+
tag: "button",
212+
value: createFeishuCardInteractionEnvelope({
213+
k: "quick",
214+
a: "feishu.quick_actions.help",
215+
q: "/help",
216+
c: {
217+
u: "ou_user1",
218+
h: chatId,
219+
t: "group",
220+
e: Date.now() + 60_000,
221+
},
222+
}),
223+
},
224+
context: {
225+
open_message_id: "om_card_v2",
226+
open_chat_id: chatId,
227+
},
228+
});
229+
230+
expect(lastRuntime?.error).not.toHaveBeenCalled();
231+
expect(dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
232+
expect(createFeishuReplyDispatcherMock).toHaveBeenCalledWith(
233+
expect.objectContaining({
234+
accountId: "acct-card",
235+
chatId,
236+
replyToMessageId: "card-action-tok-card-v2-context",
237+
}),
238+
);
239+
});
240+
241+
it("routes SDK-style card callbacks without context as direct callbacks", async () => {
242+
const onCardAction = await setupLifecycleMonitor();
243+
244+
await onCardAction({
245+
open_id: "ou_user1",
246+
user_id: "user_1",
247+
tenant_key: "tenant_1",
248+
open_message_id: "om_sdk_card",
249+
token: "tok-card-sdk-flat",
250+
action: {
251+
tag: "button",
252+
value: {
253+
command: "/help",
254+
},
255+
},
256+
});
257+
258+
expect(lastRuntime?.error).not.toHaveBeenCalled();
259+
expect(dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
260+
expect(createFeishuReplyDispatcherMock).toHaveBeenCalledWith(
261+
expect.objectContaining({
262+
accountId: "acct-card",
263+
chatId: "ou_user1",
264+
replyToMessageId: "card-action-tok-card-sdk-flat",
265+
}),
266+
);
267+
});
268+
201269
it("does not duplicate delivery when retrying after a post-send failure", async () => {
202270
const onCardAction = await setupLifecycleMonitor();
203271
const event = createCardActionEvent({

0 commit comments

Comments
 (0)