mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:00:43 +00:00
fix: warn on empty discord native command replies
This commit is contained in:
@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Hooks/doctor: warn when `hooks.transformsDir` points outside the canonical hooks transform directory, so invalid workspace skill paths get a direct recovery hint before the Gateway crash-loops. Fixes #75853. Thanks @midobk.
|
||||
- Proxy/audio: convert standard `FormData` bodies before proxy-backed undici fetches, so audio transcription and multipart uploads no longer send `[object FormData]` when `HTTP_PROXY` or `HTTPS_PROXY` is configured. Fixes #48554. Thanks @dco5.
|
||||
- Discord: allow explicitly configured ack reactions in tool-only guild channels while keeping automatic lifecycle/status reactions suppressed. Fixes #74922. Thanks @samvilian and @BlueBirdBack.
|
||||
- Discord/native commands: return an explicit warning when slash command dispatch or direct plugin execution produces no visible reply instead of a success-style completion ack. Fixes #58986; supersedes #62057. Thanks @jb510.
|
||||
- Discord: keep typing indicators alive during long tool runs and auto-compaction while keepalive ticks continue, so active sessions do not appear stalled before the final reply. Thanks @Squirbie.
|
||||
- Discord: preserve multipart Content-Type headers for attachment uploads across REST fetch paths, so generated images and other media no longer fail delivery with `CONTENT_TYPE_INVALID`. Thanks @FunJim.
|
||||
- Discord: preserve attachment and sticker filenames when saving inbound media, so agents can see human-readable file names instead of only UUID-based paths. Fixes #59744. Thanks @xela92 and @rockcent.
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
import type { DiscordChannelConfigResolved } from "./allow-list.js";
|
||||
import type { buildDiscordNativeCommandContext } from "./native-command-context.js";
|
||||
import {
|
||||
DISCORD_EMPTY_VISIBLE_REPLY_WARNING,
|
||||
deliverDiscordInteractionReply,
|
||||
isDiscordUnknownInteraction,
|
||||
safeDiscordInteractionCall,
|
||||
@@ -102,6 +103,7 @@ export async function dispatchDiscordNativeAgentReply(params: {
|
||||
if (
|
||||
params.suppressReplies ||
|
||||
didReply ||
|
||||
dispatchResult.queuedFinal ||
|
||||
dispatchResult.counts.final !== 0 ||
|
||||
dispatchResult.counts.block !== 0 ||
|
||||
dispatchResult.counts.tool !== 0
|
||||
@@ -111,7 +113,7 @@ export async function dispatchDiscordNativeAgentReply(params: {
|
||||
|
||||
await safeDiscordInteractionCall("interaction empty fallback", async () => {
|
||||
const payload = {
|
||||
content: "✅ Done.",
|
||||
content: DISCORD_EMPTY_VISIBLE_REPLY_WARNING,
|
||||
ephemeral: true,
|
||||
};
|
||||
if (params.preferFollowUp) {
|
||||
|
||||
@@ -13,6 +13,8 @@ import type {
|
||||
TopLevelComponents,
|
||||
} from "../internal/discord.js";
|
||||
|
||||
export const DISCORD_EMPTY_VISIBLE_REPLY_WARNING = "⚠️ Command produced no visible reply.";
|
||||
|
||||
export function isDiscordUnknownInteraction(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") {
|
||||
return false;
|
||||
|
||||
@@ -529,6 +529,88 @@ describe("Discord native plugin command dispatch", () => {
|
||||
expect(interaction.reply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns an explicit warning instead of success when dispatch produces zero visible replies", async () => {
|
||||
const cfg = createConfig();
|
||||
const interaction = createInteraction();
|
||||
runtimeModuleMocks.matchPluginCommand.mockReturnValue(null);
|
||||
runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue({
|
||||
counts: { final: 0, block: 0, tool: 0 },
|
||||
queuedFinal: false,
|
||||
} as never);
|
||||
const command = await createNativeCommand(cfg, {
|
||||
name: "new",
|
||||
description: "Start a new session.",
|
||||
acceptsArgs: true,
|
||||
});
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
|
||||
|
||||
expect(interaction.followUp).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
content: "⚠️ Command produced no visible reply.",
|
||||
ephemeral: true,
|
||||
}),
|
||||
);
|
||||
expect(interaction.reply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not warn when dispatch reports a queued final without visible counts", async () => {
|
||||
const cfg = createConfig();
|
||||
const interaction = createInteraction();
|
||||
runtimeModuleMocks.matchPluginCommand.mockReturnValue(null);
|
||||
runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue({
|
||||
counts: { final: 0, block: 0, tool: 0 },
|
||||
queuedFinal: true,
|
||||
} as never);
|
||||
const command = await createNativeCommand(cfg, {
|
||||
name: "new",
|
||||
description: "Start a new session.",
|
||||
acceptsArgs: true,
|
||||
});
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
|
||||
|
||||
expect(interaction.followUp).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: "⚠️ Command produced no visible reply." }),
|
||||
);
|
||||
expect(interaction.reply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns an explicit warning when a direct plugin command has no visible reply", async () => {
|
||||
const cfg = createConfig();
|
||||
const commandSpec: NativeCommandSpec = {
|
||||
name: "cron_jobs",
|
||||
description: "List cron jobs",
|
||||
acceptsArgs: false,
|
||||
};
|
||||
const interaction = createInteraction();
|
||||
const pluginMatch = {
|
||||
command: {
|
||||
name: "cron_jobs",
|
||||
description: "List cron jobs",
|
||||
pluginId: "cron-jobs",
|
||||
acceptsArgs: false,
|
||||
handler: vi.fn().mockResolvedValue({ text: "" }),
|
||||
},
|
||||
args: undefined,
|
||||
};
|
||||
|
||||
runtimeModuleMocks.matchPluginCommand.mockReturnValue(pluginMatch as never);
|
||||
runtimeModuleMocks.executePluginCommand.mockResolvedValue({});
|
||||
const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue(
|
||||
{} as never,
|
||||
);
|
||||
const command = await createNativeCommand(cfg, commandSpec);
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
|
||||
|
||||
expect(dispatchSpy).not.toHaveBeenCalled();
|
||||
expect(interaction.followUp).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: "⚠️ Command produced no visible reply." }),
|
||||
);
|
||||
expect(interaction.reply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forwards Discord thread metadata into direct plugin command execution", async () => {
|
||||
const cfg = {
|
||||
commands: {
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
import { buildDiscordNativeCommandContext } from "./native-command-context.js";
|
||||
import type { DispatchDiscordCommandInteractionResult } from "./native-command-dispatch.js";
|
||||
import {
|
||||
DISCORD_EMPTY_VISIBLE_REPLY_WARNING,
|
||||
deliverDiscordInteractionReply,
|
||||
hasRenderableReplyPayload,
|
||||
safeDiscordInteractionCall,
|
||||
@@ -540,7 +541,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
threadParentId: pluginThreadParentId,
|
||||
});
|
||||
if (!hasRenderableReplyPayload(pluginReply)) {
|
||||
await respond("Done.");
|
||||
await respond(DISCORD_EMPTY_VISIBLE_REPLY_WARNING);
|
||||
return { accepted: true, effectiveRoute };
|
||||
}
|
||||
await deliverDiscordInteractionReply({
|
||||
|
||||
Reference in New Issue
Block a user