diff --git a/extensions/feishu/src/bot.checkBotMentioned.test.ts b/extensions/feishu/src/bot.checkBotMentioned.test.ts index 2f390ba007a..0bc6cf69df0 100644 --- a/extensions/feishu/src/bot.checkBotMentioned.test.ts +++ b/extensions/feishu/src/bot.checkBotMentioned.test.ts @@ -61,4 +61,46 @@ describe("parseFeishuMessageEvent – mentionedBot", () => { const ctx = parseFeishuMessageEvent(event as any, ""); expect(ctx.mentionedBot).toBe(false); }); + + it("returns mentionedBot=true for post message with at (no top-level mentions)", () => { + const BOT_OPEN_ID = "ou_bot_123"; + const postContent = JSON.stringify({ + content: [ + [{ tag: "at", user_id: BOT_OPEN_ID, user_name: "claw" }], + [{ tag: "text", text: "What does this document say" }], + ], + }); + const event = { + sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } }, + message: { + message_id: "msg_1", + chat_id: "oc_chat1", + chat_type: "group", + message_type: "post", + content: postContent, + mentions: [], + }, + }; + const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID); + expect(ctx.mentionedBot).toBe(true); + }); + + it("returns mentionedBot=false for post message with no at", () => { + const postContent = JSON.stringify({ + content: [[{ tag: "text", text: "hello" }]], + }); + const event = { + sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } }, + message: { + message_id: "msg_1", + chat_id: "oc_chat1", + chat_type: "group", + message_type: "post", + content: postContent, + mentions: [], + }, + }; + const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123"); + expect(ctx.mentionedBot).toBe(false); + }); }); diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index a0646a86e0c..2c7716e8834 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -185,10 +185,17 @@ function parseMessageContent(content: string, messageType: string): string { } function checkBotMentioned(event: FeishuMessageEvent, botOpenId?: string): boolean { - const mentions = event.message.mentions ?? []; - if (mentions.length === 0) return false; if (!botOpenId) return false; - return mentions.some((m) => m.id.open_id === botOpenId); + const mentions = event.message.mentions ?? []; + if (mentions.length > 0) { + return mentions.some((m) => m.id.open_id === botOpenId); + } + // Post (rich text) messages may have empty message.mentions when they contain docs/paste + if (event.message.message_type === "post") { + const mentionedIds = getMentionedOpenIdsFromPost(event.message.content); + return mentionedIds.includes(botOpenId); + } + return false; } function stripBotMention( @@ -237,9 +244,37 @@ function parseMediaKeys( } } +/** + * Extract mentioned user open_ids from post (rich text) content. + * Feishu may not populate message.mentions for post messages (e.g. when pasting docs); + * the "at" elements in post body use user_id (open_id). Returns non-empty only for post content. + */ +function getMentionedOpenIdsFromPost(content: string): string[] { + try { + const parsed = JSON.parse(content); + const contentBlocks = + parsed.content ?? + parsed.zh_cn?.content ?? + ([] as Array>); + const ids: string[] = []; + for (const paragraph of contentBlocks) { + if (!Array.isArray(paragraph)) continue; + for (const element of paragraph) { + if (element.tag === "at" && element.user_id && element.user_id !== "all") { + ids.push(element.user_id); + } + } + } + return ids; + } catch { + return []; + } +} + /** * Parse post (rich text) content and extract embedded image keys. * Post structure: { title?: string, content: [[{ tag, text?, image_key?, ... }]] } + * or { zh_cn: { title?, content: [...] } } when received from Feishu. */ function parsePostContent(content: string): { textContent: string; @@ -247,8 +282,9 @@ function parsePostContent(content: string): { } { try { const parsed = JSON.parse(content); - const title = parsed.title || ""; - const contentBlocks = parsed.content || []; + const locale = parsed.zh_cn ?? parsed; + const title = locale.title || ""; + const contentBlocks = locale.content || []; let textContent = title ? `${title}\n\n` : ""; const imageKeys: string[] = []; @@ -273,11 +309,11 @@ function parsePostContent(content: string): { } return { - textContent: textContent.trim() || "[富文本消息]", + textContent: textContent.trim() || "[Rich text message]", imageKeys, }; } catch { - return { textContent: "[富文本消息]", imageKeys: [] }; + return { textContent: "[Rich text message]", imageKeys: [] }; } }