mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-29 01:52:04 +00:00
Discord: add component v2 UI tool support (#17419)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { DiscordActionConfig } from "../../config/config.js";
|
||||
import type { DiscordSendComponents, DiscordSendEmbeds } from "../../discord/send.shared.js";
|
||||
import { readDiscordComponentSpec } from "../../discord/components.js";
|
||||
import {
|
||||
createThreadDiscord,
|
||||
deleteMessageDiscord,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
removeOwnReactionsDiscord,
|
||||
removeReactionDiscord,
|
||||
searchMessagesDiscord,
|
||||
sendDiscordComponentMessage,
|
||||
sendMessageDiscord,
|
||||
sendPollDiscord,
|
||||
sendStickerDiscord,
|
||||
@@ -233,24 +235,54 @@ export async function handleDiscordMessagingAction(
|
||||
const to = readStringParam(params, "to", { required: true });
|
||||
const asVoice = params.asVoice === true;
|
||||
const silent = params.silent === true;
|
||||
const rawComponents = params.components;
|
||||
const componentSpec =
|
||||
rawComponents && typeof rawComponents === "object" && !Array.isArray(rawComponents)
|
||||
? readDiscordComponentSpec(rawComponents)
|
||||
: null;
|
||||
const components: DiscordSendComponents | undefined =
|
||||
Array.isArray(rawComponents) || typeof rawComponents === "function"
|
||||
? (rawComponents as DiscordSendComponents)
|
||||
: undefined;
|
||||
const content = readStringParam(params, "content", {
|
||||
required: !asVoice,
|
||||
required: !asVoice && !componentSpec && !components,
|
||||
allowEmpty: true,
|
||||
});
|
||||
const mediaUrl =
|
||||
readStringParam(params, "mediaUrl", { trim: false }) ??
|
||||
readStringParam(params, "path", { trim: false }) ??
|
||||
readStringParam(params, "filePath", { trim: false });
|
||||
const filename = readStringParam(params, "filename");
|
||||
const replyTo = readStringParam(params, "replyTo");
|
||||
const rawComponents = params.components;
|
||||
const components: DiscordSendComponents | undefined =
|
||||
Array.isArray(rawComponents) || typeof rawComponents === "function"
|
||||
? (rawComponents as DiscordSendComponents)
|
||||
: undefined;
|
||||
const rawEmbeds = params.embeds;
|
||||
const embeds: DiscordSendEmbeds | undefined = Array.isArray(rawEmbeds)
|
||||
? (rawEmbeds as DiscordSendEmbeds)
|
||||
: undefined;
|
||||
const sessionKey = readStringParam(params, "__sessionKey");
|
||||
const agentId = readStringParam(params, "__agentId");
|
||||
|
||||
if (componentSpec) {
|
||||
if (asVoice) {
|
||||
throw new Error("Discord components cannot be sent as voice messages.");
|
||||
}
|
||||
if (embeds?.length) {
|
||||
throw new Error("Discord components cannot include embeds.");
|
||||
}
|
||||
const normalizedContent = content?.trim() ? content : undefined;
|
||||
const payload = componentSpec.text
|
||||
? componentSpec
|
||||
: { ...componentSpec, text: normalizedContent };
|
||||
const result = await sendDiscordComponentMessage(to, payload, {
|
||||
...(accountId ? { accountId } : {}),
|
||||
silent,
|
||||
replyTo: replyTo ?? undefined,
|
||||
sessionKey: sessionKey ?? undefined,
|
||||
agentId: agentId ?? undefined,
|
||||
mediaUrl: mediaUrl ?? undefined,
|
||||
filename: filename ?? undefined,
|
||||
});
|
||||
return jsonResult({ ok: true, result, components: true });
|
||||
}
|
||||
|
||||
// Handle voice message sending
|
||||
if (asVoice) {
|
||||
|
||||
@@ -47,6 +47,98 @@ function buildRoutingSchema() {
|
||||
};
|
||||
}
|
||||
|
||||
const discordComponentEmojiSchema = Type.Object({
|
||||
name: Type.String(),
|
||||
id: Type.Optional(Type.String()),
|
||||
animated: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
const discordComponentOptionSchema = Type.Object({
|
||||
label: Type.String(),
|
||||
value: Type.String(),
|
||||
description: Type.Optional(Type.String()),
|
||||
emoji: Type.Optional(discordComponentEmojiSchema),
|
||||
default: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
const discordComponentButtonSchema = Type.Object({
|
||||
label: Type.String(),
|
||||
style: Type.Optional(stringEnum(["primary", "secondary", "success", "danger", "link"])),
|
||||
url: Type.Optional(Type.String()),
|
||||
emoji: Type.Optional(discordComponentEmojiSchema),
|
||||
disabled: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
const discordComponentSelectSchema = Type.Object({
|
||||
type: Type.Optional(stringEnum(["string", "user", "role", "mentionable", "channel"])),
|
||||
placeholder: Type.Optional(Type.String()),
|
||||
minValues: Type.Optional(Type.Number()),
|
||||
maxValues: Type.Optional(Type.Number()),
|
||||
options: Type.Optional(Type.Array(discordComponentOptionSchema)),
|
||||
});
|
||||
|
||||
const discordComponentBlockSchema = Type.Object({
|
||||
type: Type.String(),
|
||||
text: Type.Optional(Type.String()),
|
||||
texts: Type.Optional(Type.Array(Type.String())),
|
||||
accessory: Type.Optional(
|
||||
Type.Object({
|
||||
type: Type.String(),
|
||||
url: Type.Optional(Type.String()),
|
||||
button: Type.Optional(discordComponentButtonSchema),
|
||||
}),
|
||||
),
|
||||
spacing: Type.Optional(stringEnum(["small", "large"])),
|
||||
divider: Type.Optional(Type.Boolean()),
|
||||
buttons: Type.Optional(Type.Array(discordComponentButtonSchema)),
|
||||
select: Type.Optional(discordComponentSelectSchema),
|
||||
items: Type.Optional(
|
||||
Type.Array(
|
||||
Type.Object({
|
||||
url: Type.String(),
|
||||
description: Type.Optional(Type.String()),
|
||||
spoiler: Type.Optional(Type.Boolean()),
|
||||
}),
|
||||
),
|
||||
),
|
||||
file: Type.Optional(Type.String()),
|
||||
spoiler: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
const discordComponentModalFieldSchema = Type.Object({
|
||||
type: Type.String(),
|
||||
name: Type.Optional(Type.String()),
|
||||
label: Type.String(),
|
||||
description: Type.Optional(Type.String()),
|
||||
placeholder: Type.Optional(Type.String()),
|
||||
required: Type.Optional(Type.Boolean()),
|
||||
options: Type.Optional(Type.Array(discordComponentOptionSchema)),
|
||||
minValues: Type.Optional(Type.Number()),
|
||||
maxValues: Type.Optional(Type.Number()),
|
||||
minLength: Type.Optional(Type.Number()),
|
||||
maxLength: Type.Optional(Type.Number()),
|
||||
style: Type.Optional(stringEnum(["short", "paragraph"])),
|
||||
});
|
||||
|
||||
const discordComponentModalSchema = Type.Object({
|
||||
title: Type.String(),
|
||||
triggerLabel: Type.Optional(Type.String()),
|
||||
triggerStyle: Type.Optional(stringEnum(["primary", "secondary", "success", "danger", "link"])),
|
||||
fields: Type.Array(discordComponentModalFieldSchema),
|
||||
});
|
||||
|
||||
const discordComponentMessageSchema = Type.Object({
|
||||
text: Type.Optional(Type.String()),
|
||||
container: Type.Optional(
|
||||
Type.Object({
|
||||
accentColor: Type.Optional(Type.String()),
|
||||
spoiler: Type.Optional(Type.Boolean()),
|
||||
}),
|
||||
),
|
||||
blocks: Type.Optional(Type.Array(discordComponentBlockSchema)),
|
||||
modal: Type.Optional(discordComponentModalSchema),
|
||||
});
|
||||
|
||||
function buildSendSchema(options: { includeButtons: boolean; includeCards: boolean }) {
|
||||
const props: Record<string, unknown> = {
|
||||
message: Type.Optional(Type.String()),
|
||||
@@ -105,6 +197,7 @@ function buildSendSchema(options: { includeButtons: boolean; includeCards: boole
|
||||
},
|
||||
),
|
||||
),
|
||||
components: Type.Optional(discordComponentMessageSchema),
|
||||
};
|
||||
if (!options.includeButtons) {
|
||||
delete props.buttons;
|
||||
@@ -481,6 +574,7 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||
defaultAccountId: accountId ?? undefined,
|
||||
gateway,
|
||||
toolContext,
|
||||
sessionKey: options?.agentSessionKey,
|
||||
agentId: options?.agentSessionKey
|
||||
? resolveSessionAgentId({ sessionKey: options.agentSessionKey, config: cfg })
|
||||
: undefined,
|
||||
|
||||
Reference in New Issue
Block a user