From 80daf16550a2f9fe402e2cc880e480cd114bb2df Mon Sep 17 00:00:00 2001 From: Strider Date: Mon, 9 Mar 2026 11:32:29 +0800 Subject: [PATCH] feat(feishu): add requireMentionInThread to allow thread replies without @mention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When requireMention is true for a group, thread replies still require an explicit @mention — even though thread context makes the intent unambiguous. This adds a requireMentionInThread config option at both the global and per-group level, letting users opt out of the mention requirement for thread replies while keeping it for top-level group messages. Closes #40475 Co-Authored-By: Claude Opus 4.6 --- extensions/feishu/src/bot.ts | 1 + extensions/feishu/src/config-schema.ts | 2 + extensions/feishu/src/policy.test.ts | 76 +++++++++++++++++++++++++- extensions/feishu/src/policy.ts | 9 +++ 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 63b898a23fb..a3b125ceecd 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -414,6 +414,7 @@ export async function handleFeishuMessage(params: { ({ requireMention } = resolveFeishuReplyPolicy({ isDirectMessage: false, + isThreadReply: groupSession?.threadReply, globalConfig: feishuCfg, groupConfig, })); diff --git a/extensions/feishu/src/config-schema.ts b/extensions/feishu/src/config-schema.ts index db1714f173f..38b7255626d 100644 --- a/extensions/feishu/src/config-schema.ts +++ b/extensions/feishu/src/config-schema.ts @@ -141,6 +141,7 @@ const ReplyInThreadSchema = z.enum(["disabled", "enabled"]).optional(); export const FeishuGroupSchema = z .object({ requireMention: z.boolean().optional(), + requireMentionInThread: z.boolean().optional(), tools: ToolPolicySchema, skills: z.array(z.string()).optional(), enabled: z.boolean().optional(), @@ -164,6 +165,7 @@ const FeishuSharedConfigShape = { groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupSenderAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), requireMention: z.boolean().optional(), + requireMentionInThread: z.boolean().optional(), groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(), historyLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(), diff --git a/extensions/feishu/src/policy.test.ts b/extensions/feishu/src/policy.test.ts index c53532df3ff..ec5f4430ebe 100644 --- a/extensions/feishu/src/policy.test.ts +++ b/extensions/feishu/src/policy.test.ts @@ -3,8 +3,9 @@ import { isFeishuGroupAllowed, resolveFeishuAllowlistMatch, resolveFeishuGroupConfig, + resolveFeishuReplyPolicy, } from "./policy.js"; -import type { FeishuConfig } from "./types.js"; +import type { FeishuConfig, FeishuGroupConfig } from "./types.js"; describe("feishu policy", () => { describe("resolveFeishuGroupConfig", () => { @@ -100,6 +101,79 @@ describe("feishu policy", () => { }); }); + describe("resolveFeishuReplyPolicy", () => { + it("does not require mention for DMs", () => { + expect(resolveFeishuReplyPolicy({ isDirectMessage: true })).toEqual({ + requireMention: false, + }); + }); + + it("requires mention in group by default", () => { + expect(resolveFeishuReplyPolicy({ isDirectMessage: false })).toEqual({ + requireMention: true, + }); + }); + + it("respects group-level requireMention override", () => { + expect( + resolveFeishuReplyPolicy({ + isDirectMessage: false, + groupConfig: { requireMention: false } as FeishuGroupConfig, + }), + ).toEqual({ requireMention: false }); + }); + + it("still requires mention for thread replies when no thread override is set", () => { + expect( + resolveFeishuReplyPolicy({ + isDirectMessage: false, + isThreadReply: true, + }), + ).toEqual({ requireMention: true }); + }); + + it("skips mention for thread replies when requireMentionInThread is false (global)", () => { + expect( + resolveFeishuReplyPolicy({ + isDirectMessage: false, + isThreadReply: true, + globalConfig: { requireMentionInThread: false } as FeishuConfig, + }), + ).toEqual({ requireMention: false }); + }); + + it("skips mention for thread replies when requireMentionInThread is false (group)", () => { + expect( + resolveFeishuReplyPolicy({ + isDirectMessage: false, + isThreadReply: true, + groupConfig: { requireMentionInThread: false } as FeishuGroupConfig, + }), + ).toEqual({ requireMention: false }); + }); + + it("group-level requireMentionInThread overrides global", () => { + expect( + resolveFeishuReplyPolicy({ + isDirectMessage: false, + isThreadReply: true, + globalConfig: { requireMentionInThread: false } as FeishuConfig, + groupConfig: { requireMentionInThread: true } as FeishuGroupConfig, + }), + ).toEqual({ requireMention: true }); + }); + + it("does not apply thread override for non-thread messages", () => { + expect( + resolveFeishuReplyPolicy({ + isDirectMessage: false, + isThreadReply: false, + globalConfig: { requireMentionInThread: false } as FeishuConfig, + }), + ).toEqual({ requireMention: true }); + }); + }); + describe("isFeishuGroupAllowed", () => { it("matches group IDs with chat: prefix", () => { expect( diff --git a/extensions/feishu/src/policy.ts b/extensions/feishu/src/policy.ts index faee6675127..7a08dfa1877 100644 --- a/extensions/feishu/src/policy.ts +++ b/extensions/feishu/src/policy.ts @@ -105,6 +105,7 @@ export function isFeishuGroupAllowed(params: { export function resolveFeishuReplyPolicy(params: { isDirectMessage: boolean; + isThreadReply?: boolean; globalConfig?: FeishuConfig; groupConfig?: FeishuGroupConfig; }): { requireMention: boolean } { @@ -115,5 +116,13 @@ export function resolveFeishuReplyPolicy(params: { const requireMention = params.groupConfig?.requireMention ?? params.globalConfig?.requireMention ?? true; + if (requireMention && params.isThreadReply) { + const threadOverride = + params.groupConfig?.requireMentionInThread ?? params.globalConfig?.requireMentionInThread; + if (threadOverride !== undefined) { + return { requireMention: threadOverride }; + } + } + return { requireMention }; }