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
This commit is contained in:
nightq
2026-04-18 21:11:55 +08:00
committed by Gustavo Madeira Santana
parent efc19f0ddb
commit f371453ec2
2 changed files with 133 additions and 1 deletions

View File

@@ -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");
});
});

View File

@@ -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({