feishu: fall back to user_id for inbound sender identity (openclaw#26703) thanks @NewdlDewdl

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: NewdlDewdl <230946873+NewdlDewdl@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Rohin
2026-02-27 22:59:42 -06:00
committed by GitHub
parent f29c642c13
commit e0b1b48be3
3 changed files with 42 additions and 13 deletions

View File

@@ -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.<id>.allowFrom` precedence and regression coverage for allow/block/precedence behavior. (#29174) Thanks @1MoreBuild.

View File

@@ -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 } },

View File

@@ -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<string, { name: string; expireAt: number }>();
@@ -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<SenderNameResult> {
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=<open_id|user_id|union_id>
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 };