mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
fix(feishu): accept v2 card action callbacks
This commit is contained in:
@@ -62,6 +62,9 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Feishu: accept Schema 2.0 card action callbacks that report
|
||||
`context.open_chat_id` instead of legacy `context.chat_id`, so button
|
||||
callbacks no longer drop as malformed. Fixes #71670. Thanks @eddy1068.
|
||||
- QQ Bot: make `qqbot_remind` schedule, list, and remove Gateway cron jobs
|
||||
directly for owner-authorized senders instead of returning `cronParams` and
|
||||
relying on a follow-up generic `cron` tool call. Fixes #70865. (#70937)
|
||||
|
||||
@@ -14,8 +14,8 @@ import { sendCardFeishu, sendMessageFeishu } from "./send.js";
|
||||
export type FeishuCardActionEvent = {
|
||||
operator: {
|
||||
open_id: string;
|
||||
user_id: string;
|
||||
union_id: string;
|
||||
user_id?: string;
|
||||
union_id?: string;
|
||||
};
|
||||
token: string;
|
||||
action: {
|
||||
@@ -23,9 +23,9 @@ export type FeishuCardActionEvent = {
|
||||
tag: string;
|
||||
};
|
||||
context: {
|
||||
open_id: string;
|
||||
user_id: string;
|
||||
chat_id: string;
|
||||
open_id?: string;
|
||||
user_id?: string;
|
||||
chat_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -183,43 +183,44 @@ function parseFeishuBotRemovedChatId(value: unknown): string | null {
|
||||
return readString(value.chat_id) ?? null;
|
||||
}
|
||||
|
||||
function firstString(...values: unknown[]): string | undefined {
|
||||
for (const value of values) {
|
||||
const stringValue = readString(value);
|
||||
const trimmed = stringValue?.trim();
|
||||
if (trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseFeishuCardActionEventPayload(value: unknown): FeishuCardActionEvent | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
const operator = value.operator;
|
||||
const operator = isRecord(value.operator) ? value.operator : {};
|
||||
const action = value.action;
|
||||
const context = value.context;
|
||||
if (!isRecord(operator) || !isRecord(action) || !isRecord(context)) {
|
||||
const context = isRecord(value.context) ? value.context : {};
|
||||
if (!isRecord(action)) {
|
||||
return null;
|
||||
}
|
||||
const token = readString(value.token);
|
||||
const openId = readString(operator.open_id);
|
||||
const userId = readString(operator.user_id);
|
||||
const unionId = readString(operator.union_id);
|
||||
const openId = firstString(operator.open_id, value.open_id, context.open_id);
|
||||
const userId = firstString(operator.user_id, value.user_id, context.user_id);
|
||||
const unionId = firstString(operator.union_id);
|
||||
const tag = readString(action.tag);
|
||||
const actionValue = action.value;
|
||||
const contextOpenId = readString(context.open_id);
|
||||
const contextUserId = readString(context.user_id);
|
||||
const chatId = readString(context.chat_id);
|
||||
if (
|
||||
!token ||
|
||||
!openId ||
|
||||
!userId ||
|
||||
!unionId ||
|
||||
!tag ||
|
||||
!isRecord(actionValue) ||
|
||||
!contextOpenId ||
|
||||
!contextUserId ||
|
||||
!chatId
|
||||
) {
|
||||
const contextOpenId = firstString(context.open_id, openId);
|
||||
const contextUserId = firstString(context.user_id, userId);
|
||||
const chatId = firstString(context.chat_id, context.open_chat_id);
|
||||
if (!token || !openId || !tag || !isRecord(actionValue)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
operator: {
|
||||
open_id: openId,
|
||||
user_id: userId,
|
||||
union_id: unionId,
|
||||
...(userId ? { user_id: userId } : {}),
|
||||
...(unionId ? { union_id: unionId } : {}),
|
||||
},
|
||||
token,
|
||||
action: {
|
||||
@@ -227,9 +228,9 @@ function parseFeishuCardActionEventPayload(value: unknown): FeishuCardActionEven
|
||||
tag,
|
||||
},
|
||||
context: {
|
||||
open_id: contextOpenId,
|
||||
user_id: contextUserId,
|
||||
chat_id: chatId,
|
||||
...(contextOpenId ? { open_id: contextOpenId } : {}),
|
||||
...(contextUserId ? { user_id: contextUserId } : {}),
|
||||
...(chatId ? { chat_id: chatId } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -198,6 +198,74 @@ describe("Feishu card-action lifecycle", () => {
|
||||
expect(sendCardFeishuMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes v2 callbacks that report open_chat_id instead of chat_id", async () => {
|
||||
const onCardAction = await setupLifecycleMonitor();
|
||||
const chatId = "oc_group_v2";
|
||||
|
||||
await onCardAction({
|
||||
operator: {
|
||||
open_id: "ou_user1",
|
||||
},
|
||||
token: "tok-card-v2-context",
|
||||
action: {
|
||||
tag: "button",
|
||||
value: createFeishuCardInteractionEnvelope({
|
||||
k: "quick",
|
||||
a: "feishu.quick_actions.help",
|
||||
q: "/help",
|
||||
c: {
|
||||
u: "ou_user1",
|
||||
h: chatId,
|
||||
t: "group",
|
||||
e: Date.now() + 60_000,
|
||||
},
|
||||
}),
|
||||
},
|
||||
context: {
|
||||
open_message_id: "om_card_v2",
|
||||
open_chat_id: chatId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(lastRuntime?.error).not.toHaveBeenCalled();
|
||||
expect(dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
|
||||
expect(createFeishuReplyDispatcherMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
accountId: "acct-card",
|
||||
chatId,
|
||||
replyToMessageId: "card-action-tok-card-v2-context",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("routes SDK-style card callbacks without context as direct callbacks", async () => {
|
||||
const onCardAction = await setupLifecycleMonitor();
|
||||
|
||||
await onCardAction({
|
||||
open_id: "ou_user1",
|
||||
user_id: "user_1",
|
||||
tenant_key: "tenant_1",
|
||||
open_message_id: "om_sdk_card",
|
||||
token: "tok-card-sdk-flat",
|
||||
action: {
|
||||
tag: "button",
|
||||
value: {
|
||||
command: "/help",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(lastRuntime?.error).not.toHaveBeenCalled();
|
||||
expect(dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
|
||||
expect(createFeishuReplyDispatcherMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
accountId: "acct-card",
|
||||
chatId: "ou_user1",
|
||||
replyToMessageId: "card-action-tok-card-sdk-flat",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not duplicate delivery when retrying after a post-send failure", async () => {
|
||||
const onCardAction = await setupLifecycleMonitor();
|
||||
const event = createCardActionEvent({
|
||||
|
||||
Reference in New Issue
Block a user