Skip to content

Commit e0b1b48

Browse files
feishu: fall back to user_id for inbound sender identity (#26703) thanks @NewdlDewdl
Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: NewdlDewdl <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent f29c642 commit e0b1b48

File tree

3 files changed

+42
-13
lines changed

3 files changed

+42
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
2828
- 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.
2929
- 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.
3030
- 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.
31+
- 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.
3132
- 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.
3233
- 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.
3334
- Feishu/Group sender allowlist fallback: add global `channels.feishu.groupSenderAllowFrom` sender authorization for group chats, with per-group `groups.<id>.allowFrom` precedence and regression coverage for allow/block/precedence behavior. (#29174) Thanks @1MoreBuild.

extensions/feishu/src/bot.checkBotMentioned.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
5959
expect(ctx.mentionedBot).toBe(false);
6060
});
6161

62+
it("falls back to sender user_id when open_id is missing", () => {
63+
const event = makeEvent("p2p", []);
64+
(event as any).sender.sender_id = { user_id: "u_mobile_only" };
65+
66+
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
67+
expect(ctx.senderOpenId).toBe("u_mobile_only");
68+
expect(ctx.senderId).toBe("u_mobile_only");
69+
});
70+
6271
it("returns mentionedBot=true when bot is mentioned", () => {
6372
const event = makeEvent("group", [
6473
{ key: "@_user_1", name: "Bot", id: { open_id: BOT_OPEN_ID } },

extensions/feishu/src/bot.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function extractPermissionError(err: unknown): PermissionError | null {
7373
}
7474

7575
// --- Sender name resolution (so the agent can distinguish who is speaking in group chats) ---
76-
// Cache display names by open_id to avoid an API call on every message.
76+
// Cache display names by sender id (open_id/user_id) to avoid an API call on every message.
7777
const SENDER_NAME_TTL_MS = 10 * 60 * 1000;
7878
const senderNameCache = new Map<string, { name: string; expireAt: number }>();
7979

@@ -87,26 +87,40 @@ type SenderNameResult = {
8787
permissionError?: PermissionError;
8888
};
8989

90+
function resolveSenderLookupIdType(senderId: string): "open_id" | "user_id" | "union_id" {
91+
const trimmed = senderId.trim();
92+
if (trimmed.startsWith("ou_")) {
93+
return "open_id";
94+
}
95+
if (trimmed.startsWith("on_")) {
96+
return "union_id";
97+
}
98+
return "user_id";
99+
}
100+
90101
async function resolveFeishuSenderName(params: {
91102
account: ResolvedFeishuAccount;
92-
senderOpenId: string;
103+
senderId: string;
93104
log: (...args: any[]) => void;
94105
}): Promise<SenderNameResult> {
95-
const { account, senderOpenId, log } = params;
106+
const { account, senderId, log } = params;
96107
if (!account.configured) return {};
97-
if (!senderOpenId) return {};
98108

99-
const cached = senderNameCache.get(senderOpenId);
109+
const normalizedSenderId = senderId.trim();
110+
if (!normalizedSenderId) return {};
111+
112+
const cached = senderNameCache.get(normalizedSenderId);
100113
const now = Date.now();
101114
if (cached && cached.expireAt > now) return { name: cached.name };
102115

103116
try {
104117
const client = createFeishuClient(account);
118+
const userIdType = resolveSenderLookupIdType(normalizedSenderId);
105119

106-
// contact/v3/users/:user_id?user_id_type=open_id
120+
// contact/v3/users/:user_id?user_id_type=<open_id|user_id|union_id>
107121
const res: any = await client.contact.user.get({
108-
path: { user_id: senderOpenId },
109-
params: { user_id_type: "open_id" },
122+
path: { user_id: normalizedSenderId },
123+
params: { user_id_type: userIdType },
110124
});
111125

112126
const name: string | undefined =
@@ -116,7 +130,7 @@ async function resolveFeishuSenderName(params: {
116130
res?.data?.user?.en_name;
117131

118132
if (name && typeof name === "string") {
119-
senderNameCache.set(senderOpenId, { name, expireAt: now + SENDER_NAME_TTL_MS });
133+
senderNameCache.set(normalizedSenderId, { name, expireAt: now + SENDER_NAME_TTL_MS });
120134
return { name };
121135
}
122136

@@ -130,7 +144,7 @@ async function resolveFeishuSenderName(params: {
130144
}
131145

132146
// Best-effort. Don't fail message handling if name lookup fails.
133-
log(`feishu: failed to resolve sender name for ${senderOpenId}: ${String(err)}`);
147+
log(`feishu: failed to resolve sender name for ${normalizedSenderId}: ${String(err)}`);
134148
return {};
135149
}
136150
}
@@ -629,12 +643,17 @@ export function parseFeishuMessageEvent(
629643
const rawContent = parseMessageContent(event.message.content, event.message.message_type);
630644
const mentionedBot = checkBotMentioned(event, botOpenId);
631645
const content = stripBotMention(rawContent, event.message.mentions);
646+
const senderOpenId = event.sender.sender_id.open_id?.trim();
647+
const senderUserId = event.sender.sender_id.user_id?.trim();
648+
const senderFallbackId = senderOpenId || senderUserId || "";
632649

633650
const ctx: FeishuMessageContext = {
634651
chatId: event.message.chat_id,
635652
messageId: event.message.message_id,
636-
senderId: event.sender.sender_id.user_id || event.sender.sender_id.open_id || "",
637-
senderOpenId: event.sender.sender_id.open_id || "",
653+
senderId: senderUserId || senderOpenId || "",
654+
// Keep the historical field name, but fall back to user_id when open_id is unavailable
655+
// (common in some mobile app deliveries).
656+
senderOpenId: senderFallbackId,
638657
chatType: event.message.chat_type,
639658
mentionedBot,
640659
rootId: event.message.root_id || undefined,
@@ -754,7 +773,7 @@ export async function handleFeishuMessage(params: {
754773
// Resolve sender display name (best-effort) so the agent can attribute messages correctly.
755774
const senderResult = await resolveFeishuSenderName({
756775
account,
757-
senderOpenId: ctx.senderOpenId,
776+
senderId: ctx.senderOpenId,
758777
log,
759778
});
760779
if (senderResult.name) ctx = { ...ctx, senderName: senderResult.name };

0 commit comments

Comments
 (0)