From f4b288b8f7be98faa43a899ac0b25b422e1b0cd1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 19 Feb 2026 15:04:40 +0100 Subject: [PATCH] refactor(feishu): dedupe mention regex escaping --- .../feishu/src/bot.stripBotMention.test.ts | 38 +++++++++++++++++++ extensions/feishu/src/bot.ts | 23 +++++------ extensions/feishu/src/mention.ts | 9 ++++- 3 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 extensions/feishu/src/bot.stripBotMention.test.ts diff --git a/extensions/feishu/src/bot.stripBotMention.test.ts b/extensions/feishu/src/bot.stripBotMention.test.ts new file mode 100644 index 0000000000..98016115a1 --- /dev/null +++ b/extensions/feishu/src/bot.stripBotMention.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { stripBotMention, type FeishuMessageEvent } from "./bot.js"; + +type Mentions = FeishuMessageEvent["message"]["mentions"]; + +describe("stripBotMention", () => { + it("returns original text when mentions are missing", () => { + expect(stripBotMention("hello world", undefined)).toBe("hello world"); + }); + + it("strips mention name and key for normal mentions", () => { + const mentions: Mentions = [{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } }]; + expect(stripBotMention("@Bot hello @_bot_1", mentions)).toBe("hello"); + }); + + it("treats mention.name regex metacharacters as literal text", () => { + const mentions: Mentions = [{ key: "@_bot_1", name: ".*", id: { open_id: "ou_bot" } }]; + expect(stripBotMention("@NotBot hello", mentions)).toBe("@NotBot hello"); + }); + + it("treats mention.key regex metacharacters as literal text", () => { + const mentions: Mentions = [{ key: ".*", name: "Bot", id: { open_id: "ou_bot" } }]; + expect(stripBotMention("hello world", mentions)).toBe("hello world"); + }); + + it("trims once after all mention replacements", () => { + const mentions: Mentions = [{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } }]; + expect(stripBotMention(" @_bot_1 hello ", mentions)).toBe("hello"); + }); + + it("strips multiple mentions in one pass", () => { + const mentions: Mentions = [ + { key: "@_bot_1", name: "Bot One", id: { open_id: "ou_bot_1" } }, + { key: "@_bot_2", name: "Bot Two", id: { open_id: "ou_bot_2" } }, + ]; + expect(stripBotMention("@Bot One @_bot_1 hi @Bot Two @_bot_2", mentions)).toBe("hi"); + }); +}); diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 8ac28430a0..1a534ed40c 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -7,13 +7,20 @@ import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry, } from "openclaw/plugin-sdk"; +import type { FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } from "./types.js"; +import type { DynamicAgentCreationConfig } from "./types.js"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { tryRecordMessage } from "./dedup.js"; import { maybeCreateDynamicAgent } from "./dynamic-agent.js"; import { normalizeFeishuExternalKey } from "./external-keys.js"; import { downloadMessageResourceFeishu } from "./media.js"; -import { extractMentionTargets, extractMessageBody, isMentionForwardRequest } from "./mention.js"; +import { + escapeRegExp, + extractMentionTargets, + extractMessageBody, + isMentionForwardRequest, +} from "./mention.js"; import { resolveFeishuGroupConfig, resolveFeishuReplyPolicy, @@ -23,8 +30,6 @@ import { import { createFeishuReplyDispatcher } from "./reply-dispatcher.js"; import { getFeishuRuntime } from "./runtime.js"; import { getMessageFeishu, sendMessageFeishu } from "./send.js"; -import type { FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } from "./types.js"; -import type { DynamicAgentCreationConfig } from "./types.js"; // --- Permission error extraction --- // Extract permission grant URL from Feishu API error response. @@ -199,21 +204,17 @@ function checkBotMentioned(event: FeishuMessageEvent, botOpenId?: string): boole return false; } -function escapeRegExp(s: string): string { - return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -function stripBotMention( +export function stripBotMention( text: string, mentions?: FeishuMessageEvent["message"]["mentions"], ): string { if (!mentions || mentions.length === 0) return text; let result = text; for (const mention of mentions) { - result = result.replace(new RegExp(`@${escapeRegExp(mention.name)}\\s*`, "g"), "").trim(); - result = result.replace(new RegExp(escapeRegExp(mention.key), "g"), "").trim(); + result = result.replace(new RegExp(`@${escapeRegExp(mention.name)}\\s*`, "g"), ""); + result = result.replace(new RegExp(escapeRegExp(mention.key), "g"), ""); } - return result; + return result.trim(); } /** diff --git a/extensions/feishu/src/mention.ts b/extensions/feishu/src/mention.ts index 1b7acb85d1..50c6fae5ed 100644 --- a/extensions/feishu/src/mention.ts +++ b/extensions/feishu/src/mention.ts @@ -1,5 +1,12 @@ import type { FeishuMessageEvent } from "./bot.js"; +/** + * Escape regex metacharacters so user-controlled mention fields are treated literally. + */ +export function escapeRegExp(input: string): string { + return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + /** * Mention target user info */ @@ -67,7 +74,7 @@ export function extractMessageBody(text: string, allMentionKeys: string[]): stri // Remove all @ placeholders for (const key of allMentionKeys) { - result = result.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), ""); + result = result.replace(new RegExp(escapeRegExp(key), "g"), ""); } return result.replace(/\s+/g, " ").trim();