fix(slack): send block-only slash replies

This commit is contained in:
Vincent Koc
2026-05-01 03:58:03 -07:00
parent 6dac51569e
commit 778b49b8fd
3 changed files with 38 additions and 4 deletions

View File

@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
- WhatsApp: stage `qrcode` through root mirrored runtime dependencies so packaged QR pairing can render from staged plugin-runtime-deps installs. Fixes #75394. Thanks @FelipeX2001.
- Discord/voice: apply per-channel Discord `systemPrompt` overrides to voice transcript turns by forwarding the trusted channel prompt through the voice agent run. Fixes #47095. Thanks @qearlyao.
- Discord/native commands: send component-only interaction replies from slash command and status handlers instead of treating renderable Discord components as an empty response. Thanks @vincentkoc.
- Slack/slash commands: send block-only slash command replies instead of dropping Slack block payloads with no plain-text fallback. Thanks @vincentkoc.
- Gateway/agent: reject strict `openclaw agent --deliver` requests with missing delivery targets before starting the agent run, so users do not wait for a completed turn that cannot send anywhere. Thanks @vincentkoc.
- Setup/import: honor non-interactive `--import-from` onboarding flags by running the migration import path instead of silently completing normal setup without importing anything. Thanks @vincentkoc.
- Discord/voice: run voice-channel turns under a voice-output policy that hides the agent `tts` tool and asks for spoken reply text, so `/vc join` sessions synthesize and play agent replies instead of ending with `NO_REPLY`. Fixes #61536. Thanks @aounakram.

View File

@@ -297,4 +297,31 @@ describe("deliverSlackSlashReplies chunking", () => {
response_type: "ephemeral",
});
});
it("sends block-only slash replies instead of dropping them", async () => {
const respond = vi.fn(async () => undefined);
const blocks = [{ type: "divider" }];
await deliverSlackSlashReplies({
replies: [
{
channelData: {
slack: {
blocks,
},
},
},
],
respond,
ephemeral: false,
textLimit: 8000,
});
expect(respond).toHaveBeenCalledTimes(1);
expect(respond).toHaveBeenCalledWith({
text: "",
blocks,
response_type: "in_channel",
});
});
});

View File

@@ -116,6 +116,7 @@ export async function deliverReplies(params: {
export type SlackRespondFn = (payload: {
text: string;
blocks?: ReturnType<typeof readSlackReplyBlocks>;
response_type?: "ephemeral" | "in_channel";
}) => Promise<unknown>;
@@ -202,14 +203,19 @@ export async function deliverSlackSlashReplies(params: {
tableMode?: MarkdownTableMode;
chunkMode?: ChunkMode;
}) {
const messages: string[] = [];
const messages: Array<{ text: string; blocks?: ReturnType<typeof readSlackReplyBlocks> }> = [];
const chunkLimit = Math.min(params.textLimit, SLACK_TEXT_LIMIT);
for (const payload of params.replies) {
const reply = resolveSendableOutboundReplyParts(payload);
const slackBlocks = readSlackReplyBlocks(payload);
const text =
reply.hasText && !isSilentReplyText(reply.trimmedText, SILENT_REPLY_TOKEN)
? reply.trimmedText
: undefined;
if (slackBlocks?.length && !reply.hasMedia) {
messages.push({ text: text ?? "", blocks: slackBlocks });
continue;
}
const combined = [text ?? "", ...reply.mediaUrls].filter(Boolean).join("\n");
if (!combined) {
continue;
@@ -226,7 +232,7 @@ export async function deliverSlackSlashReplies(params: {
chunks.push(combined);
}
for (const chunk of chunks) {
messages.push(chunk);
messages.push({ text: chunk });
}
}
@@ -236,7 +242,7 @@ export async function deliverSlackSlashReplies(params: {
// Slack slash command responses can be multi-part by sending follow-ups via response_url.
const responseType = params.ephemeral ? "ephemeral" : "in_channel";
for (const text of messages) {
await params.respond({ text, response_type: responseType });
for (const message of messages) {
await params.respond({ ...message, response_type: responseType });
}
}