mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:38:38 +00:00
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:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- Ollama: use configured `models.providers.ollama.baseUrl` for model discovery and normalize `/v1` endpoints to the native Ollama API root. (#14131) Thanks @shtse8.
|
- Ollama: use configured `models.providers.ollama.baseUrl` for model discovery and normalize `/v1` endpoints to the native Ollama API root. (#14131) Thanks @shtse8.
|
||||||
|
- Slack: detect control commands when channel messages start with bot mention prefixes (for example, `@Bot /new`). (#14142) Thanks @beefiker.
|
||||||
|
|
||||||
## 2026.2.9
|
## 2026.2.9
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import type { SlackSlashCommandConfig } from "../../config/config.js";
|
import type { SlackSlashCommandConfig } from "../../config/config.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip Slack mentions (<@U123>, <@U123|name>) so command detection works on
|
||||||
|
* normalized text. Use in both prepare and debounce gate for consistency.
|
||||||
|
*/
|
||||||
|
export function stripSlackMentionsForCommandDetection(text: string): string {
|
||||||
|
return (text ?? "")
|
||||||
|
.replace(/<@[^>]+>/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeSlackSlashCommandName(raw: string) {
|
export function normalizeSlackSlashCommandName(raw: string) {
|
||||||
return raw.replace(/^\/+/, "");
|
return raw.replace(/^\/+/, "");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
createInboundDebouncer,
|
createInboundDebouncer,
|
||||||
resolveInboundDebounceMs,
|
resolveInboundDebounceMs,
|
||||||
} from "../../auto-reply/inbound-debounce.js";
|
} from "../../auto-reply/inbound-debounce.js";
|
||||||
|
import { stripSlackMentionsForCommandDetection } from "./commands.js";
|
||||||
import { dispatchPreparedSlackMessage } from "./message-handler/dispatch.js";
|
import { dispatchPreparedSlackMessage } from "./message-handler/dispatch.js";
|
||||||
import { prepareSlackMessage } from "./message-handler/prepare.js";
|
import { prepareSlackMessage } from "./message-handler/prepare.js";
|
||||||
import { createSlackThreadTsResolver } from "./thread-resolution.js";
|
import { createSlackThreadTsResolver } from "./thread-resolution.js";
|
||||||
@@ -50,7 +51,8 @@ export function createSlackMessageHandler(params: {
|
|||||||
if (entry.message.files && entry.message.files.length > 0) {
|
if (entry.message.files && entry.message.files.length > 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !hasControlCommand(text, ctx.cfg);
|
const textForCommandDetection = stripSlackMentionsForCommandDetection(text);
|
||||||
|
return !hasControlCommand(textForCommandDetection, ctx.cfg);
|
||||||
},
|
},
|
||||||
onFlush: async (entries) => {
|
onFlush: async (entries) => {
|
||||||
const last = entries.at(-1);
|
const last = entries.at(-1);
|
||||||
|
|||||||
@@ -76,4 +76,79 @@ describe("prepareSlackMessage sender prefix", () => {
|
|||||||
const body = result?.ctxPayload.Body ?? "";
|
const body = result?.ctxPayload.Body ?? "";
|
||||||
expect(body).toContain("Alice (U1): <@BOT> hello");
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { resolveSlackThreadContext } from "../../threading.js";
|
|||||||
import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-list.js";
|
import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-list.js";
|
||||||
import { resolveSlackEffectiveAllowFrom } from "../auth.js";
|
import { resolveSlackEffectiveAllowFrom } from "../auth.js";
|
||||||
import { resolveSlackChannelConfig } from "../channel-config.js";
|
import { resolveSlackChannelConfig } from "../channel-config.js";
|
||||||
|
import { stripSlackMentionsForCommandDetection } from "../commands.js";
|
||||||
import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js";
|
import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js";
|
||||||
import { resolveSlackMedia, resolveSlackThreadStarter } from "../media.js";
|
import { resolveSlackMedia, resolveSlackThreadStarter } from "../media.js";
|
||||||
|
|
||||||
@@ -249,7 +250,9 @@ export async function prepareSlackMessage(params: {
|
|||||||
cfg,
|
cfg,
|
||||||
surface: "slack",
|
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({
|
const ownerAuthorized = resolveSlackAllowListMatch({
|
||||||
allowList: allowFromLower,
|
allowList: allowFromLower,
|
||||||
|
|||||||
Reference in New Issue
Block a user