diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 63b898a23fb..0cfda65ea47 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -414,6 +414,11 @@ export async function handleFeishuMessage(params: { ({ requireMention } = resolveFeishuReplyPolicy({ isDirectMessage: false, + isThreadReply: + groupSession?.threadReply && + (groupSession?.groupSessionScope === "group_topic" || + groupSession?.groupSessionScope === "group_topic_sender" || + groupSession?.replyInThread), 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..01530e12cba 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,90 @@ 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("requireMentionInThread tightens when base requireMention is false", () => { + expect( + resolveFeishuReplyPolicy({ + isDirectMessage: false, + isThreadReply: true, + globalConfig: { requireMention: 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..1db6e6618c6 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 (params.isThreadReply) { + const threadOverride = + params.groupConfig?.requireMentionInThread ?? params.globalConfig?.requireMentionInThread; + if (threadOverride !== undefined) { + return { requireMention: threadOverride }; + } + } + return { requireMention }; }