Files
openclaw/src/auto-reply/command-detection.ts
HansY 3b1f8e3461 fix: strip inbound metadata before slash command detection (#58674)
Slash commands like /model and /new were silently ignored when the inbound
message body included metadata prefix blocks (Conversation info, Sender info,
timestamps) injected by buildInboundUserContextPrefix. The command detection
functions (hasControlCommand, isControlCommandMessage, parseSendPolicyCommand)
now call stripInboundMetadata before normalizeCommandBody so embedded slash
commands are correctly recognized.
2026-04-01 10:17:20 +01:00

95 lines
2.6 KiB
TypeScript

import type { OpenClawConfig } from "../config/types.js";
import {
type CommandNormalizeOptions,
listChatCommands,
listChatCommandsForConfig,
normalizeCommandBody,
} from "./commands-registry.js";
import { isAbortTrigger } from "./reply/abort-primitives.js";
import { stripInboundMetadata } from "./reply/strip-inbound-meta.js";
export function hasControlCommand(
text?: string,
cfg?: OpenClawConfig,
options?: CommandNormalizeOptions,
): boolean {
if (!text) {
return false;
}
const trimmed = text.trim();
if (!trimmed) {
return false;
}
const stripped = stripInboundMetadata(trimmed);
if (!stripped) {
return false;
}
const normalizedBody = normalizeCommandBody(stripped, options);
if (!normalizedBody) {
return false;
}
const lowered = normalizedBody.toLowerCase();
const commands = cfg ? listChatCommandsForConfig(cfg) : listChatCommands();
for (const command of commands) {
for (const alias of command.textAliases) {
const normalized = alias.trim().toLowerCase();
if (!normalized) {
continue;
}
if (lowered === normalized) {
return true;
}
if (command.acceptsArgs && lowered.startsWith(normalized)) {
const nextChar = normalizedBody.charAt(normalized.length);
if (nextChar && /\s/.test(nextChar)) {
return true;
}
}
}
}
return false;
}
export function isControlCommandMessage(
text?: string,
cfg?: OpenClawConfig,
options?: CommandNormalizeOptions,
): boolean {
if (!text) {
return false;
}
const trimmed = text.trim();
if (!trimmed) {
return false;
}
if (hasControlCommand(trimmed, cfg, options)) {
return true;
}
const stripped = stripInboundMetadata(trimmed);
const normalized = normalizeCommandBody(stripped, options).trim().toLowerCase();
return isAbortTrigger(normalized);
}
/**
* Coarse detection for inline directives/shortcuts (e.g. "hey /status") so channel monitors
* can decide whether to compute CommandAuthorized for a message.
*
* This intentionally errs on the side of false positives; CommandAuthorized only gates
* command/directive execution, not normal chat replies.
*/
export function hasInlineCommandTokens(text?: string): boolean {
const body = text ?? "";
if (!body.trim()) {
return false;
}
return /(?:^|\s)[/!][a-z]/i.test(body);
}
export function shouldComputeCommandAuthorized(
text?: string,
cfg?: OpenClawConfig,
options?: CommandNormalizeOptions,
): boolean {
return isControlCommandMessage(text, cfg, options) || hasInlineCommandTokens(text);
}