From a9dfeb0d62cfdfef2909bb0ab447aaf303c592ff Mon Sep 17 00:00:00 2001 From: George Zhang Date: Tue, 31 Mar 2026 22:46:56 -0700 Subject: [PATCH] fix: honor Feishu comment user_id allowlists --- CHANGELOG.md | 1 + extensions/feishu/src/comment-handler.test.ts | 53 +++++++++++++++++++ extensions/feishu/src/comment-handler.ts | 1 + extensions/feishu/src/monitor.comment.test.ts | 23 ++++++++ extensions/feishu/src/monitor.comment.ts | 5 ++ 5 files changed, 83 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 126ad49ba9f..1d28dc24fde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai - Agents/compaction: resolve `agents.defaults.compaction.model` consistently for manual `/compact` and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg - Channels/session routing: move provider-specific session conversation grammar into plugin-owned session-key surfaces, preserving Telegram topic routing and Feishu scoped inheritance across bootstrap, model override, restart, and tool-policy paths. - WhatsApp/reactions: add `reactionLevel` guidance for agent reactions. Thanks @mcaxtr. +- Feishu/comments: add a dedicated Drive comment-event flow with comment-thread context resolution, in-thread replies, and `feishu_drive` comment actions for document collaboration workflows. (#58497) thanks @wittam-01. ### Fixes diff --git a/extensions/feishu/src/comment-handler.test.ts b/extensions/feishu/src/comment-handler.test.ts index ee274a71954..946430db31b 100644 --- a/extensions/feishu/src/comment-handler.test.ts +++ b/extensions/feishu/src/comment-handler.test.ts @@ -67,6 +67,7 @@ describe("handleFeishuCommentEvent", () => { fileToken: "doc_token_1", fileType: "docx", senderId: "ou_sender", + senderUserId: "on_sender_user", timestamp: "1774951528000", isMentioned: true, documentTitle: "Project review", @@ -146,6 +147,58 @@ describe("handleFeishuCommentEvent", () => { expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1); }); + it("allows comment senders matched by user_id allowlist entries", async () => { + const runtime = createPluginRuntimeMock({ + channel: { + pairing: { + readAllowFromStore: vi.fn(async () => []), + }, + routing: { + resolveAgentRoute: vi.fn(() => buildResolvedRoute()), + }, + reply: { + dispatchReplyFromConfig: vi.fn(async () => ({ + queuedFinal: true, + counts: { tool: 0, block: 0, final: 1 }, + })), + withReplyDispatcher: vi.fn(async ({ run, onSettled }) => { + try { + return await run(); + } finally { + await onSettled?.(); + } + }), + }, + }, + }); + setFeishuRuntime(runtime); + + await handleFeishuCommentEvent({ + cfg: buildConfig({ + channels: { + feishu: { + enabled: true, + dmPolicy: "allowlist", + allowFrom: ["on_sender_user"], + }, + }, + }), + accountId: "default", + event: { event_id: "evt_1" }, + botOpenId: "ou_bot", + runtime: { + log: vi.fn(), + error: vi.fn(), + } as never, + }); + + const dispatchReplyFromConfig = runtime.channel.reply.dispatchReplyFromConfig as ReturnType< + typeof vi.fn + >; + expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1); + expect(replyCommentMock).not.toHaveBeenCalled(); + }); + it("issues a pairing challenge in the comment thread when dmPolicy=pairing", async () => { const runtime = createPluginRuntimeMock({ channel: { diff --git a/extensions/feishu/src/comment-handler.ts b/extensions/feishu/src/comment-handler.ts index b14a4e8938a..f566c7875c5 100644 --- a/extensions/feishu/src/comment-handler.ts +++ b/extensions/feishu/src/comment-handler.ts @@ -93,6 +93,7 @@ export async function handleFeishuCommentEvent( const senderAllowed = resolveFeishuAllowlistMatch({ allowFrom: effectiveDmAllowFrom, senderId: turn.senderId, + senderIds: [turn.senderUserId], }).allowed; if (dmPolicy !== "open" && !senderAllowed) { if (dmPolicy === "pairing") { diff --git a/extensions/feishu/src/monitor.comment.test.ts b/extensions/feishu/src/monitor.comment.test.ts index fee1184e5af..17557c1e541 100644 --- a/extensions/feishu/src/monitor.comment.test.ts +++ b/extensions/feishu/src/monitor.comment.test.ts @@ -262,6 +262,29 @@ describe("resolveDriveCommentEventTurn", () => { expect(turn?.prompt).toContain("The system will automatically reply with your final answer"); }); + it("preserves sender user_id for downstream allowlist checks", async () => { + const client = makeOpenApiClient({ includeTargetReplyInBatch: true }); + + const turn = await resolveDriveCommentEventTurn({ + cfg: buildMonitorConfig(), + accountId: "default", + event: makeDriveCommentEvent({ + notice_meta: { + ...makeDriveCommentEvent().notice_meta, + from_user_id: { + open_id: "ou_509d4d7ace4a9addec2312676ffcba9b", + user_id: "on_comment_user_1", + }, + }, + }), + botOpenId: "ou_bot", + createClient: () => client as never, + }); + + expect(turn?.senderId).toBe("ou_509d4d7ace4a9addec2312676ffcba9b"); + expect(turn?.senderUserId).toBe("on_comment_user_1"); + }); + it("falls back to the replies API to resolve add_reply text", async () => { const client = makeOpenApiClient({ includeTargetReplyInBatch: false, diff --git a/extensions/feishu/src/monitor.comment.ts b/extensions/feishu/src/monitor.comment.ts index a906bbcb332..8c8f9332318 100644 --- a/extensions/feishu/src/monitor.comment.ts +++ b/extensions/feishu/src/monitor.comment.ts @@ -50,6 +50,7 @@ export type ResolvedDriveCommentEventTurn = { fileToken: string; fileType: CommentFileType; senderId: string; + senderUserId?: string; timestamp?: string; isMentioned?: boolean; documentTitle?: string; @@ -443,6 +444,7 @@ async function resolveDriveCommentEventCore(params: ResolveDriveCommentEventPara fileToken: string; fileType: CommentFileType; senderId: string; + senderUserId?: string; timestamp?: string; isMentioned?: boolean; context: { @@ -469,6 +471,7 @@ async function resolveDriveCommentEventCore(params: ResolveDriveCommentEventPara const fileToken = event.notice_meta?.file_token?.trim(); const fileType = normalizeCommentFileType(event.notice_meta?.file_type); const senderId = event.notice_meta?.from_user_id?.open_id?.trim(); + const senderUserId = event.notice_meta?.from_user_id?.user_id?.trim() || undefined; if (!eventId || !commentId || !noticeType || !fileToken || !fileType || !senderId) { logger?.( `feishu[${accountId}]: drive comment notice missing required fields event=${eventId ?? "unknown"} comment=${commentId ?? "unknown"}`, @@ -513,6 +516,7 @@ async function resolveDriveCommentEventCore(params: ResolveDriveCommentEventPara fileToken, fileType, senderId, + senderUserId, timestamp: event.timestamp, isMentioned: event.is_mentioned, context, @@ -587,6 +591,7 @@ export async function resolveDriveCommentEventTurn( fileToken: resolved.fileToken, fileType: resolved.fileType, senderId: resolved.senderId, + senderUserId: resolved.senderUserId, timestamp: resolved.timestamp, isMentioned: resolved.isMentioned, documentTitle: resolved.context.documentTitle,