feat(feishu): add requireMentionInThread to allow thread replies without @mention

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 <noreply@anthropic.com>
This commit is contained in:
Strider 2026-03-09 11:32:29 +08:00
parent 4c60956d8e
commit 80daf16550
4 changed files with 87 additions and 1 deletions

View File

@ -414,6 +414,7 @@ export async function handleFeishuMessage(params: {
({ requireMention } = resolveFeishuReplyPolicy({
isDirectMessage: false,
isThreadReply: groupSession?.threadReply,
globalConfig: feishuCfg,
groupConfig,
}));

View File

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

View File

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

View File

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