fix: keep discord media follow-ups ephemeral

This commit is contained in:
Gustavo Madeira Santana
2026-04-21 19:45:38 -04:00
parent e4e11a44a3
commit fbe6595266
3 changed files with 49 additions and 9 deletions

View File

@@ -11,7 +11,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Discord: keep slash command follow-up chunks ephemeral when the command is configured for ephemeral replies, so long `/status` output no longer leaks fallback model or runtime details into the public channel.
- Discord: keep slash command follow-up chunks ephemeral when the command is configured for ephemeral replies, so long `/status` output no longer leaks fallback model or runtime details into the public channel. (#69869) thanks @gumadeiras.
## 2026.4.21

View File

@@ -6,6 +6,7 @@ import { createNoopThreadBindingManager } from "./thread-bindings.js";
const runtimeModuleMocks = vi.hoisted(() => ({
dispatchReplyWithDispatcher: vi.fn(),
loadWebMedia: vi.fn(),
resolveDirectStatusReplyForSession: vi.fn(),
}));
@@ -25,6 +26,10 @@ vi.mock("openclaw/plugin-sdk/command-status-runtime", () => ({
runtimeModuleMocks.resolveDirectStatusReplyForSession(...args),
}));
vi.mock("openclaw/plugin-sdk/web-media", () => ({
loadWebMedia: (...args: unknown[]) => runtimeModuleMocks.loadWebMedia(...args),
}));
let createDiscordNativeCommand: typeof import("./native-command.js").createDiscordNativeCommand;
let discordNativeCommandTesting: typeof import("./native-command.js").__testing;
@@ -134,6 +139,10 @@ describe("discord native /status", () => {
runtimeModuleMocks.resolveDirectStatusReplyForSession.mockResolvedValue({
text: "status reply",
});
runtimeModuleMocks.loadWebMedia.mockResolvedValue({
buffer: Buffer.from("image"),
fileName: "status.png",
});
discordNativeCommandTesting.setDispatchReplyWithDispatcher(
runtimeModuleMocks.dispatchReplyWithDispatcher as typeof import("openclaw/plugin-sdk/reply-dispatch-runtime").dispatchReplyWithDispatcher,
);
@@ -178,6 +187,37 @@ describe("discord native /status", () => {
}
});
it("keeps direct status media follow-up chunks ephemeral", async () => {
runtimeModuleMocks.resolveDirectStatusReplyForSession.mockResolvedValue({
text: `status image\n${"x".repeat(2200)}`,
mediaUrls: ["https://example.com/status.png"],
});
const cfg = createConfig();
const command = await createStatusCommand(cfg);
const interaction = createInteraction();
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
expect(runtimeModuleMocks.loadWebMedia).toHaveBeenCalledWith("https://example.com/status.png", {
localRoots: expect.any(Array),
});
expect(interaction.followUp.mock.calls.length).toBeGreaterThan(1);
expect(interaction.followUp.mock.calls[0]?.[0]).toEqual(
expect.objectContaining({
ephemeral: true,
files: expect.arrayContaining([expect.objectContaining({ name: "status.png" })]),
}),
);
for (const [payload] of interaction.followUp.mock.calls) {
expect(payload).toEqual(
expect.objectContaining({
ephemeral: true,
}),
);
}
expect(interaction.reply).not.toHaveBeenCalled();
});
it("passes through the effective guild activation when requireMention is disabled", async () => {
const cfg = createConfig({ requireMention: false });
const command = await createStatusCommand(cfg);

View File

@@ -33,7 +33,6 @@ import {
type ChatCommandDefinition,
type CommandArgDefinition,
type CommandArgValues,
type CommandArgs,
type NativeCommandSpec,
} from "openclaw/plugin-sdk/native-command-registry";
import * as pluginRuntime from "openclaw/plugin-sdk/plugin-runtime";
@@ -88,6 +87,10 @@ import type { ThreadBindingManager } from "./thread-bindings.js";
import { resolveDiscordThreadParentInfo } from "./threading.js";
type DiscordConfig = NonNullable<OpenClawConfig["channels"]>["discord"];
type DiscordCommandArgs = {
raw?: string;
values?: CommandArgValues;
};
const log = createSubsystemLogger("discord/native-command");
// Discord application command and option descriptions are limited to 1-100 chars.
// https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure
@@ -559,7 +562,7 @@ async function resolveDiscordNativeAutocompleteAuthorized(params: {
function readDiscordCommandArgs(
interaction: CommandInteraction,
definitions?: CommandArgDefinition[],
): CommandArgs | undefined {
): DiscordCommandArgs | undefined {
if (!definitions || definitions.length === 0) {
return undefined;
}
@@ -726,7 +729,7 @@ export function createDiscordNativeCommand(params: {
? ({
...commandArgs,
raw: serializeCommandArgs(commandDefinition, commandArgs) ?? commandArgs.raw,
} satisfies CommandArgs)
} satisfies DiscordCommandArgs)
: undefined;
const prompt = buildCommandTextFromArgs(commandDefinition, commandArgsWithRaw);
await dispatchDiscordCommandInteraction({
@@ -752,7 +755,7 @@ async function dispatchDiscordCommandInteraction(params: {
interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction;
prompt: string;
command: ChatCommandDefinition;
commandArgs?: CommandArgs;
commandArgs?: DiscordCommandArgs;
cfg: ReturnType<typeof loadConfig>;
discordConfig: DiscordConfig;
accountId: string;
@@ -1402,10 +1405,7 @@ async function deliverDiscordInteractionReply(params: {
if (!chunk.trim()) {
continue;
}
await interaction.followUp({
content: chunk,
...(params.responseEphemeral !== undefined ? { ephemeral: params.responseEphemeral } : {}),
});
await sendMessage(chunk);
}
return;
}