Skip to content

Commit aef5355

Browse files
authored
fix(feishu): add reactionNotifications mode gating (#29388) thanks @Takhoffman
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: Takhoffman <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent 0e4c24e commit aef5355

File tree

4 files changed

+63
-4
lines changed

4 files changed

+63
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
1818
### Fixes
1919

2020
- Feishu/Reply media attachments: send Feishu reply `mediaUrl`/`mediaUrls` payloads as attachments alongside text/streamed replies in the reply dispatcher, including legacy fallback when `mediaUrls` is empty. (#28959)
21+
- Feishu/Reaction notifications: add `channels.feishu.reactionNotifications` (`off | own | all`, default `own`) so operators can disable reaction ingress or allow all verified reaction events (not only bot-authored message reactions). (#28529)
2122
- Feishu/Group session routing: add configurable group session scopes (`group`, `group_sender`, `group_topic`, `group_topic_sender`) with legacy `topicSessionMode=enabled` compatibility so Feishu group conversations can isolate sessions by sender/topic as configured. (#17798)
2223
- Feishu/Reply-in-thread routing: add `replyInThread` config (`disabled|enabled`) for group replies, propagate `reply_in_thread` across text/card/media/streaming sends, and align topic-scoped session routing so newly created reply threads stay on the same session root. (#27325)
2324
- Feishu/Typing backoff: re-throw Feishu typing add/remove rate-limit and quota errors (`429`, `99991400`, `99991403`) and detect SDK non-throwing backoff responses so the typing keepalive circuit breaker can stop retries instead of looping indefinitely. (#28494)

extensions/feishu/src/config-schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ const GroupSessionScopeSchema = z
110110
* - "enabled": Messages in different topics get separate sessions
111111
*/
112112
const TopicSessionModeSchema = z.enum(["disabled", "enabled"]).optional();
113+
const ReactionNotificationModeSchema = z.enum(["off", "own", "all"]).optional();
113114

114115
/**
115116
* Reply-in-thread mode for group chats.
@@ -159,6 +160,7 @@ const FeishuSharedConfigShape = {
159160
streaming: StreamingModeSchema,
160161
tools: FeishuToolsConfigSchema,
161162
replyInThread: ReplyInThreadSchema,
163+
reactionNotifications: ReactionNotificationModeSchema,
162164
};
163165

164166
/**
@@ -195,6 +197,7 @@ export const FeishuConfigSchema = z
195197
webhookPath: z.string().optional().default("/feishu/events"),
196198
...FeishuSharedConfigShape,
197199
dmPolicy: DmPolicySchema.optional().default("pairing"),
200+
reactionNotifications: ReactionNotificationModeSchema.optional().default("own"),
198201
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
199202
requireMention: z.boolean().optional().default(true),
200203
groupSessionScope: GroupSessionScopeSchema,

extensions/feishu/src/monitor.reaction.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,31 @@ describe("resolveReactionSyntheticEvent", () => {
4949
expect(result).toBeNull();
5050
});
5151

52+
it("drops reactions when reactionNotifications is off", async () => {
53+
const event = makeReactionEvent();
54+
const result = await resolveReactionSyntheticEvent({
55+
cfg: {
56+
channels: {
57+
feishu: {
58+
reactionNotifications: "off",
59+
},
60+
},
61+
} as ClawdbotConfig,
62+
accountId: "default",
63+
event,
64+
botOpenId: "ou_bot",
65+
fetchMessage: async () => ({
66+
messageId: "om_msg1",
67+
chatId: "oc_group",
68+
senderOpenId: "ou_bot",
69+
senderType: "app",
70+
content: "hello",
71+
contentType: "text",
72+
}),
73+
});
74+
expect(result).toBeNull();
75+
});
76+
5277
it("filters reactions on non-bot messages", async () => {
5378
const event = makeReactionEvent();
5479
const result = await resolveReactionSyntheticEvent({
@@ -68,6 +93,32 @@ describe("resolveReactionSyntheticEvent", () => {
6893
expect(result).toBeNull();
6994
});
7095

96+
it("allows non-bot reactions when reactionNotifications is all", async () => {
97+
const event = makeReactionEvent();
98+
const result = await resolveReactionSyntheticEvent({
99+
cfg: {
100+
channels: {
101+
feishu: {
102+
reactionNotifications: "all",
103+
},
104+
},
105+
} as ClawdbotConfig,
106+
accountId: "default",
107+
event,
108+
botOpenId: "ou_bot",
109+
fetchMessage: async () => ({
110+
messageId: "om_msg1",
111+
chatId: "oc_group",
112+
senderOpenId: "ou_other",
113+
senderType: "user",
114+
content: "hello",
115+
contentType: "text",
116+
}),
117+
uuid: () => "fixed-uuid",
118+
});
119+
expect(result?.message.message_id).toBe("om_msg1:reaction:THUMBSUP:fixed-uuid");
120+
});
121+
71122
it("drops unverified reactions when sender verification times out", async () => {
72123
const event = makeReactionEvent();
73124
const result = await resolveReactionSyntheticEvent({

extensions/feishu/src/monitor.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ export async function resolveReactionSyntheticEvent(
177177
return null;
178178
}
179179

180+
const account = resolveFeishuAccount({ cfg, accountId });
181+
const reactionNotifications = account.config.reactionNotifications ?? "own";
182+
if (reactionNotifications === "off") {
183+
return null;
184+
}
185+
180186
// Skip bot self-reactions
181187
if (event.operator_type === "app" || senderId === botOpenId) {
182188
return null;
@@ -187,9 +193,7 @@ export async function resolveReactionSyntheticEvent(
187193
return null;
188194
}
189195

190-
// Fail closed if bot identity cannot be resolved; otherwise reactions on any
191-
// message can leak into the agent.
192-
if (!botOpenId) {
196+
if (reactionNotifications === "own" && !botOpenId) {
193197
logger?.(
194198
`feishu[${accountId}]: bot open_id unavailable, skipping reaction ${emoji} on ${messageId}`,
195199
);
@@ -201,7 +205,7 @@ export async function resolveReactionSyntheticEvent(
201205
verificationTimeoutMs,
202206
).catch(() => null);
203207
const isBotMessage = reactedMsg?.senderType === "app" || reactedMsg?.senderOpenId === botOpenId;
204-
if (!reactedMsg || !isBotMessage) {
208+
if (!reactedMsg || (reactionNotifications === "own" && !isBotMessage)) {
205209
logger?.(
206210
`feishu[${accountId}]: ignoring reaction on non-bot/unverified message ${messageId} ` +
207211
`(sender: ${reactedMsg?.senderOpenId ?? "unknown"})`,

0 commit comments

Comments
 (0)