mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:38:38 +00:00
fix: truncate skill command descriptions to 100 chars for Discord (#1018)
* fix: truncate skill command descriptions to 100 chars for Discord Discord slash commands have a 100 character limit for descriptions. Skill descriptions were not being truncated, causing command registration to fail with an empty error from the Discord API. * style: format * style: format
This commit is contained in:
@@ -50,7 +50,7 @@ test("background exec still times out after tool signal abort", async () => {
|
|||||||
const result = await tool.execute(
|
const result = await tool.execute(
|
||||||
"toolcall",
|
"toolcall",
|
||||||
{
|
{
|
||||||
command: "node -e \"setTimeout(() => {}, 5000)\"",
|
command: 'node -e "setTimeout(() => {}, 5000)"',
|
||||||
background: true,
|
background: true,
|
||||||
timeout: 0.2,
|
timeout: 0.2,
|
||||||
},
|
},
|
||||||
@@ -85,7 +85,7 @@ test("yielded background exec is not killed when tool signal aborts", async () =
|
|||||||
|
|
||||||
const result = await tool.execute(
|
const result = await tool.execute(
|
||||||
"toolcall",
|
"toolcall",
|
||||||
{ command: "node -e \"setTimeout(() => {}, 5000)\"", yieldMs: 5 },
|
{ command: 'node -e "setTimeout(() => {}, 5000)"', yieldMs: 5 },
|
||||||
abortController.signal,
|
abortController.signal,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ test("yielded background exec still times out", async () => {
|
|||||||
const tool = createExecTool({ allowBackground: true, backgroundMs: 10 });
|
const tool = createExecTool({ allowBackground: true, backgroundMs: 10 });
|
||||||
|
|
||||||
const result = await tool.execute("toolcall", {
|
const result = await tool.execute("toolcall", {
|
||||||
command: "node -e \"setTimeout(() => {}, 5000)\"",
|
command: 'node -e "setTimeout(() => {}, 5000)"',
|
||||||
yieldMs: 5,
|
yieldMs: 5,
|
||||||
timeout: 0.2,
|
timeout: 0.2,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,4 +61,32 @@ describe("buildWorkspaceSkillCommandSpecs", () => {
|
|||||||
expect(names).toEqual(["hello_world", "hello_world_2", "help_2"]);
|
expect(names).toEqual(["hello_world", "hello_world_2", "help_2"]);
|
||||||
expect(commands.find((entry) => entry.skillName === "hidden-skill")).toBeUndefined();
|
expect(commands.find((entry) => entry.skillName === "hidden-skill")).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("truncates descriptions longer than 100 characters for Discord compatibility", async () => {
|
||||||
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
|
||||||
|
const longDescription =
|
||||||
|
"This is a very long description that exceeds Discord's 100 character limit for slash command descriptions and should be truncated";
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(workspaceDir, "skills", "long-desc"),
|
||||||
|
name: "long-desc",
|
||||||
|
description: longDescription,
|
||||||
|
});
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(workspaceDir, "skills", "short-desc"),
|
||||||
|
name: "short-desc",
|
||||||
|
description: "Short description",
|
||||||
|
});
|
||||||
|
|
||||||
|
const commands = buildWorkspaceSkillCommandSpecs(workspaceDir, {
|
||||||
|
managedSkillsDir: path.join(workspaceDir, ".managed"),
|
||||||
|
bundledSkillsDir: path.join(workspaceDir, ".bundled"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const longCmd = commands.find((entry) => entry.skillName === "long-desc");
|
||||||
|
const shortCmd = commands.find((entry) => entry.skillName === "short-desc");
|
||||||
|
|
||||||
|
expect(longCmd?.description.length).toBeLessThanOrEqual(100);
|
||||||
|
expect(longCmd?.description.endsWith("…")).toBe(true);
|
||||||
|
expect(shortCmd?.description).toBe("Short description");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -87,12 +87,7 @@ function parseFrontmatterBool(value: string | undefined, fallback: boolean): boo
|
|||||||
if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
|
if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (
|
if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off") {
|
||||||
normalized === "false" ||
|
|
||||||
normalized === "0" ||
|
|
||||||
normalized === "no" ||
|
|
||||||
normalized === "off"
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return fallback;
|
return fallback;
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ function filterSkillEntries(
|
|||||||
|
|
||||||
const SKILL_COMMAND_MAX_LENGTH = 32;
|
const SKILL_COMMAND_MAX_LENGTH = 32;
|
||||||
const SKILL_COMMAND_FALLBACK = "skill";
|
const SKILL_COMMAND_FALLBACK = "skill";
|
||||||
|
// Discord command descriptions must be ≤100 characters
|
||||||
|
const SKILL_COMMAND_DESCRIPTION_MAX_LENGTH = 100;
|
||||||
|
|
||||||
function sanitizeSkillCommandName(raw: string): string {
|
function sanitizeSkillCommandName(raw: string): string {
|
||||||
const normalized = raw
|
const normalized = raw
|
||||||
@@ -311,9 +313,7 @@ export function buildWorkspaceSkillCommandSpecs(
|
|||||||
opts?.skillFilter,
|
opts?.skillFilter,
|
||||||
opts?.eligibility,
|
opts?.eligibility,
|
||||||
);
|
);
|
||||||
const userInvocable = eligible.filter(
|
const userInvocable = eligible.filter((entry) => entry.invocation?.userInvocable !== false);
|
||||||
(entry) => entry.invocation?.userInvocable !== false,
|
|
||||||
);
|
|
||||||
const used = new Set<string>();
|
const used = new Set<string>();
|
||||||
for (const reserved of opts?.reservedNames ?? []) {
|
for (const reserved of opts?.reservedNames ?? []) {
|
||||||
used.add(reserved.toLowerCase());
|
used.add(reserved.toLowerCase());
|
||||||
@@ -324,10 +324,15 @@ export function buildWorkspaceSkillCommandSpecs(
|
|||||||
const base = sanitizeSkillCommandName(entry.skill.name);
|
const base = sanitizeSkillCommandName(entry.skill.name);
|
||||||
const unique = resolveUniqueSkillCommandName(base, used);
|
const unique = resolveUniqueSkillCommandName(base, used);
|
||||||
used.add(unique.toLowerCase());
|
used.add(unique.toLowerCase());
|
||||||
|
const rawDescription = entry.skill.description?.trim() || entry.skill.name;
|
||||||
|
const description =
|
||||||
|
rawDescription.length > SKILL_COMMAND_DESCRIPTION_MAX_LENGTH
|
||||||
|
? rawDescription.slice(0, SKILL_COMMAND_DESCRIPTION_MAX_LENGTH - 1) + "…"
|
||||||
|
: rawDescription;
|
||||||
specs.push({
|
specs.push({
|
||||||
name: unique,
|
name: unique,
|
||||||
skillName: entry.skill.name,
|
skillName: entry.skill.name,
|
||||||
description: entry.skill.description?.trim() || entry.skill.name,
|
description,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return specs;
|
return specs;
|
||||||
|
|||||||
@@ -62,9 +62,7 @@ function escapeRegExp(value: string) {
|
|||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSkillCommandDefinitions(
|
function buildSkillCommandDefinitions(skillCommands?: SkillCommandSpec[]): ChatCommandDefinition[] {
|
||||||
skillCommands?: SkillCommandSpec[],
|
|
||||||
): ChatCommandDefinition[] {
|
|
||||||
if (!skillCommands || skillCommands.length === 0) return [];
|
if (!skillCommands || skillCommands.length === 0) return [];
|
||||||
return skillCommands.map((spec) => ({
|
return skillCommands.map((spec) => ({
|
||||||
key: `skill:${spec.skillName}`,
|
key: `skill:${spec.skillName}`,
|
||||||
@@ -77,7 +75,9 @@ function buildSkillCommandDefinitions(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listChatCommands(params?: { skillCommands?: SkillCommandSpec[] }): ChatCommandDefinition[] {
|
export function listChatCommands(params?: {
|
||||||
|
skillCommands?: SkillCommandSpec[];
|
||||||
|
}): ChatCommandDefinition[] {
|
||||||
if (!params?.skillCommands?.length) return [...CHAT_COMMANDS];
|
if (!params?.skillCommands?.length) return [...CHAT_COMMANDS];
|
||||||
return [...CHAT_COMMANDS, ...buildSkillCommandDefinitions(params.skillCommands)];
|
return [...CHAT_COMMANDS, ...buildSkillCommandDefinitions(params.skillCommands)];
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,9 @@ export function listChatCommandsForConfig(
|
|||||||
return [...base, ...buildSkillCommandDefinitions(params.skillCommands)];
|
return [...base, ...buildSkillCommandDefinitions(params.skillCommands)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listNativeCommandSpecs(params?: { skillCommands?: SkillCommandSpec[] }): NativeCommandSpec[] {
|
export function listNativeCommandSpecs(params?: {
|
||||||
|
skillCommands?: SkillCommandSpec[];
|
||||||
|
}): NativeCommandSpec[] {
|
||||||
return listChatCommands({ skillCommands: params?.skillCommands })
|
return listChatCommands({ skillCommands: params?.skillCommands })
|
||||||
.filter((command) => command.scope !== "text" && command.nativeName)
|
.filter((command) => command.scope !== "text" && command.nativeName)
|
||||||
.map((command) => ({
|
.map((command) => ({
|
||||||
|
|||||||
@@ -187,34 +187,34 @@ export async function handleInlineActions(params: {
|
|||||||
commandBodyNormalized: inlineCommand.command,
|
commandBodyNormalized: inlineCommand.command,
|
||||||
};
|
};
|
||||||
const inlineResult = await handleCommands({
|
const inlineResult = await handleCommands({
|
||||||
ctx,
|
ctx,
|
||||||
cfg,
|
cfg,
|
||||||
command: inlineCommandContext,
|
command: inlineCommandContext,
|
||||||
agentId,
|
agentId,
|
||||||
directives,
|
directives,
|
||||||
elevated: {
|
elevated: {
|
||||||
enabled: elevatedEnabled,
|
enabled: elevatedEnabled,
|
||||||
allowed: elevatedAllowed,
|
allowed: elevatedAllowed,
|
||||||
failures: elevatedFailures,
|
failures: elevatedFailures,
|
||||||
},
|
},
|
||||||
sessionEntry,
|
sessionEntry,
|
||||||
sessionStore,
|
sessionStore,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
storePath,
|
storePath,
|
||||||
sessionScope,
|
sessionScope,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
defaultGroupActivation: defaultActivation,
|
defaultGroupActivation: defaultActivation,
|
||||||
resolvedThinkLevel,
|
resolvedThinkLevel,
|
||||||
resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
|
resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
|
||||||
resolvedReasoningLevel,
|
resolvedReasoningLevel,
|
||||||
resolvedElevatedLevel,
|
resolvedElevatedLevel,
|
||||||
resolveDefaultThinkingLevel,
|
resolveDefaultThinkingLevel,
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
contextTokens,
|
contextTokens,
|
||||||
isGroup,
|
isGroup,
|
||||||
skillCommands,
|
skillCommands,
|
||||||
});
|
});
|
||||||
if (inlineResult.reply) {
|
if (inlineResult.reply) {
|
||||||
if (!inlineCommand.cleaned) {
|
if (!inlineCommand.cleaned) {
|
||||||
typing.cleanup();
|
typing.cleanup();
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ describe("resolveSkillCommandInvocation", () => {
|
|||||||
it("matches skill commands and parses args", () => {
|
it("matches skill commands and parses args", () => {
|
||||||
const invocation = resolveSkillCommandInvocation({
|
const invocation = resolveSkillCommandInvocation({
|
||||||
commandBodyNormalized: "/demo_skill do the thing",
|
commandBodyNormalized: "/demo_skill do the thing",
|
||||||
skillCommands: [
|
skillCommands: [{ name: "demo_skill", skillName: "demo-skill", description: "Demo" }],
|
||||||
{ name: "demo_skill", skillName: "demo-skill", description: "Demo" },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
expect(invocation?.command.skillName).toBe("demo-skill");
|
expect(invocation?.command.skillName).toBe("demo-skill");
|
||||||
expect(invocation?.args).toBe("do the thing");
|
expect(invocation?.args).toBe("do the thing");
|
||||||
@@ -16,9 +14,7 @@ describe("resolveSkillCommandInvocation", () => {
|
|||||||
it("returns null for unknown commands", () => {
|
it("returns null for unknown commands", () => {
|
||||||
const invocation = resolveSkillCommandInvocation({
|
const invocation = resolveSkillCommandInvocation({
|
||||||
commandBodyNormalized: "/unknown arg",
|
commandBodyNormalized: "/unknown arg",
|
||||||
skillCommands: [
|
skillCommands: [{ name: "demo_skill", skillName: "demo-skill", description: "Demo" }],
|
||||||
{ name: "demo_skill", skillName: "demo-skill", description: "Demo" },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
expect(invocation).toBeNull();
|
expect(invocation).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
||||||
import {
|
import { buildWorkspaceSkillCommandSpecs, type SkillCommandSpec } from "../agents/skills.js";
|
||||||
buildWorkspaceSkillCommandSpecs,
|
|
||||||
type SkillCommandSpec,
|
|
||||||
} from "../agents/skills.js";
|
|
||||||
import { listChatCommands } from "./commands-registry.js";
|
import { listChatCommands } from "./commands-registry.js";
|
||||||
|
|
||||||
function resolveReservedCommandNames(): Set<string> {
|
function resolveReservedCommandNames(): Set<string> {
|
||||||
@@ -42,9 +39,7 @@ export function resolveSkillCommandInvocation(params: {
|
|||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
const commandName = match[1]?.trim().toLowerCase();
|
const commandName = match[1]?.trim().toLowerCase();
|
||||||
if (!commandName) return null;
|
if (!commandName) return null;
|
||||||
const command = params.skillCommands.find(
|
const command = params.skillCommands.find((entry) => entry.name.toLowerCase() === commandName);
|
||||||
(entry) => entry.name.toLowerCase() === commandName,
|
|
||||||
);
|
|
||||||
if (!command) return null;
|
if (!command) return null;
|
||||||
const args = match[2]?.trim();
|
const args = match[2]?.trim();
|
||||||
return { command, args: args || undefined };
|
return { command, args: args || undefined };
|
||||||
|
|||||||
@@ -211,9 +211,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
defaultRuntime.log(
|
defaultRuntime.log(
|
||||||
theme.muted(
|
theme.muted("Examples: `npm i -g clawdbot@latest` or `pnpm add -g clawdbot@latest`"),
|
||||||
"Examples: `npm i -g clawdbot@latest` or `pnpm add -g clawdbot@latest`",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
defaultRuntime.exit(0);
|
defaultRuntime.exit(0);
|
||||||
|
|||||||
@@ -124,9 +124,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
cfg,
|
cfg,
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const commandSpecs = nativeEnabled
|
const commandSpecs = nativeEnabled ? listNativeCommandSpecsForConfig(cfg, { skillCommands }) : [];
|
||||||
? listNativeCommandSpecsForConfig(cfg, { skillCommands })
|
|
||||||
: [];
|
|
||||||
const commands = commandSpecs.map((spec) =>
|
const commands = commandSpecs.map((spec) =>
|
||||||
createDiscordNativeCommand({
|
createDiscordNativeCommand({
|
||||||
command: spec,
|
command: spec,
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export async function processMessage(params: {
|
|||||||
const responsePrefix =
|
const responsePrefix =
|
||||||
resolvedMessages.responsePrefix ??
|
resolvedMessages.responsePrefix ??
|
||||||
(configuredResponsePrefix === undefined && isSelfChat
|
(configuredResponsePrefix === undefined && isSelfChat
|
||||||
? resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[clawdbot]"
|
? (resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[clawdbot]")
|
||||||
: undefined);
|
: undefined);
|
||||||
|
|
||||||
// Create mutable context for response prefix template interpolation
|
// Create mutable context for response prefix template interpolation
|
||||||
|
|||||||
Reference in New Issue
Block a user