fix(feishu): do not treat @all as a bot mention (#72658)

* fix(feishu): do not treat @all as a bot mention

* fix(feishu): do not treat @all as a bot mention
This commit is contained in:
Vincent Koc
2026-04-27 02:10:17 -07:00
committed by GitHub
parent 14ab00755f
commit b642ebece9
4 changed files with 106 additions and 7 deletions

View File

@@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai
- TTS/SecretRef: resolve `messages.tts.providers.*.apiKey` from the active runtime snapshot so SecretRef-backed MiniMax and other TTS provider keys work in runtime reply/audio paths. Fixes #68690. Thanks @joshavant.
- Gateway/install: surface systemd user-bus recovery hints during Linux service activation and retry via the target user scope when `systemctl --user` reports no-medium bus failures, without letting stale `SUDO_USER` override `sudo -u` installs. Fixes #39673; refs #44417 and #63561. Thanks @Arbor4, @myrsu, @mssteuer, and @boyuaner.
- CLI/startup: read generated startup metadata from the bundled `dist` layout before falling back to live help rendering, so root/browser help and channel-option bootstrap stay on the fast path. Thanks @vincentkoc.
- Feishu/Lark: stop treating broadcast-only `@all`/`@_all` messages as bot mentions while preserving direct bot mentions, including messages that also include `@all`. Fixes #37706. Thanks @JosepLee.
- CLI/help: treat positional `help` invocations like `openclaw channels help` as help paths for startup gating, avoiding model/auth warmup while preserving positional arguments such as `openclaw docs help`. Thanks @gumadeiras.
- Web search: route plugin-scoped web_search SecretRefs through the active runtime config snapshot so provider execution receives resolved credentials across app/runtime paths, including `plugins.entries.brave.config.webSearch.apiKey`. Fixes #68690. Thanks @VACInc.
- Voice Call: allow SecretRef-backed Twilio auth tokens and call-specific OpenAI/ElevenLabs TTS API keys through the plugin config surface. Fixes #68690. Thanks @joshavant.

View File

@@ -2,6 +2,7 @@ import type { ClawdbotConfig } from "../runtime-api.js";
import { buildFeishuConversationId } from "./conversation-id.js";
import { normalizeFeishuExternalKey } from "./external-keys.js";
import { downloadMessageResourceFeishu } from "./media.js";
import { isFeishuBroadcastMention } from "./mention.js";
import { parsePostContent } from "./post.js";
import { getFeishuRuntime } from "./runtime.js";
import type { FeishuChatType, FeishuMediaInfo } from "./types.js";
@@ -249,15 +250,16 @@ export function checkBotMentioned(event: FeishuMessageLike, botOpenId?: string):
if (!botOpenId) {
return false;
}
if ((event.message.content ?? "").includes("@_all")) {
return true;
}
const mentions = event.message.mentions ?? [];
if (mentions.length > 0) {
return mentions.some((mention) => mention.id.open_id === botOpenId);
return mentions.some(
(mention) => !isFeishuBroadcastMention(mention) && mention.id.open_id === botOpenId,
);
}
if (event.message.message_type === "post") {
return parsePostContent(event.message.content).mentionedOpenIds.some((id) => id === botOpenId);
return parsePostContent(event.message.content).mentionedOpenIds.some(
(id) => id.trim().toLowerCase() !== "all" && id === botOpenId,
);
}
return false;
}

View File

@@ -92,6 +92,45 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
expect(ctx.mentionedBot).toBe(false);
});
it("returns mentionedBot=false for broadcast-only @_all text", () => {
const event = makeEvent("group", [], "@_all please review");
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.mentionedBot).toBe(false);
});
it("returns mentionedBot=false for broadcast-only @all mention metadata", () => {
const event = makeEvent("group", [{ key: "@_all", name: "all", id: { open_id: "all" } }]);
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.mentionedBot).toBe(false);
});
it("returns mentionedBot=false for @all even when botOpenId is the broadcast id", () => {
const event = makeEvent("group", [{ key: "@_all", name: "all", id: { open_id: "all" } }]);
const ctx = parseFeishuMessageEvent(event, "all");
expect(ctx.mentionedBot).toBe(false);
});
it("returns mentionedBot=true when bot is mentioned alongside @all", () => {
const event = makeEvent("group", [
{ key: "@_all", name: "all", id: { open_id: "all" } },
{ key: "@_bot_1", name: "Bot", id: { open_id: BOT_OPEN_ID } },
]);
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.mentionedBot).toBe(true);
expect(ctx.mentionTargets).toBeUndefined();
});
it("does not include @all in mention-forward targets", () => {
const event = makeEvent("group", [
{ key: "@_all", name: "all", id: { open_id: "all" } },
{ key: "@_bot_1", name: "Bot", id: { open_id: BOT_OPEN_ID } },
{ key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
]);
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.mentionedBot).toBe(true);
expect(ctx.mentionTargets).toEqual([{ openId: "ou_alice", name: "Alice", key: "@_user_1" }]);
});
it("returns mentionedBot=false when botOpenId is undefined (unknown bot)", () => {
const event = makeEvent("group", [
{ key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
@@ -159,6 +198,39 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
expect(ctx.mentionedBot).toBe(false);
});
it("returns mentionedBot=false for post message with broadcast-only @all", () => {
const event = makePostEvent({
content: [
[{ tag: "at", user_id: "all", user_name: "all" }],
[{ tag: "text", text: "hello" }],
],
});
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.mentionedBot).toBe(false);
});
it("returns mentionedBot=false for post @all even when botOpenId is the broadcast id", () => {
const event = makePostEvent({
content: [[{ tag: "at", user_id: "all", user_name: "all" }]],
});
const ctx = parseFeishuMessageEvent(event, "all");
expect(ctx.mentionedBot).toBe(false);
});
it("returns mentionedBot=true for post message with bot mention and broadcast @all", () => {
const event = makePostEvent({
content: [
[
{ tag: "at", user_id: "all", user_name: "all" },
{ tag: "text", text: " " },
{ tag: "at", user_id: BOT_OPEN_ID, user_name: "claw" },
],
],
});
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.mentionedBot).toBe(true);
});
it("preserves post code and code_block content", () => {
const event = makePostEvent({
content: [

View File

@@ -3,6 +3,16 @@ export type { MentionTarget } from "./mention-target.types.js";
import type { MentionTarget } from "./mention-target.types.js";
import { isFeishuGroupChatType } from "./types.js";
type FeishuMentionLike = {
key?: string;
id?: {
open_id?: string;
user_id?: string;
union_id?: string;
};
name?: string;
};
/**
* Escape regex metacharacters so user-controlled mention fields are treated literally.
*/
@@ -10,6 +20,16 @@ export function escapeRegExp(input: string): string {
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function isFeishuBroadcastMention(mention: FeishuMentionLike): boolean {
const normalizedKey = mention.key?.trim().toLowerCase();
if (normalizedKey === "@all" || normalizedKey === "@_all") {
return true;
}
const mentionIds = [mention.id?.open_id, mention.id?.user_id, mention.id?.union_id];
return mentionIds.some((id) => id?.trim().toLowerCase() === "all");
}
/**
* Extract mention targets from message event (excluding the bot itself)
*/
@@ -21,6 +41,9 @@ export function extractMentionTargets(
return mentions
.filter((m) => {
if (isFeishuBroadcastMention(m)) {
return false;
}
// Exclude the bot itself
if (botOpenId && m.id.open_id === botOpenId) {
return false;
@@ -48,14 +71,15 @@ export function isMentionForwardRequest(event: FeishuMessageEvent, botOpenId?: s
}
const isDirectMessage = !isFeishuGroupChatType(event.message.chat_type);
const hasOtherMention = mentions.some((m) => m.id.open_id !== botOpenId);
const userMentions = mentions.filter((m) => !isFeishuBroadcastMention(m));
const hasOtherMention = userMentions.some((m) => m.id.open_id !== botOpenId);
if (isDirectMessage) {
// DM: trigger if any non-bot user is mentioned
return hasOtherMention;
}
// Group: need to mention both bot and other users
const hasBotMention = mentions.some((m) => m.id.open_id === botOpenId);
const hasBotMention = userMentions.some((m) => m.id.open_id === botOpenId);
return hasBotMention && hasOtherMention;
}