fix: honor Feishu comment user_id allowlists

This commit is contained in:
George Zhang 2026-03-31 22:46:56 -07:00
parent 6d3c59bd79
commit a9dfeb0d62
No known key found for this signature in database
5 changed files with 83 additions and 0 deletions

View File

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

View File

@ -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: {

View File

@ -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") {

View File

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

View File

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