mirror of https://github.com/openclaw/openclaw.git
fix(discord): avoid native plugin command collisions
This commit is contained in:
parent
4dcd930923
commit
be9ea991de
|
|
@ -283,6 +283,7 @@ Docs: https://docs.openclaw.ai
|
||||||
- Discord inbound listener non-blocking dispatch: make `MESSAGE_CREATE` listener handoff asynchronous (no per-listener queue blocking), so long runs no longer stall unrelated incoming events. (#39154) Thanks @yaseenkadlemakki.
|
- Discord inbound listener non-blocking dispatch: make `MESSAGE_CREATE` listener handoff asynchronous (no per-listener queue blocking), so long runs no longer stall unrelated incoming events. (#39154) Thanks @yaseenkadlemakki.
|
||||||
- Daemon/Windows PATH freeze fix: stop persisting install-time `PATH` snapshots into Scheduled Task scripts so runtime tool lookup follows current host PATH updates; also refresh local TUI history on silent local finals. (#39139) Thanks @Narcooo.
|
- Daemon/Windows PATH freeze fix: stop persisting install-time `PATH` snapshots into Scheduled Task scripts so runtime tool lookup follows current host PATH updates; also refresh local TUI history on silent local finals. (#39139) Thanks @Narcooo.
|
||||||
- Gateway/systemd service restart hardening: clear stale gateway listeners by explicit run-port before service bind, add restart stale-pid port-override support, tune systemd start/stop/exit handling, and disable detached child mode only in service-managed runtime so cgroup stop semantics clean up descendants reliably. (#38463) Thanks @spirittechie.
|
- Gateway/systemd service restart hardening: clear stale gateway listeners by explicit run-port before service bind, add restart stale-pid port-override support, tune systemd start/stop/exit handling, and disable detached child mode only in service-managed runtime so cgroup stop semantics clean up descendants reliably. (#38463) Thanks @spirittechie.
|
||||||
|
- Discord/plugin native command aliases: let plugins declare provider-specific slash names so native Discord registration can avoid built-in command collisions; the bundled Talk voice plugin now uses `/talkvoice` natively on Discord while keeping text `/voice`.
|
||||||
|
|
||||||
## 2026.3.2
|
## 2026.3.2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -863,6 +863,7 @@ Command handler context:
|
||||||
Command options:
|
Command options:
|
||||||
|
|
||||||
- `name`: Command name (without the leading `/`)
|
- `name`: Command name (without the leading `/`)
|
||||||
|
- `nativeNames`: Optional native-command aliases for slash/menu surfaces. Use `default` for all native providers, or provider-specific keys like `discord`
|
||||||
- `description`: Help text shown in command lists
|
- `description`: Help text shown in command lists
|
||||||
- `acceptsArgs`: Whether the command accepts arguments (default: false). If false and arguments are provided, the command won't match and the message falls through to other handlers
|
- `acceptsArgs`: Whether the command accepts arguments (default: false). If false and arguments are provided, the command won't match and the message falls through to other handlers
|
||||||
- `requireAuth`: Whether to require authorized sender (default: true)
|
- `requireAuth`: Whether to require authorized sender (default: true)
|
||||||
|
|
|
||||||
|
|
@ -77,12 +77,20 @@ function asTrimmedString(value: unknown): string {
|
||||||
return typeof value === "string" ? value.trim() : "";
|
return typeof value === "string" ? value.trim() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveCommandLabel(channel: string): string {
|
||||||
|
return channel === "discord" ? "/talkvoice" : "/voice";
|
||||||
|
}
|
||||||
|
|
||||||
export default function register(api: OpenClawPluginApi) {
|
export default function register(api: OpenClawPluginApi) {
|
||||||
api.registerCommand({
|
api.registerCommand({
|
||||||
name: "voice",
|
name: "voice",
|
||||||
|
nativeNames: {
|
||||||
|
discord: "talkvoice",
|
||||||
|
},
|
||||||
description: "List/set ElevenLabs Talk voice (affects iOS Talk playback).",
|
description: "List/set ElevenLabs Talk voice (affects iOS Talk playback).",
|
||||||
acceptsArgs: true,
|
acceptsArgs: true,
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
|
const commandLabel = resolveCommandLabel(ctx.channel);
|
||||||
const args = ctx.args?.trim() ?? "";
|
const args = ctx.args?.trim() ?? "";
|
||||||
const tokens = args.split(/\s+/).filter(Boolean);
|
const tokens = args.split(/\s+/).filter(Boolean);
|
||||||
const action = (tokens[0] ?? "status").toLowerCase();
|
const action = (tokens[0] ?? "status").toLowerCase();
|
||||||
|
|
@ -118,13 +126,13 @@ export default function register(api: OpenClawPluginApi) {
|
||||||
if (action === "set") {
|
if (action === "set") {
|
||||||
const query = tokens.slice(1).join(" ").trim();
|
const query = tokens.slice(1).join(" ").trim();
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return { text: "Usage: /voice set <voiceId|name>" };
|
return { text: `Usage: ${commandLabel} set <voiceId|name>` };
|
||||||
}
|
}
|
||||||
const voices = await listVoices(apiKey);
|
const voices = await listVoices(apiKey);
|
||||||
const chosen = findVoice(voices, query);
|
const chosen = findVoice(voices, query);
|
||||||
if (!chosen) {
|
if (!chosen) {
|
||||||
const hint = isLikelyVoiceId(query) ? query : `"${query}"`;
|
const hint = isLikelyVoiceId(query) ? query : `"${query}"`;
|
||||||
return { text: `No voice found for ${hint}. Try: /voice list` };
|
return { text: `No voice found for ${hint}. Try: ${commandLabel} list` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
|
@ -144,9 +152,9 @@ export default function register(api: OpenClawPluginApi) {
|
||||||
text: [
|
text: [
|
||||||
"Voice commands:",
|
"Voice commands:",
|
||||||
"",
|
"",
|
||||||
"/voice status",
|
`${commandLabel} status`,
|
||||||
"/voice list [limit]",
|
`${commandLabel} list [limit]`,
|
||||||
"/voice set <voiceId|name>",
|
`${commandLabel} set <voiceId|name>`,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -720,6 +720,7 @@ describe("monitorDiscordProvider", () => {
|
||||||
const commandNames = (createDiscordNativeCommandMock.mock.calls as Array<unknown[]>)
|
const commandNames = (createDiscordNativeCommandMock.mock.calls as Array<unknown[]>)
|
||||||
.map((call) => (call[0] as { command?: { name?: string } } | undefined)?.command?.name)
|
.map((call) => (call[0] as { command?: { name?: string } } | undefined)?.command?.name)
|
||||||
.filter((value): value is string => typeof value === "string");
|
.filter((value): value is string => typeof value === "string");
|
||||||
|
expect(getPluginCommandSpecsMock).toHaveBeenCalledWith("discord");
|
||||||
expect(commandNames).toContain("cmd");
|
expect(commandNames).toContain("cmd");
|
||||||
expect(commandNames).toContain("cron_jobs");
|
expect(commandNames).toContain("cron_jobs");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ function appendPluginCommandSpecs(params: {
|
||||||
const existingNames = new Set(
|
const existingNames = new Set(
|
||||||
merged.map((spec) => spec.name.trim().toLowerCase()).filter(Boolean),
|
merged.map((spec) => spec.name.trim().toLowerCase()).filter(Boolean),
|
||||||
);
|
);
|
||||||
for (const pluginCommand of getPluginCommandSpecs()) {
|
for (const pluginCommand of getPluginCommandSpecs("discord")) {
|
||||||
const normalizedName = pluginCommand.name.trim().toLowerCase();
|
const normalizedName = pluginCommand.name.trim().toLowerCase();
|
||||||
if (!normalizedName) {
|
if (!normalizedName) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -59,4 +59,39 @@ describe("registerPluginCommand", () => {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("supports provider-specific native command aliases", () => {
|
||||||
|
const result = registerPluginCommand("demo-plugin", {
|
||||||
|
name: "voice",
|
||||||
|
nativeNames: {
|
||||||
|
default: "talkvoice",
|
||||||
|
discord: "discordvoice",
|
||||||
|
},
|
||||||
|
description: "Demo command",
|
||||||
|
handler: async () => ({ text: "ok" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ ok: true });
|
||||||
|
expect(getPluginCommandSpecs()).toEqual([
|
||||||
|
{
|
||||||
|
name: "talkvoice",
|
||||||
|
description: "Demo command",
|
||||||
|
acceptsArgs: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(getPluginCommandSpecs("discord")).toEqual([
|
||||||
|
{
|
||||||
|
name: "discordvoice",
|
||||||
|
description: "Demo command",
|
||||||
|
acceptsArgs: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(getPluginCommandSpecs("telegram")).toEqual([
|
||||||
|
{
|
||||||
|
name: "talkvoice",
|
||||||
|
description: "Demo command",
|
||||||
|
acceptsArgs: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -316,16 +316,32 @@ export function listPluginCommands(): Array<{
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePluginNativeName(
|
||||||
|
command: OpenClawPluginCommandDefinition,
|
||||||
|
provider?: string,
|
||||||
|
): string {
|
||||||
|
const providerName = provider?.trim().toLowerCase();
|
||||||
|
const providerOverride = providerName ? command.nativeNames?.[providerName] : undefined;
|
||||||
|
if (typeof providerOverride === "string" && providerOverride.trim()) {
|
||||||
|
return providerOverride.trim();
|
||||||
|
}
|
||||||
|
const defaultOverride = command.nativeNames?.default;
|
||||||
|
if (typeof defaultOverride === "string" && defaultOverride.trim()) {
|
||||||
|
return defaultOverride.trim();
|
||||||
|
}
|
||||||
|
return command.name;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get plugin command specs for native command registration (e.g., Telegram).
|
* Get plugin command specs for native command registration (e.g., Telegram).
|
||||||
*/
|
*/
|
||||||
export function getPluginCommandSpecs(): Array<{
|
export function getPluginCommandSpecs(provider?: string): Array<{
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
acceptsArgs: boolean;
|
acceptsArgs: boolean;
|
||||||
}> {
|
}> {
|
||||||
return Array.from(pluginCommands.values()).map((cmd) => ({
|
return Array.from(pluginCommands.values()).map((cmd) => ({
|
||||||
name: cmd.name,
|
name: resolvePluginNativeName(cmd, provider),
|
||||||
description: cmd.description,
|
description: cmd.description,
|
||||||
acceptsArgs: cmd.acceptsArgs ?? false,
|
acceptsArgs: cmd.acceptsArgs ?? false,
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,12 @@ export type PluginCommandHandler = (
|
||||||
export type OpenClawPluginCommandDefinition = {
|
export type OpenClawPluginCommandDefinition = {
|
||||||
/** Command name without leading slash (e.g., "tts") */
|
/** Command name without leading slash (e.g., "tts") */
|
||||||
name: string;
|
name: string;
|
||||||
|
/**
|
||||||
|
* Optional native-command aliases for slash/menu surfaces.
|
||||||
|
* `default` applies to all native providers unless a provider-specific
|
||||||
|
* override exists (for example `{ default: "talkvoice", discord: "voice2" }`).
|
||||||
|
*/
|
||||||
|
nativeNames?: Partial<Record<string, string>> & { default?: string };
|
||||||
/** Description shown in /help and command menus */
|
/** Description shown in /help and command menus */
|
||||||
description: string;
|
description: string;
|
||||||
/** Whether this command accepts arguments */
|
/** Whether this command accepts arguments */
|
||||||
|
|
|
||||||
|
|
@ -359,7 +359,7 @@ export const registerTelegramNativeCommands = ({
|
||||||
runtime.error?.(danger(issue.message));
|
runtime.error?.(danger(issue.message));
|
||||||
}
|
}
|
||||||
const customCommands = customResolution.commands;
|
const customCommands = customResolution.commands;
|
||||||
const pluginCommandSpecs = getPluginCommandSpecs();
|
const pluginCommandSpecs = getPluginCommandSpecs("telegram");
|
||||||
const existingCommands = new Set(
|
const existingCommands = new Set(
|
||||||
[
|
[
|
||||||
...nativeCommands.map((command) => normalizeTelegramCommandName(command.name)),
|
...nativeCommands.map((command) => normalizeTelegramCommandName(command.name)),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue