fix(slack): detect control commands when message starts with @mention (#14142)

Merged via /review-pr-v2 -> /prepare-pr-v2 -> /merge-pr-v2.

Prepared head SHA: cb0b4f6a3b
Co-authored-by: beefiker <55247450+beefiker@users.noreply.github.com>
Co-authored-by: gumadeiras <gumadeiras@gmail.com>
Reviewed-by: @gumadeiras
This commit is contained in:
J young Lee
2026-02-12 01:41:48 +09:00
committed by GitHub
parent 50a60b8be6
commit 2aa9570465
5 changed files with 94 additions and 2 deletions

View File

@@ -76,4 +76,79 @@ describe("prepareSlackMessage sender prefix", () => {
const body = result?.ctxPayload.Body ?? "";
expect(body).toContain("Alice (U1): <@BOT> hello");
});
it("detects /new as control command when prefixed with Slack mention", async () => {
const ctx = {
cfg: {
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
channels: { slack: { dm: { enabled: true, policy: "open", allowFrom: ["*"] } } },
},
accountId: "default",
botToken: "xoxb",
app: { client: {} },
runtime: {
log: vi.fn(),
error: vi.fn(),
exit: (code: number): never => {
throw new Error(`exit ${code}`);
},
},
botUserId: "BOT",
teamId: "T1",
apiAppId: "A1",
historyLimit: 0,
channelHistories: new Map(),
sessionScope: "per-sender",
mainKey: "agent:main:main",
dmEnabled: true,
dmPolicy: "open",
allowFrom: ["U1"],
groupDmEnabled: false,
groupDmChannels: [],
defaultRequireMention: true,
groupPolicy: "open",
useAccessGroups: true,
reactionMode: "off",
reactionAllowlist: [],
replyToMode: "off",
threadHistoryScope: "channel",
threadInheritParent: false,
slashCommand: {
enabled: false,
name: "openclaw",
sessionPrefix: "slack:slash",
ephemeral: true,
},
textLimit: 2000,
ackReactionScope: "off",
mediaMaxBytes: 1000,
removeAckAfterReply: false,
logger: { info: vi.fn() },
markMessageSeen: () => false,
shouldDropMismatchedSlackEvent: () => false,
resolveSlackSystemEventSessionKey: () => "agent:main:slack:channel:c1",
isChannelAllowed: () => true,
resolveChannelName: async () => ({ name: "general", type: "channel" }),
resolveUserName: async () => ({ name: "Alice" }),
setSlackThreadStatus: async () => undefined,
} satisfies SlackMonitorContext;
const result = await prepareSlackMessage({
ctx,
account: { accountId: "default", config: {} } as never,
message: {
type: "message",
channel: "C1",
channel_type: "channel",
text: "<@BOT> /new",
user: "U1",
ts: "1700000000.0002",
event_ts: "1700000000.0002",
} as never,
opts: { source: "message", wasMentioned: true },
});
expect(result).not.toBeNull();
expect(result?.ctxPayload.CommandAuthorized).toBe(true);
});
});

View File

@@ -42,6 +42,7 @@ import { resolveSlackThreadContext } from "../../threading.js";
import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-list.js";
import { resolveSlackEffectiveAllowFrom } from "../auth.js";
import { resolveSlackChannelConfig } from "../channel-config.js";
import { stripSlackMentionsForCommandDetection } from "../commands.js";
import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js";
import { resolveSlackMedia, resolveSlackThreadStarter } from "../media.js";
@@ -249,7 +250,9 @@ export async function prepareSlackMessage(params: {
cfg,
surface: "slack",
});
const hasControlCommandInMessage = hasControlCommand(message.text ?? "", cfg);
// Strip Slack mentions (<@U123>) before command detection so "@Labrador /new" is recognized
const textForCommandDetection = stripSlackMentionsForCommandDetection(message.text ?? "");
const hasControlCommandInMessage = hasControlCommand(textForCommandDetection, cfg);
const ownerAuthorized = resolveSlackAllowListMatch({
allowList: allowFromLower,