refactor(discord): split outbound payload helpers

This commit is contained in:
Peter Steinberger
2026-04-25 03:22:45 +01:00
parent 30aa7e0d4d
commit 1bdf5307d9
4 changed files with 118 additions and 117 deletions

View File

@@ -16,11 +16,9 @@ import { withDiscordDeliveryRetry } from "./delivery-retry.js";
import { isLikelyDiscordVideoMedia } from "./media-detection.js";
import type { ThreadBindingRecord } from "./monitor/thread-bindings.js";
import { normalizeDiscordOutboundTarget } from "./normalize.js";
import {
buildDiscordPresentationPayload,
normalizeDiscordApprovalPayload,
sendDiscordOutboundPayload,
} from "./outbound-payload.js";
import { normalizeDiscordApprovalPayload } from "./outbound-approval.js";
import { buildDiscordPresentationPayload } from "./outbound-components.js";
import { sendDiscordOutboundPayload } from "./outbound-payload.js";
import {
loadDiscordSendRuntime,
resolveDiscordFormattingOptions,

View File

@@ -0,0 +1,29 @@
function hasApprovalChannelData(payload: { channelData?: unknown }): boolean {
const channelData = payload.channelData;
if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
return false;
}
return Boolean((channelData as { execApproval?: unknown }).execApproval);
}
function neutralizeDiscordApprovalMentions(value: string): string {
return value
.replace(/@everyone/gi, "@\u200beveryone")
.replace(/@here/gi, "@\u200bhere")
.replace(/<@/g, "<@\u200b")
.replace(/<#/g, "<#\u200b");
}
export function normalizeDiscordApprovalPayload<
T extends {
text?: string;
channelData?: unknown;
},
>(payload: T): T {
return hasApprovalChannelData(payload) && payload.text
? {
...payload,
text: neutralizeDiscordApprovalMentions(payload.text),
}
: payload;
}

View File

@@ -0,0 +1,81 @@
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-send-result";
import { readDiscordComponentSpec, type DiscordComponentMessageSpec } from "./components.js";
type DiscordComponentSendFn = typeof import("./send.components.js").sendDiscordComponentMessage;
type DiscordSharedInteractiveModule = typeof import("./shared-interactive.js");
type OutboundPayload = Parameters<NonNullable<ChannelOutboundAdapter["sendPayload"]>>[0]["payload"];
let discordComponentSendPromise: Promise<DiscordComponentSendFn> | undefined;
let discordSharedInteractivePromise: Promise<DiscordSharedInteractiveModule> | undefined;
export async function sendDiscordComponentMessageLazy(
...args: Parameters<DiscordComponentSendFn>
): ReturnType<DiscordComponentSendFn> {
discordComponentSendPromise ??= import("./send.components.js").then(
(module) => module.sendDiscordComponentMessage,
);
return await (
await discordComponentSendPromise
)(...args);
}
function loadDiscordSharedInteractive(): Promise<DiscordSharedInteractiveModule> {
discordSharedInteractivePromise ??= import("./shared-interactive.js");
return discordSharedInteractivePromise;
}
function addPayloadTextFallback(
spec: DiscordComponentMessageSpec,
payload: Pick<OutboundPayload, "text">,
): DiscordComponentMessageSpec {
return spec.text
? spec
: {
...spec,
text: payload.text?.trim() ? payload.text : undefined,
};
}
export async function buildDiscordPresentationPayload(params: {
payload: Parameters<NonNullable<ChannelOutboundAdapter["renderPresentation"]>>[0]["payload"];
presentation: Parameters<
NonNullable<ChannelOutboundAdapter["renderPresentation"]>
>[0]["presentation"];
}): Promise<typeof params.payload | null> {
const componentSpec = (await loadDiscordSharedInteractive()).buildDiscordPresentationComponents(
params.presentation,
);
if (!componentSpec) {
return null;
}
return {
...params.payload,
channelData: {
...params.payload.channelData,
discord: {
...(params.payload.channelData?.discord as Record<string, unknown> | undefined),
presentationComponents: componentSpec,
},
},
};
}
export async function resolveDiscordComponentSpec(
payload: OutboundPayload,
): Promise<DiscordComponentMessageSpec | undefined> {
const discordData = payload.channelData?.discord as
| { components?: unknown; presentationComponents?: DiscordComponentMessageSpec }
| undefined;
const rawComponentSpec =
discordData?.presentationComponents ?? readDiscordComponentSpec(discordData?.components);
if (rawComponentSpec) {
return addPayloadTextFallback(rawComponentSpec, payload);
}
if (!payload.interactive) {
return undefined;
}
const interactiveSpec = (await loadDiscordSharedInteractive()).buildDiscordInteractiveComponents(
payload.interactive,
);
return interactiveSpec ? addPayloadTextFallback(interactiveSpec, payload) : undefined;
}

View File

@@ -7,120 +7,13 @@ import {
sendPayloadMediaSequenceOrFallback,
sendTextMediaPayload,
} from "openclaw/plugin-sdk/reply-payload";
import { readDiscordComponentSpec, type DiscordComponentMessageSpec } from "./components.js";
import { normalizeDiscordApprovalPayload } from "./outbound-approval.js";
import {
resolveDiscordComponentSpec,
sendDiscordComponentMessageLazy,
} from "./outbound-components.js";
import { createDiscordPayloadSendContext } from "./outbound-send-context.js";
type DiscordComponentSendFn = typeof import("./send.components.js").sendDiscordComponentMessage;
type DiscordSharedInteractiveModule = typeof import("./shared-interactive.js");
let discordComponentSendPromise: Promise<DiscordComponentSendFn> | undefined;
let discordSharedInteractivePromise: Promise<DiscordSharedInteractiveModule> | undefined;
async function sendDiscordComponentMessageLazy(
...args: Parameters<DiscordComponentSendFn>
): ReturnType<DiscordComponentSendFn> {
discordComponentSendPromise ??= import("./send.components.js").then(
(module) => module.sendDiscordComponentMessage,
);
return await (
await discordComponentSendPromise
)(...args);
}
function loadDiscordSharedInteractive(): Promise<DiscordSharedInteractiveModule> {
discordSharedInteractivePromise ??= import("./shared-interactive.js");
return discordSharedInteractivePromise;
}
function hasApprovalChannelData(payload: { channelData?: unknown }): boolean {
const channelData = payload.channelData;
if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
return false;
}
return Boolean((channelData as { execApproval?: unknown }).execApproval);
}
function neutralizeDiscordApprovalMentions(value: string): string {
return value
.replace(/@everyone/gi, "@\u200beveryone")
.replace(/@here/gi, "@\u200bhere")
.replace(/<@/g, "<@\u200b")
.replace(/<#/g, "<#\u200b");
}
export function normalizeDiscordApprovalPayload<
T extends {
text?: string;
channelData?: unknown;
},
>(payload: T): T {
return hasApprovalChannelData(payload) && payload.text
? {
...payload,
text: neutralizeDiscordApprovalMentions(payload.text),
}
: payload;
}
export async function buildDiscordPresentationPayload(params: {
payload: Parameters<NonNullable<ChannelOutboundAdapter["renderPresentation"]>>[0]["payload"];
presentation: Parameters<
NonNullable<ChannelOutboundAdapter["renderPresentation"]>
>[0]["presentation"];
}): Promise<typeof params.payload | null> {
const componentSpec = (await loadDiscordSharedInteractive()).buildDiscordPresentationComponents(
params.presentation,
);
if (!componentSpec) {
return null;
}
return {
...params.payload,
channelData: {
...params.payload.channelData,
discord: {
...(params.payload.channelData?.discord as Record<string, unknown> | undefined),
presentationComponents: componentSpec,
},
},
};
}
function resolveDiscordComponentSpec(
payload: Parameters<NonNullable<ChannelOutboundAdapter["sendPayload"]>>[0]["payload"],
): Promise<DiscordComponentMessageSpec | undefined> {
const discordData = payload.channelData?.discord as
| { components?: unknown; presentationComponents?: DiscordComponentMessageSpec }
| undefined;
const rawComponentSpec =
discordData?.presentationComponents ?? readDiscordComponentSpec(discordData?.components);
if (rawComponentSpec) {
return Promise.resolve(
rawComponentSpec.text
? rawComponentSpec
: {
...rawComponentSpec,
text: payload.text?.trim() ? payload.text : undefined,
},
);
}
if (!payload.interactive) {
return Promise.resolve(undefined);
}
return loadDiscordSharedInteractive().then((module) => {
const interactiveSpec = module.buildDiscordInteractiveComponents(payload.interactive);
if (!interactiveSpec) {
return undefined;
}
return interactiveSpec.text
? interactiveSpec
: {
...interactiveSpec,
text: payload.text?.trim() ? payload.text : undefined,
};
});
}
export async function sendDiscordOutboundPayload(params: {
ctx: Parameters<NonNullable<ChannelOutboundAdapter["sendPayload"]>>[0];
fallbackAdapter: ChannelOutboundAdapter;