import { resolvePayloadMediaUrls, sendPayloadMediaSequence, sendTextMediaPayload, } from "openclaw/plugin-sdk/channel-runtime"; import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-runtime"; import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { OutboundIdentity } from "openclaw/plugin-sdk/infra-runtime"; import type { DiscordComponentMessageSpec } from "./components.js"; import { getThreadBindingManager, type ThreadBindingRecord } from "./monitor/thread-bindings.js"; import { normalizeDiscordOutboundTarget } from "./normalize.js"; import { sendDiscordComponentMessage, sendMessageDiscord, sendPollDiscord, sendWebhookMessageDiscord, } from "./send.js"; import { buildDiscordInteractiveComponents } from "./shared-interactive.js"; export const DISCORD_TEXT_CHUNK_LIMIT = 2000; function resolveDiscordOutboundTarget(params: { to: string; threadId?: string | number | null; }): string { if (params.threadId == null) { return params.to; } const threadId = String(params.threadId).trim(); if (!threadId) { return params.to; } return `channel:${threadId}`; } function resolveDiscordWebhookIdentity(params: { identity?: OutboundIdentity; binding: ThreadBindingRecord; }): { username?: string; avatarUrl?: string } { const usernameRaw = params.identity?.name?.trim(); const fallbackUsername = params.binding.label?.trim() || params.binding.agentId; const username = (usernameRaw || fallbackUsername || "").slice(0, 80) || undefined; const avatarUrl = params.identity?.avatarUrl?.trim() || undefined; return { username, avatarUrl }; } async function maybeSendDiscordWebhookText(params: { cfg?: OpenClawConfig; text: string; threadId?: string | number | null; accountId?: string | null; identity?: OutboundIdentity; replyToId?: string | null; }): Promise<{ messageId: string; channelId: string } | null> { if (params.threadId == null) { return null; } const threadId = String(params.threadId).trim(); if (!threadId) { return null; } const manager = getThreadBindingManager(params.accountId ?? undefined); if (!manager) { return null; } const binding = manager.getByThreadId(threadId); if (!binding?.webhookId || !binding?.webhookToken) { return null; } const persona = resolveDiscordWebhookIdentity({ identity: params.identity, binding, }); const result = await sendWebhookMessageDiscord(params.text, { webhookId: binding.webhookId, webhookToken: binding.webhookToken, accountId: binding.accountId, threadId: binding.threadId, cfg: params.cfg, replyTo: params.replyToId ?? undefined, username: persona.username, avatarUrl: persona.avatarUrl, }); return result; } export const discordOutbound: ChannelOutboundAdapter = { deliveryMode: "direct", chunker: null, textChunkLimit: DISCORD_TEXT_CHUNK_LIMIT, pollMaxOptions: 10, resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to), sendPayload: async (ctx) => { const payload = { ...ctx.payload, text: ctx.payload.text ?? "", }; 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({ cfg, text, threadId, accountId, identity, replyToId, }).catch(() => null); if (webhookResult) { return { channel: "discord", ...webhookResult }; } } const send = resolveOutboundSendDep(deps, "discord") ?? sendMessageDiscord; const target = resolveDiscordOutboundTarget({ to, threadId }); const result = await send(target, text, { verbose: false, replyTo: replyToId ?? undefined, accountId: accountId ?? undefined, silent: silent ?? undefined, cfg, }); return { channel: "discord", ...result }; }, sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId, deps, replyToId, threadId, silent, }) => { const send = resolveOutboundSendDep(deps, "discord") ?? sendMessageDiscord; const target = resolveDiscordOutboundTarget({ to, threadId }); const result = await send(target, text, { verbose: false, mediaUrl, mediaLocalRoots, replyTo: replyToId ?? undefined, accountId: accountId ?? undefined, silent: silent ?? undefined, cfg, }); return { channel: "discord", ...result }; }, sendPoll: async ({ cfg, to, poll, accountId, threadId, silent }) => { const target = resolveDiscordOutboundTarget({ to, threadId }); return await sendPollDiscord(target, poll, { accountId: accountId ?? undefined, silent: silent ?? undefined, cfg, }); }, };