mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user