diff --git a/CHANGELOG.md b/CHANGELOG.md index 4da388df8ec..30068bc5419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/extensions/feishu/src/card-action.ts b/extensions/feishu/src/card-action.ts index a47bbb150a6..cff6b89b9f4 100644 --- a/extensions/feishu/src/card-action.ts +++ b/extensions/feishu/src/card-action.ts @@ -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; }; }; diff --git a/extensions/feishu/src/monitor.account.ts b/extensions/feishu/src/monitor.account.ts index 10254a2511f..f08645f7a9d 100644 --- a/extensions/feishu/src/monitor.account.ts +++ b/extensions/feishu/src/monitor.account.ts @@ -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 } : {}), }, }; } diff --git a/extensions/feishu/src/monitor.card-action.lifecycle.test-support.ts b/extensions/feishu/src/monitor.card-action.lifecycle.test-support.ts index b7102bd2447..4d14f81bd45 100644 --- a/extensions/feishu/src/monitor.card-action.lifecycle.test-support.ts +++ b/extensions/feishu/src/monitor.card-action.lifecycle.test-support.ts @@ -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({