mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:50:42 +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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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 { buildFeishuConversationId } from "./conversation-id.js";
|
||||||
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
||||||
import { downloadMessageResourceFeishu } from "./media.js";
|
import { downloadMessageResourceFeishu } from "./media.js";
|
||||||
|
import { isFeishuBroadcastMention } from "./mention.js";
|
||||||
import { parsePostContent } from "./post.js";
|
import { parsePostContent } from "./post.js";
|
||||||
import { getFeishuRuntime } from "./runtime.js";
|
import { getFeishuRuntime } from "./runtime.js";
|
||||||
import type { FeishuChatType, FeishuMediaInfo } from "./types.js";
|
import type { FeishuChatType, FeishuMediaInfo } from "./types.js";
|
||||||
@@ -249,15 +250,16 @@ export function checkBotMentioned(event: FeishuMessageLike, botOpenId?: string):
|
|||||||
if (!botOpenId) {
|
if (!botOpenId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ((event.message.content ?? "").includes("@_all")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const mentions = event.message.mentions ?? [];
|
const mentions = event.message.mentions ?? [];
|
||||||
if (mentions.length > 0) {
|
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") {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,45 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
|
|||||||
expect(ctx.mentionedBot).toBe(false);
|
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)", () => {
|
it("returns mentionedBot=false when botOpenId is undefined (unknown bot)", () => {
|
||||||
const event = makeEvent("group", [
|
const event = makeEvent("group", [
|
||||||
{ key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
|
{ key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
|
||||||
@@ -159,6 +198,39 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
|
|||||||
expect(ctx.mentionedBot).toBe(false);
|
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", () => {
|
it("preserves post code and code_block content", () => {
|
||||||
const event = makePostEvent({
|
const event = makePostEvent({
|
||||||
content: [
|
content: [
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ export type { MentionTarget } from "./mention-target.types.js";
|
|||||||
import type { MentionTarget } from "./mention-target.types.js";
|
import type { MentionTarget } from "./mention-target.types.js";
|
||||||
import { isFeishuGroupChatType } from "./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.
|
* 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, "\\$&");
|
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)
|
* Extract mention targets from message event (excluding the bot itself)
|
||||||
*/
|
*/
|
||||||
@@ -21,6 +41,9 @@ export function extractMentionTargets(
|
|||||||
|
|
||||||
return mentions
|
return mentions
|
||||||
.filter((m) => {
|
.filter((m) => {
|
||||||
|
if (isFeishuBroadcastMention(m)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Exclude the bot itself
|
// Exclude the bot itself
|
||||||
if (botOpenId && m.id.open_id === botOpenId) {
|
if (botOpenId && m.id.open_id === botOpenId) {
|
||||||
return false;
|
return false;
|
||||||
@@ -48,14 +71,15 @@ export function isMentionForwardRequest(event: FeishuMessageEvent, botOpenId?: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDirectMessage = !isFeishuGroupChatType(event.message.chat_type);
|
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) {
|
if (isDirectMessage) {
|
||||||
// DM: trigger if any non-bot user is mentioned
|
// DM: trigger if any non-bot user is mentioned
|
||||||
return hasOtherMention;
|
return hasOtherMention;
|
||||||
}
|
}
|
||||||
// Group: need to mention both bot and other users
|
// 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;
|
return hasBotMention && hasOtherMention;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user