225 lines
7.1 KiB
TypeScript
225 lines
7.1 KiB
TypeScript
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<typeof sendMessageDiscord>(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<typeof sendMessageDiscord>(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<typeof sendMessageDiscord>(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,
|
|
});
|
|
},
|
|
};
|