fix(mattermost): export skill-commands via plugin-sdk + thread triggerMap for accurate command name resolution

- Export listSkillCommandsForAgents and SkillCommandSpec from plugin-sdk/index.ts
  (removes deep relative import in monitor.ts)
- Add originalName field to MattermostCommandSpec for preserving pre-prefix names
- Build trigger→originalName map in monitor.ts, thread through slash-state → slash-http
- resolveCommandText() now uses triggerMap for accurate name lookup
  (oc_report → /oc_report correctly, not /report)
This commit is contained in:
Echo 2026-02-15 03:06:39 -05:00 committed by Muhammed Mukhthar CM
parent 81087ecb6b
commit 5bbe16f2ce
5 changed files with 40 additions and 8 deletions

View File

@ -25,6 +25,7 @@ import {
resolveDefaultGroupPolicy,
resolveChannelMediaMaxBytes,
warnMissingProviderGroupPolicyFallbackOnce,
listSkillCommandsForAgents,
type HistoryEntry,
} from "openclaw/plugin-sdk";
import { getMattermostRuntime } from "../runtime.js";
@ -258,8 +259,6 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
if (slashConfig.nativeSkills === true) {
try {
const { listSkillCommandsForAgents } =
await import("../../../../src/auto-reply/skill-commands.js");
const skillCommands = listSkillCommandsForAgents({ cfg: cfg as any });
for (const spec of skillCommands) {
const name = typeof spec.name === "string" ? spec.name.trim() : "";
@ -270,6 +269,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
description: spec.description || `Run skill ${name}`,
autoComplete: true,
autoCompleteHint: "[args]",
originalName: name,
});
}
} catch (err) {
@ -300,10 +300,19 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
allRegistered.push(...registered);
}
// Build trigger→originalName map for accurate command name resolution
const triggerMap = new Map<string, string>();
for (const cmd of dedupedCommands) {
if (cmd.originalName) {
triggerMap.set(cmd.trigger, cmd.originalName);
}
}
activateSlashCommands({
account,
commandTokens: allRegistered.map((cmd) => cmd.token).filter(Boolean),
registeredCommands: allRegistered,
triggerMap,
api: { cfg, runtime },
log: (msg) => runtime.log?.(msg),
});

View File

@ -35,6 +35,8 @@ export type MattermostCommandSpec = {
description: string;
autoComplete: boolean;
autoCompleteHint?: string;
/** Original command name (for skill commands that start with oc_) */
originalName?: string;
};
export type MattermostRegisteredCommand = {
@ -128,39 +130,46 @@ type MattermostCommandResponse = {
export const DEFAULT_COMMAND_SPECS: MattermostCommandSpec[] = [
{
trigger: "oc_status",
originalName: "status",
description: "Show session status (model, usage, uptime)",
autoComplete: true,
},
{
trigger: "oc_model",
originalName: "model",
description: "View or change the current model",
autoComplete: true,
autoCompleteHint: "[model-name]",
},
{
trigger: "oc_new",
originalName: "new",
description: "Start a new conversation session",
autoComplete: true,
},
{
trigger: "oc_help",
originalName: "help",
description: "Show available commands",
autoComplete: true,
},
{
trigger: "oc_think",
originalName: "think",
description: "Set thinking/reasoning level",
autoComplete: true,
autoCompleteHint: "[off|low|medium|high]",
},
{
trigger: "oc_reasoning",
originalName: "reasoning",
description: "Toggle reasoning mode",
autoComplete: true,
autoCompleteHint: "[on|off]",
},
{
trigger: "oc_verbose",
originalName: "verbose",
description: "Toggle verbose mode",
autoComplete: true,
autoCompleteHint: "[on|off]",
@ -435,9 +444,14 @@ export function parseSlashCommandPayload(
* Map the trigger word back to the original OpenClaw command name.
* e.g. "oc_status" -> "/status", "oc_model" -> "/model"
*/
export function resolveCommandText(trigger: string, text: string): string {
// Strip the "oc_" prefix to get the original command name
const commandName = trigger.startsWith("oc_") ? trigger.slice(3) : trigger;
export function resolveCommandText(
trigger: string,
text: string,
triggerMap?: ReadonlyMap<string, string>,
): string {
// Use the trigger map if available for accurate name resolution
const commandName =
triggerMap?.get(trigger) ?? (trigger.startsWith("oc_") ? trigger.slice(3) : trigger);
const args = text.trim();
return args ? `/${commandName} ${args}` : `/${commandName}`;
}

View File

@ -36,6 +36,8 @@ type SlashHttpHandlerParams = {
runtime: RuntimeEnv;
/** Expected token from registered commands (for validation). */
commandTokens: Set<string>;
/** Map from trigger to original command name (for skill commands that start with oc_). */
triggerMap?: ReadonlyMap<string, string>;
log?: (msg: string) => void;
};
@ -361,7 +363,7 @@ async function authorizeSlashInvocation(params: {
* from the Mattermost server when a user invokes a registered slash command.
*/
export function createSlashCommandHttpHandler(params: SlashHttpHandlerParams) {
const { account, cfg, runtime, commandTokens, log } = params;
const { account, cfg, runtime, commandTokens, triggerMap, log } = params;
const MAX_BODY_BYTES = 64 * 1024; // 64KB
@ -404,7 +406,7 @@ export function createSlashCommandHttpHandler(params: SlashHttpHandlerParams) {
// Extract command info
const trigger = payload.command.replace(/^\//, "").trim();
const commandText = resolveCommandText(trigger, payload.text);
const commandText = resolveCommandText(trigger, payload.text, triggerMap);
const channelId = payload.channel_id;
const senderId = payload.user_id;
const senderName = payload.user_name ?? senderId;

View File

@ -26,6 +26,8 @@ type SlashCommandAccountState = {
handler: ((req: IncomingMessage, res: ServerResponse) => Promise<void>) | null;
/** The account that activated slash commands. */
account: ResolvedMattermostAccount;
/** Map from trigger to original command name (for skill commands that start with oc_). */
triggerMap: Map<string, string>;
};
/** Map from accountId → per-account slash command state. */
@ -53,13 +55,14 @@ export function activateSlashCommands(params: {
account: ResolvedMattermostAccount;
commandTokens: string[];
registeredCommands: MattermostRegisteredCommand[];
triggerMap?: Map<string, string>;
api: {
cfg: import("openclaw/plugin-sdk").OpenClawConfig;
runtime: import("openclaw/plugin-sdk").RuntimeEnv;
};
log?: (msg: string) => void;
}) {
const { account, commandTokens, registeredCommands, api, log } = params;
const { account, commandTokens, registeredCommands, triggerMap, api, log } = params;
const accountId = account.accountId;
const tokenSet = new Set(commandTokens);
@ -69,6 +72,7 @@ export function activateSlashCommands(params: {
cfg: api.cfg,
runtime: api.runtime,
commandTokens: tokenSet,
triggerMap,
log,
});
@ -77,6 +81,7 @@ export function activateSlashCommands(params: {
registeredCommands,
handler,
account,
triggerMap: triggerMap ?? new Map(),
});
log?.(

View File

@ -543,6 +543,8 @@ export type {
} from "../infra/diagnostic-events.js";
export { detectMime, extensionForMime, getFileExtension } from "../media/mime.js";
export { extractOriginalFilename } from "../media/store.js";
export { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
export type { SkillCommandSpec } from "../agents/skills.js";
// Channel: Discord
export {