fix(feishu): add reactionNotifications mode gating (openclaw#29388) thanks @Takhoffman

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Tak Hoffman
2026-02-27 21:47:12 -06:00
committed by GitHub
parent 0e4c24ebe2
commit aef5355102
4 changed files with 63 additions and 4 deletions

View File

@@ -110,6 +110,7 @@ const GroupSessionScopeSchema = z
* - "enabled": Messages in different topics get separate sessions
*/
const TopicSessionModeSchema = z.enum(["disabled", "enabled"]).optional();
const ReactionNotificationModeSchema = z.enum(["off", "own", "all"]).optional();
/**
* Reply-in-thread mode for group chats.
@@ -159,6 +160,7 @@ const FeishuSharedConfigShape = {
streaming: StreamingModeSchema,
tools: FeishuToolsConfigSchema,
replyInThread: ReplyInThreadSchema,
reactionNotifications: ReactionNotificationModeSchema,
};
/**
@@ -195,6 +197,7 @@ export const FeishuConfigSchema = z
webhookPath: z.string().optional().default("/feishu/events"),
...FeishuSharedConfigShape,
dmPolicy: DmPolicySchema.optional().default("pairing"),
reactionNotifications: ReactionNotificationModeSchema.optional().default("own"),
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
requireMention: z.boolean().optional().default(true),
groupSessionScope: GroupSessionScopeSchema,

View File

@@ -49,6 +49,31 @@ describe("resolveReactionSyntheticEvent", () => {
expect(result).toBeNull();
});
it("drops reactions when reactionNotifications is off", async () => {
const event = makeReactionEvent();
const result = await resolveReactionSyntheticEvent({
cfg: {
channels: {
feishu: {
reactionNotifications: "off",
},
},
} as ClawdbotConfig,
accountId: "default",
event,
botOpenId: "ou_bot",
fetchMessage: async () => ({
messageId: "om_msg1",
chatId: "oc_group",
senderOpenId: "ou_bot",
senderType: "app",
content: "hello",
contentType: "text",
}),
});
expect(result).toBeNull();
});
it("filters reactions on non-bot messages", async () => {
const event = makeReactionEvent();
const result = await resolveReactionSyntheticEvent({
@@ -68,6 +93,32 @@ describe("resolveReactionSyntheticEvent", () => {
expect(result).toBeNull();
});
it("allows non-bot reactions when reactionNotifications is all", async () => {
const event = makeReactionEvent();
const result = await resolveReactionSyntheticEvent({
cfg: {
channels: {
feishu: {
reactionNotifications: "all",
},
},
} as ClawdbotConfig,
accountId: "default",
event,
botOpenId: "ou_bot",
fetchMessage: async () => ({
messageId: "om_msg1",
chatId: "oc_group",
senderOpenId: "ou_other",
senderType: "user",
content: "hello",
contentType: "text",
}),
uuid: () => "fixed-uuid",
});
expect(result?.message.message_id).toBe("om_msg1:reaction:THUMBSUP:fixed-uuid");
});
it("drops unverified reactions when sender verification times out", async () => {
const event = makeReactionEvent();
const result = await resolveReactionSyntheticEvent({

View File

@@ -177,6 +177,12 @@ export async function resolveReactionSyntheticEvent(
return null;
}
const account = resolveFeishuAccount({ cfg, accountId });
const reactionNotifications = account.config.reactionNotifications ?? "own";
if (reactionNotifications === "off") {
return null;
}
// Skip bot self-reactions
if (event.operator_type === "app" || senderId === botOpenId) {
return null;
@@ -187,9 +193,7 @@ export async function resolveReactionSyntheticEvent(
return null;
}
// Fail closed if bot identity cannot be resolved; otherwise reactions on any
// message can leak into the agent.
if (!botOpenId) {
if (reactionNotifications === "own" && !botOpenId) {
logger?.(
`feishu[${accountId}]: bot open_id unavailable, skipping reaction ${emoji} on ${messageId}`,
);
@@ -201,7 +205,7 @@ export async function resolveReactionSyntheticEvent(
verificationTimeoutMs,
).catch(() => null);
const isBotMessage = reactedMsg?.senderType === "app" || reactedMsg?.senderOpenId === botOpenId;
if (!reactedMsg || !isBotMessage) {
if (!reactedMsg || (reactionNotifications === "own" && !isBotMessage)) {
logger?.(
`feishu[${accountId}]: ignoring reaction on non-bot/unverified message ${messageId} ` +
`(sender: ${reactedMsg?.senderOpenId ?? "unknown"})`,