From b48194a07eeca5d3ca3f30261b8a06dc23347962 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Thu, 19 Mar 2026 00:53:52 -0400 Subject: [PATCH] Plugins: move message tool schemas into channel plugins --- extensions/discord/src/channel-actions.ts | 2 +- extensions/discord/src/message-tool-schema.ts | 114 +++++++++++++++ extensions/slack/src/channel-actions.ts | 2 +- extensions/slack/src/message-tool-schema.ts | 13 ++ extensions/telegram/src/channel-actions.ts | 2 +- .../telegram/src/message-tool-schema.ts | 9 ++ src/agents/tools/message-tool.test.ts | 24 +++- src/channels/plugins/message-tool-schema.ts | 132 ------------------ 8 files changed, 157 insertions(+), 141 deletions(-) create mode 100644 extensions/discord/src/message-tool-schema.ts create mode 100644 extensions/slack/src/message-tool-schema.ts create mode 100644 extensions/telegram/src/message-tool-schema.ts diff --git a/extensions/discord/src/channel-actions.ts b/extensions/discord/src/channel-actions.ts index c4be7728439..1c6b9b5c70f 100644 --- a/extensions/discord/src/channel-actions.ts +++ b/extensions/discord/src/channel-actions.ts @@ -1,5 +1,4 @@ import { - createDiscordMessageToolComponentsSchema, createUnionActionGate, listTokenSourcedAccounts, } from "openclaw/plugin-sdk/channel-runtime"; @@ -11,6 +10,7 @@ import type { import type { DiscordActionConfig } from "openclaw/plugin-sdk/config-runtime"; import { createDiscordActionGate, listEnabledDiscordAccounts } from "./accounts.js"; import { handleDiscordMessageAction } from "./actions/handle-action.js"; +import { createDiscordMessageToolComponentsSchema } from "./message-tool-schema.js"; function resolveDiscordActionDiscovery(cfg: Parameters[0]) { const accounts = listTokenSourcedAccounts(listEnabledDiscordAccounts(cfg)); diff --git a/extensions/discord/src/message-tool-schema.ts b/extensions/discord/src/message-tool-schema.ts new file mode 100644 index 00000000000..0ad9c87480d --- /dev/null +++ b/extensions/discord/src/message-tool-schema.ts @@ -0,0 +1,114 @@ +import { Type } from "@sinclair/typebox"; +import { stringEnum } from "openclaw/plugin-sdk/core"; + +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()), + allowedUsers: Type.Optional( + Type.Array( + Type.String({ + description: "Discord user ids or names allowed to interact with this button.", + }), + ), + ), +}); + +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), +}); + +export function createDiscordMessageToolComponentsSchema() { + return Type.Object( + { + text: Type.Optional(Type.String()), + reusable: Type.Optional( + Type.Boolean({ + description: "Allow components to be used multiple times until they expire.", + }), + ), + 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), + }, + { + description: + "Discord components v2 payload. Set reusable=true to keep buttons, selects, and forms active until expiry.", + }, + ); +} diff --git a/extensions/slack/src/channel-actions.ts b/extensions/slack/src/channel-actions.ts index 76606f6433f..3d9c2417306 100644 --- a/extensions/slack/src/channel-actions.ts +++ b/extensions/slack/src/channel-actions.ts @@ -1,6 +1,5 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import { - createSlackMessageToolBlocksSchema, type ChannelMessageActionAdapter, type ChannelMessageToolDiscovery, } from "openclaw/plugin-sdk/channel-runtime"; @@ -8,6 +7,7 @@ import type { SlackActionContext } from "./action-runtime.js"; import { handleSlackAction } from "./action-runtime.js"; import { handleSlackMessageAction } from "./message-action-dispatch.js"; import { extractSlackToolSend, listSlackMessageActions } from "./message-actions.js"; +import { createSlackMessageToolBlocksSchema } from "./message-tool-schema.js"; import { isSlackInteractiveRepliesEnabled } from "./runtime-api.js"; import { resolveSlackChannelId } from "./targets.js"; diff --git a/extensions/slack/src/message-tool-schema.ts b/extensions/slack/src/message-tool-schema.ts new file mode 100644 index 00000000000..b9b6d8d3de9 --- /dev/null +++ b/extensions/slack/src/message-tool-schema.ts @@ -0,0 +1,13 @@ +import { Type } from "@sinclair/typebox"; + +export function createSlackMessageToolBlocksSchema() { + return Type.Array( + Type.Object( + {}, + { + additionalProperties: true, + description: "Slack Block Kit payload blocks (Slack only).", + }, + ), + ); +} diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index 867a0951a42..d01c5f91839 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -1,6 +1,5 @@ import { createMessageToolButtonsSchema, - createTelegramPollExtraToolSchemas, createUnionActionGate, listTokenSourcedAccounts, resolveReactionMessageId, @@ -18,6 +17,7 @@ import { } from "./accounts.js"; import { handleTelegramAction } from "./action-runtime.js"; import { isTelegramInlineButtonsEnabled } from "./inline-buttons.js"; +import { createTelegramPollExtraToolSchemas } from "./message-tool-schema.js"; export const telegramMessageActionRuntime = { handleTelegramAction, diff --git a/extensions/telegram/src/message-tool-schema.ts b/extensions/telegram/src/message-tool-schema.ts new file mode 100644 index 00000000000..bfc91fbfd67 --- /dev/null +++ b/extensions/telegram/src/message-tool-schema.ts @@ -0,0 +1,9 @@ +import { Type } from "@sinclair/typebox"; + +export function createTelegramPollExtraToolSchemas() { + return { + pollDurationSeconds: Type.Optional(Type.Number()), + pollAnonymous: Type.Optional(Type.Boolean()), + pollPublic: Type.Optional(Type.Boolean()), + }; +} diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts index 9d6f252a256..bd5b45f94f6 100644 --- a/src/agents/tools/message-tool.test.ts +++ b/src/agents/tools/message-tool.test.ts @@ -1,11 +1,7 @@ +import { Type } from "@sinclair/typebox"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelMessageCapability } from "../../channels/plugins/message-capabilities.js"; -import { - createDiscordMessageToolComponentsSchema, - createMessageToolButtonsSchema, - createSlackMessageToolBlocksSchema, - createTelegramPollExtraToolSchemas, -} from "../../channels/plugins/message-tool-schema.js"; +import { createMessageToolButtonsSchema } from "../../channels/plugins/message-tool-schema.js"; import type { ChannelMessageActionName, ChannelPlugin } from "../../channels/plugins/types.js"; import type { MessageActionRunResult } from "../../infra/outbound/message-action-runner.js"; type CreateMessageTool = typeof import("./message-tool.js").createMessageTool; @@ -22,6 +18,22 @@ type DescribeMessageTool = NonNullable< type MessageToolDiscoveryContext = Parameters[0]; type MessageToolSchema = NonNullable>["schema"]; +function createDiscordMessageToolComponentsSchema() { + return Type.Object({ type: Type.Literal("discord-components") }); +} + +function createSlackMessageToolBlocksSchema() { + return Type.Array(Type.Object({}, { additionalProperties: true })); +} + +function createTelegramPollExtraToolSchemas() { + return { + pollDurationSeconds: Type.Optional(Type.Number()), + pollAnonymous: Type.Optional(Type.Boolean()), + pollPublic: Type.Optional(Type.Boolean()), + }; +} + const mocks = vi.hoisted(() => ({ runMessageAction: vi.fn(), loadConfig: vi.fn(() => ({})), diff --git a/src/channels/plugins/message-tool-schema.ts b/src/channels/plugins/message-tool-schema.ts index 008fdf08f81..1e3557729b6 100644 --- a/src/channels/plugins/message-tool-schema.ts +++ b/src/channels/plugins/message-tool-schema.ts @@ -2,93 +2,6 @@ import { Type } from "@sinclair/typebox"; import type { TSchema } from "@sinclair/typebox"; import { stringEnum } from "../../agents/schema/typebox.js"; -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()), - allowedUsers: Type.Optional( - Type.Array( - Type.String({ - description: "Discord user ids or names allowed to interact with this button.", - }), - ), - ), -}); - -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), -}); - export function createMessageToolButtonsSchema(): TSchema { return Type.Array( Type.Array( @@ -113,48 +26,3 @@ export function createMessageToolCardSchema(): TSchema { }, ); } - -export function createDiscordMessageToolComponentsSchema(): TSchema { - return Type.Object( - { - text: Type.Optional(Type.String()), - reusable: Type.Optional( - Type.Boolean({ - description: "Allow components to be used multiple times until they expire.", - }), - ), - 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), - }, - { - description: - "Discord components v2 payload. Set reusable=true to keep buttons, selects, and forms active until expiry.", - }, - ); -} - -export function createSlackMessageToolBlocksSchema(): TSchema { - return Type.Array( - Type.Object( - {}, - { - additionalProperties: true, - description: "Slack Block Kit payload blocks (Slack only).", - }, - ), - ); -} - -export function createTelegramPollExtraToolSchemas(): Record { - return { - pollDurationSeconds: Type.Optional(Type.Number()), - pollAnonymous: Type.Optional(Type.Boolean()), - pollPublic: Type.Optional(Type.Boolean()), - }; -}