mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
refactor(discord): split outbound payload helpers
This commit is contained in:
@@ -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,
|
||||
|
||||
29
extensions/discord/src/outbound-approval.ts
Normal file
29
extensions/discord/src/outbound-approval.ts
Normal 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;
|
||||
}
|
||||
81
extensions/discord/src/outbound-components.ts
Normal file
81
extensions/discord/src/outbound-components.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user