fix(feishu): @所有人 (@all) no longer triggers every bot unconditionally

Previously checkBotMentioned() returned true for any message containing
'@_all', causing ALL bots in a Feishu group to respond simultaneously
whenever a user broadcast to @所有人 (#49761).

Fix: remove the unconditional @_all -> true early-return from
checkBotMentioned().  Opt-in is now controlled by a new respondToAtAll
config flag (boolean, default false) that can be set at the account level
(channels.feishu.respondToAtAll or channels.feishu.accounts.<id>.respondToAtAll)
or per-group (channels.feishu.groups.<chatId>.respondToAtAll).

The check is applied in handleFeishuMessage after groupConfig is resolved
so that per-group and per-account settings are both honoured.  Bots that
do not opt in remain silent when @所有人 is used, preserving the pre-existing
behaviour for single-bot deployments and groups that do not want broadcast
responses.

Changes:
- extensions/feishu/src/bot-content.ts: drop @_all -> true, add comment
- extensions/feishu/src/bot.ts: add respondToAtAll opt-in check after
  groupConfig is resolved, before the requireMention gate
- extensions/feishu/src/config-schema.ts: add respondToAtAll?:boolean to
  FeishuSharedConfigShape (account+global level) and FeishuGroupSchema
- extensions/feishu/src/bot.checkBotMentioned.test.ts: two new tests
  confirming mentionedBot=false for @_all via parseFeishuMessageEvent
This commit is contained in:
lbo728 2026-03-18 20:36:47 +09:00
parent 6ae68faf5f
commit 433041c44e
4 changed files with 39 additions and 3 deletions

View File

@ -248,9 +248,11 @@ export function checkBotMentioned(event: FeishuMessageLike, botOpenId?: string):
if (!botOpenId) {
return false;
}
if ((event.message.content ?? "").includes("@_all")) {
return true;
}
// @_all (@所有人) is intentionally NOT treated as mentioning every bot here.
// Whether to respond to @所有人 is an opt-in decision controlled by the
// respondToAtAll config flag; the check is applied in handleFeishuMessage
// after the group config is resolved so that per-group and per-account
// settings are both honoured.
const mentions = event.message.mentions ?? [];
if (mentions.length > 0) {
return mentions.some((mention) => mention.id.open_id === botOpenId);

View File

@ -190,4 +190,25 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
expect(ctx.content).toBe("[Forwarded message: sc_abc123]");
});
// @_all (@所有人) behaviour — opt-in is controlled by respondToAtAll config
// and resolved later in handleFeishuMessage; parseFeishuMessageEvent itself
// no longer unconditionally sets mentionedBot=true for @_all.
it("returns mentionedBot=false for @_all message (respondToAtAll opt-in not yet applied)", () => {
const event = makeEvent("group", [], "@_all hello everyone");
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
expect(ctx.mentionedBot).toBe(false);
});
it("returns mentionedBot=false for @_all even when bot is also in mentions list (opt-in path takes precedence)", () => {
// Explicit bot mention still wins via the mentions array path
const event = makeEvent(
"group",
[{ key: "@_user_1", name: "Bot", id: { open_id: BOT_OPEN_ID } }],
"@_all hello",
);
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
// Bot IS in the mentions array, so it should still be true via that path
expect(ctx.mentionedBot).toBe(true);
});
});

View File

@ -418,6 +418,17 @@ export async function handleFeishuMessage(params: {
groupConfig,
}));
// @_all (@所有人) opt-in: if the message targets all group members and the
// account or group is explicitly configured with respondToAtAll:true, treat
// it as mentioning this bot. Default is false so bots that do not opt in
// stay silent when a user broadcasts to the whole group.
if (!ctx.mentionedBot && (event.message.content ?? "").includes("@_all")) {
const respondToAtAll = groupConfig?.respondToAtAll ?? feishuCfg?.respondToAtAll ?? false;
if (respondToAtAll) {
ctx = { ...ctx, mentionedBot: true };
}
}
if (requireMention && !ctx.mentionedBot) {
log(`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot`);
// Record to pending history for non-broadcast groups only. For broadcast groups,

View File

@ -141,6 +141,7 @@ const ReplyInThreadSchema = z.enum(["disabled", "enabled"]).optional();
export const FeishuGroupSchema = z
.object({
requireMention: z.boolean().optional(),
respondToAtAll: 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(),
respondToAtAll: 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(),