diff --git a/CHANGELOG.md b/CHANGELOG.md index 745bf921b..67eaf9df9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai - Feishu/Probe status caching: cache successful `probeFeishu()` bot-info results for 10 minutes (bounded cache with per-account keying) to reduce repeated status/onboarding probe API calls, while bypassing cache for failures and exceptions. (#28907) Thanks @Glucksberg. - Feishu/Opus media send type: send `.opus` attachments with `msg_type: "audio"` (instead of `"media"`) so Feishu voice messages deliver correctly while `.mp4` remains `msg_type: "media"` and documents remain `msg_type: "file"`. (#28269) Thanks @Glucksberg. - Feishu/Mobile video media type: treat inbound `message_type: "media"` as video-equivalent for media key extraction, placeholder inference, and media download resolution so mobile-app video sends ingest correctly. (#25502) Thanks @4ier. +- Feishu/Inbound sender fallback: fall back to `sender_id.user_id` when `sender_id.open_id` is missing on inbound events, and use ID-type-aware sender lookup so mobile-delivered messages keep stable sender identity/routing. (#26703) Thanks @NewdlDewdl. - Feishu/Inbound rich-text parsing: preserve `share_chat` payload summaries when available and add explicit parsing for rich-text `code`/`code_block`/`pre` tags so forwarded and code-heavy messages keep useful context in agent input. (#28591) Thanks @kevinWangSheng. - Feishu/Local media sends: propagate `mediaLocalRoots` through Feishu outbound media sending into `loadWebMedia` so local path attachments work with post-CVE local-root enforcement. (#27884) Thanks @joelnishanth. - Feishu/Group sender allowlist fallback: add global `channels.feishu.groupSenderAllowFrom` sender authorization for group chats, with per-group `groups..allowFrom` precedence and regression coverage for allow/block/precedence behavior. (#29174) Thanks @1MoreBuild. diff --git a/extensions/feishu/src/bot.checkBotMentioned.test.ts b/extensions/feishu/src/bot.checkBotMentioned.test.ts index 9903bdb17..3036677e4 100644 --- a/extensions/feishu/src/bot.checkBotMentioned.test.ts +++ b/extensions/feishu/src/bot.checkBotMentioned.test.ts @@ -59,6 +59,15 @@ describe("parseFeishuMessageEvent – mentionedBot", () => { expect(ctx.mentionedBot).toBe(false); }); + it("falls back to sender user_id when open_id is missing", () => { + const event = makeEvent("p2p", []); + (event as any).sender.sender_id = { user_id: "u_mobile_only" }; + + const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID); + expect(ctx.senderOpenId).toBe("u_mobile_only"); + expect(ctx.senderId).toBe("u_mobile_only"); + }); + it("returns mentionedBot=true when bot is mentioned", () => { const event = makeEvent("group", [ { key: "@_user_1", name: "Bot", id: { open_id: BOT_OPEN_ID } }, diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index b74d71e57..13c50b690 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -73,7 +73,7 @@ function extractPermissionError(err: unknown): PermissionError | null { } // --- Sender name resolution (so the agent can distinguish who is speaking in group chats) --- -// Cache display names by open_id to avoid an API call on every message. +// Cache display names by sender id (open_id/user_id) to avoid an API call on every message. const SENDER_NAME_TTL_MS = 10 * 60 * 1000; const senderNameCache = new Map(); @@ -87,26 +87,40 @@ type SenderNameResult = { permissionError?: PermissionError; }; +function resolveSenderLookupIdType(senderId: string): "open_id" | "user_id" | "union_id" { + const trimmed = senderId.trim(); + if (trimmed.startsWith("ou_")) { + return "open_id"; + } + if (trimmed.startsWith("on_")) { + return "union_id"; + } + return "user_id"; +} + async function resolveFeishuSenderName(params: { account: ResolvedFeishuAccount; - senderOpenId: string; + senderId: string; log: (...args: any[]) => void; }): Promise { - const { account, senderOpenId, log } = params; + const { account, senderId, log } = params; if (!account.configured) return {}; - if (!senderOpenId) return {}; - const cached = senderNameCache.get(senderOpenId); + const normalizedSenderId = senderId.trim(); + if (!normalizedSenderId) return {}; + + const cached = senderNameCache.get(normalizedSenderId); const now = Date.now(); if (cached && cached.expireAt > now) return { name: cached.name }; try { const client = createFeishuClient(account); + const userIdType = resolveSenderLookupIdType(normalizedSenderId); - // contact/v3/users/:user_id?user_id_type=open_id + // contact/v3/users/:user_id?user_id_type= const res: any = await client.contact.user.get({ - path: { user_id: senderOpenId }, - params: { user_id_type: "open_id" }, + path: { user_id: normalizedSenderId }, + params: { user_id_type: userIdType }, }); const name: string | undefined = @@ -116,7 +130,7 @@ async function resolveFeishuSenderName(params: { res?.data?.user?.en_name; if (name && typeof name === "string") { - senderNameCache.set(senderOpenId, { name, expireAt: now + SENDER_NAME_TTL_MS }); + senderNameCache.set(normalizedSenderId, { name, expireAt: now + SENDER_NAME_TTL_MS }); return { name }; } @@ -130,7 +144,7 @@ async function resolveFeishuSenderName(params: { } // Best-effort. Don't fail message handling if name lookup fails. - log(`feishu: failed to resolve sender name for ${senderOpenId}: ${String(err)}`); + log(`feishu: failed to resolve sender name for ${normalizedSenderId}: ${String(err)}`); return {}; } } @@ -629,12 +643,17 @@ export function parseFeishuMessageEvent( const rawContent = parseMessageContent(event.message.content, event.message.message_type); const mentionedBot = checkBotMentioned(event, botOpenId); const content = stripBotMention(rawContent, event.message.mentions); + const senderOpenId = event.sender.sender_id.open_id?.trim(); + const senderUserId = event.sender.sender_id.user_id?.trim(); + const senderFallbackId = senderOpenId || senderUserId || ""; const ctx: FeishuMessageContext = { chatId: event.message.chat_id, messageId: event.message.message_id, - senderId: event.sender.sender_id.user_id || event.sender.sender_id.open_id || "", - senderOpenId: event.sender.sender_id.open_id || "", + senderId: senderUserId || senderOpenId || "", + // Keep the historical field name, but fall back to user_id when open_id is unavailable + // (common in some mobile app deliveries). + senderOpenId: senderFallbackId, chatType: event.message.chat_type, mentionedBot, rootId: event.message.root_id || undefined, @@ -754,7 +773,7 @@ export async function handleFeishuMessage(params: { // Resolve sender display name (best-effort) so the agent can attribute messages correctly. const senderResult = await resolveFeishuSenderName({ account, - senderOpenId: ctx.senderOpenId, + senderId: ctx.senderOpenId, log, }); if (senderResult.name) ctx = { ...ctx, senderName: senderResult.name };