diff --git a/CHANGELOG.md b/CHANGELOG.md index 808c6e772ea..d43f63b7c5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/feishu/src/bot-content.ts b/extensions/feishu/src/bot-content.ts index 5eba58d191d..d9a2c88fa0e 100644 --- a/extensions/feishu/src/bot-content.ts +++ b/extensions/feishu/src/bot-content.ts @@ -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; } diff --git a/extensions/feishu/src/bot.checkBotMentioned.test.ts b/extensions/feishu/src/bot.checkBotMentioned.test.ts index 8aa80b31f3f..e2064f9d6ba 100644 --- a/extensions/feishu/src/bot.checkBotMentioned.test.ts +++ b/extensions/feishu/src/bot.checkBotMentioned.test.ts @@ -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: [ diff --git a/extensions/feishu/src/mention.ts b/extensions/feishu/src/mention.ts index f320cb0e868..86d4e2ad06d 100644 --- a/extensions/feishu/src/mention.ts +++ b/extensions/feishu/src/mention.ts @@ -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; }