diff --git a/extensions/discord/src/outbound-adapter.ts b/extensions/discord/src/outbound-adapter.ts index 4c17960791d..fd5c04a5325 100644 --- a/extensions/discord/src/outbound-adapter.ts +++ b/extensions/discord/src/outbound-adapter.ts @@ -1,11 +1,23 @@ -import { sendTextMediaPayload } from "../../../src/channels/plugins/outbound/direct-text-media.js"; +import { + resolvePayloadMediaUrls, + sendPayloadMediaSequence, + sendTextMediaPayload, +} from "../../../src/channels/plugins/outbound/direct-text-media.js"; import type { ChannelOutboundAdapter } from "../../../src/channels/plugins/types.js"; import type { OpenClawConfig } from "../../../src/config/config.js"; import type { OutboundIdentity } from "../../../src/infra/outbound/identity.js"; import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js"; +import { resolveInteractiveTextFallback } from "../../../src/interactive/payload.js"; +import type { DiscordComponentMessageSpec } from "./components.js"; import { getThreadBindingManager, type ThreadBindingRecord } from "./monitor/thread-bindings.js"; import { normalizeDiscordOutboundTarget } from "./normalize.js"; -import { sendMessageDiscord, sendPollDiscord, sendWebhookMessageDiscord } from "./send.js"; +import { + sendDiscordComponentMessage, + sendMessageDiscord, + sendPollDiscord, + sendWebhookMessageDiscord, +} from "./send.js"; +import { buildDiscordInteractiveComponents } from "./shared-interactive.js"; function resolveDiscordOutboundTarget(params: { to: string; @@ -78,8 +90,80 @@ export const discordOutbound: ChannelOutboundAdapter = { textChunkLimit: 2000, pollMaxOptions: 10, resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to), - sendPayload: async (ctx) => - await sendTextMediaPayload({ channel: "discord", ctx, adapter: discordOutbound }), + sendPayload: async (ctx) => { + const payload = { + ...ctx.payload, + text: + resolveInteractiveTextFallback({ + text: ctx.payload.text, + interactive: ctx.payload.interactive, + }) ?? "", + }; + const discordData = payload.channelData?.discord as + | { components?: DiscordComponentMessageSpec } + | undefined; + const rawComponentSpec = + discordData?.components ?? buildDiscordInteractiveComponents(payload.interactive); + const componentSpec = rawComponentSpec + ? rawComponentSpec.text + ? rawComponentSpec + : { + ...rawComponentSpec, + text: payload.text?.trim() ? payload.text : undefined, + } + : undefined; + if (!componentSpec) { + return await sendTextMediaPayload({ + channel: "discord", + ctx: { + ...ctx, + payload, + }, + adapter: discordOutbound, + }); + } + const send = + resolveOutboundSendDep(ctx.deps, "discord") ?? sendMessageDiscord; + const target = resolveDiscordOutboundTarget({ to: ctx.to, threadId: ctx.threadId }); + const mediaUrls = resolvePayloadMediaUrls(payload); + if (mediaUrls.length === 0) { + const result = await sendDiscordComponentMessage(target, componentSpec, { + replyTo: ctx.replyToId ?? undefined, + accountId: ctx.accountId ?? undefined, + silent: ctx.silent ?? undefined, + cfg: ctx.cfg, + }); + return { channel: "discord", ...result }; + } + const lastResult = await sendPayloadMediaSequence({ + text: payload.text ?? "", + mediaUrls, + send: async ({ text, mediaUrl, isFirst }) => { + if (isFirst) { + return await sendDiscordComponentMessage(target, componentSpec, { + mediaUrl, + mediaLocalRoots: ctx.mediaLocalRoots, + replyTo: ctx.replyToId ?? undefined, + accountId: ctx.accountId ?? undefined, + silent: ctx.silent ?? undefined, + cfg: ctx.cfg, + }); + } + return await send(target, text, { + verbose: false, + mediaUrl, + mediaLocalRoots: ctx.mediaLocalRoots, + replyTo: ctx.replyToId ?? undefined, + accountId: ctx.accountId ?? undefined, + silent: ctx.silent ?? undefined, + cfg: ctx.cfg, + }); + }, + }); + return lastResult + ? { channel: "discord", ...lastResult } + : { channel: "discord", messageId: "" }; + }, sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, identity, silent }) => { if (!silent) { const webhookResult = await maybeSendDiscordWebhookText({