From f371453ec2483502b21673e422dce43157be4f95 Mon Sep 17 00:00:00 2001 From: nightq Date: Sat, 18 Apr 2026 21:11:55 +0800 Subject: [PATCH] fix(matrix): strip mention prefix before slash command matching Fixes openclaw#68547 Root cause: When a user sends a message like `@bot:server /new` in a Matrix room, the slash command was not recognized because the mention prefix was not stripped before checking for slash commands via hasControlCommand(). Fix: Added stripMatrixMentionPrefixes() function that strips bot mention prefixes from the beginning of messages before passing text to hasControlCommand(). This follows the same pattern used by Feishu (normalizeMentions) and ensures that slash commands work correctly when the bot is explicitly mentioned. - Added stripMatrixMentionPrefixes() helper function in handler.ts - Applied stripping before hasControlCommand() call - Added comprehensive test coverage in handler.strip-mention.test.ts --- .../monitor/handler.strip-mention.test.ts | 105 ++++++++++++++++++ .../matrix/src/matrix/monitor/handler.ts | 29 ++++- 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 extensions/matrix/src/matrix/monitor/handler.strip-mention.test.ts diff --git a/extensions/matrix/src/matrix/monitor/handler.strip-mention.test.ts b/extensions/matrix/src/matrix/monitor/handler.strip-mention.test.ts new file mode 100644 index 00000000000..823b329f5cf --- /dev/null +++ b/extensions/matrix/src/matrix/monitor/handler.strip-mention.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from "vitest"; + +/** + * Test helper to strip mention prefixes from text for slash command detection. + * This is the test for the fix of issue #68547. + */ +function stripMatrixMentionPrefixes(text: string, mentionRegexes: RegExp[]): string { + if (!text || mentionRegexes.length === 0) { + return text; + } + let result = text; + for (const pattern of mentionRegexes) { + // Match mention at the start of the text, followed by optional whitespace + const match = result.match(new RegExp(`^(${pattern.source})\\s*`)); + if (match) { + result = result.slice(match[0].length).trimStart(); + break; // Only strip the first mention prefix + } + } + return result; +} + +describe("stripMatrixMentionPrefixes", () => { + it("returns original text when mentionRegexes is empty", () => { + const text = "@bot:server /new"; + const result = stripMatrixMentionPrefixes(text, []); + expect(result).toBe("@bot:server /new"); + }); + + it("returns original text when text is empty", () => { + const result = stripMatrixMentionPrefixes("", [/\s*@bot:server\s*/]); + expect(result).toBe(""); + }); + + it("strips mention prefix before slash command (issue #68547)", () => { + const mentionRegexes = [/@bot:server\b/]; + const text = "@bot:server /new"; + const result = stripMatrixMentionPrefixes(text, mentionRegexes); + expect(result).toBe("/new"); + }); + + it("strips mention prefix with extra whitespace", () => { + const mentionRegexes = [/@bot:server\b/]; + const text = "@bot:server /help"; + const result = stripMatrixMentionPrefixes(text, mentionRegexes); + expect(result).toBe("/help"); + }); + + it("strips mention prefix with display name", () => { + const mentionRegexes = [/@OpenClaw Bot\b/]; + const text = "@OpenClaw Bot /model"; + const result = stripMatrixMentionPrefixes(text, mentionRegexes); + expect(result).toBe("/model"); + }); + + it("does not strip mention from middle of text", () => { + const mentionRegexes = [/@bot:server\b/]; + const text = "Hello @bot:server how are you"; + const result = stripMatrixMentionPrefixes(text, mentionRegexes); + expect(result).toBe("Hello @bot:server how are you"); + }); + + it("does not strip non-matching patterns", () => { + const mentionRegexes = [/@otherbot:server\b/]; + const text = "@bot:server /new"; + const result = stripMatrixMentionPrefixes(text, mentionRegexes); + expect(result).toBe("@bot:server /new"); + }); + + it("strips only the first mention prefix", () => { + const mentionRegexes = [/@bot:server\b/]; + const text = "@bot:server @bot:server /new"; + const result = stripMatrixMentionPrefixes(text, mentionRegexes); + expect(result).toBe("@bot:server /new"); + }); + + it("handles multiple regex patterns and strips first match", () => { + const mentionRegexes = [/@otherbot:server\b/, /@bot:server\b/]; + const text = "@bot:server /new"; + const result = stripMatrixMentionPrefixes(text, mentionRegexes); + // First pattern doesn't match, second does + expect(result).toBe("/new"); + }); + + it("preserves original text when no patterns match", () => { + const mentionRegexes = [/@otherbot:server\b/, /@anotherbot:server\b/]; + const text = "@bot:server /new"; + const result = stripMatrixMentionPrefixes(text, mentionRegexes); + expect(result).toBe("@bot:server /new"); + }); + + it("handles regex with special characters in mention", () => { + const mentionRegexes = [/@bot\+123:server\.com\b/]; + const text = "@bot+123:server.com /status"; + const result = stripMatrixMentionPrefixes(text, mentionRegexes); + expect(result).toBe("/status"); + }); + + it("preserves regular message without slash command after stripping", () => { + const mentionRegexes = [/@bot:server\b/]; + const text = "@bot:server hello world"; + const result = stripMatrixMentionPrefixes(text, mentionRegexes); + expect(result).toBe("hello world"); + }); +}); diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 57792075810..94a770fbe70 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -268,6 +268,27 @@ function resolveMatrixMentionPrecheckText(params: { return ""; } +/** + * Strip mention prefixes from text for slash command detection. + * This ensures that messages like "@bot:server /new" are recognized as slash commands. + * Similar to Feishu's normalizeMentions and Mattermost's stripMentionPrefix. + */ +function stripMatrixMentionPrefixes(text: string, mentionRegexes: RegExp[]): string { + if (!text || mentionRegexes.length === 0) { + return text; + } + let result = text; + for (const pattern of mentionRegexes) { + // Match mention at the start of the text, followed by optional whitespace + const match = result.match(new RegExp(`^(${pattern.source})\\s*`)); + if (match) { + result = result.slice(match[0].length).trimStart(); + break; // Only strip the first mention prefix + } + } + return result; +} + function hasBundledMatrixReplacementRelation(event: MatrixRawEvent) { const relations = event.unsigned?.["m.relations"]; if (!relations || typeof relations !== "object") { @@ -935,8 +956,14 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam surface: "matrix", }); const useAccessGroups = cfg.commands?.useAccessGroups !== false; - const hasControlCommandInMessage = core.channel.text.hasControlCommand( + // Strip mention prefixes before checking for slash commands so that messages + // like "@bot:server /new" are recognized as slash commands (#68547) + const commandCheckText = stripMatrixMentionPrefixes( mentionPrecheckText, + agentMentionRegexes, + ); + const hasControlCommandInMessage = core.channel.text.hasControlCommand( + commandCheckText, cfg, ); const commandGate = resolveControlCommandGate({