diff --git a/extensions/discord/src/channel-actions.ts b/extensions/discord/src/channel-actions.ts index e3fab7d872a..049eb4a320c 100644 --- a/extensions/discord/src/channel-actions.ts +++ b/extensions/discord/src/channel-actions.ts @@ -106,8 +106,10 @@ export const discordMessageActions: ChannelMessageActionAdapter = { } return Array.from(actions); }, - supportsInteractive: ({ cfg }) => - listTokenSourcedAccounts(listEnabledDiscordAccounts(cfg)).length > 0, + getCapabilities: ({ cfg }) => + listTokenSourcedAccounts(listEnabledDiscordAccounts(cfg)).length > 0 + ? (["interactive", "components"] as const) + : [], extractToolSend: ({ args }) => { const action = typeof args.action === "string" ? args.action.trim() : ""; if (action === "sendMessage") { diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index 034b9b7c6a1..a9aed9f870d 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -219,11 +219,11 @@ export const feishuPlugin: ChannelPlugin = { } return Array.from(actions); }, - supportsCards: ({ cfg }) => { - return ( - cfg.channels?.feishu?.enabled !== false && + getCapabilities: ({ cfg }) => { + return cfg.channels?.feishu?.enabled !== false && Boolean(resolveFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined)) - ); + ? (["cards"] as const) + : []; }, handleAction: async (ctx) => { const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId ?? undefined }); diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index e8873b93268..4bf52904b3f 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -71,11 +71,11 @@ const mattermostMessageActions: ChannelMessageActionAdapter = { supportsAction: ({ action }) => { return action === "send" || action === "react"; }, - supportsButtons: ({ cfg }) => { + getCapabilities: ({ cfg }) => { const accounts = listMattermostAccountIds(cfg) .map((id) => resolveMattermostAccount({ cfg, accountId: id })) .filter((a) => a.enabled && a.botToken?.trim() && a.baseUrl?.trim()); - return accounts.length > 0; + return accounts.length > 0 ? (["buttons"] as const) : []; }, handleAction: async ({ action, params, cfg, accountId }) => { if (action === "react") { diff --git a/extensions/msteams/src/channel.ts b/extensions/msteams/src/channel.ts index f87f239166c..a21aa451eb8 100644 --- a/extensions/msteams/src/channel.ts +++ b/extensions/msteams/src/channel.ts @@ -366,11 +366,11 @@ export const msteamsPlugin: ChannelPlugin = { } return ["poll"] satisfies ChannelMessageActionName[]; }, - supportsCards: ({ cfg }) => { - return ( - cfg.channels?.msteams?.enabled !== false && + getCapabilities: ({ cfg }) => { + return cfg.channels?.msteams?.enabled !== false && Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)) - ); + ? (["cards"] as const) + : []; }, handleAction: async (ctx) => { // Handle send action with card parameter diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 1a2232bb5e7..9a67597ae19 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -292,6 +292,16 @@ export const slackPlugin: ChannelPlugin = { }, actions: { listActions: ({ cfg }) => listSlackMessageActions(cfg), + getCapabilities: ({ cfg }) => { + const capabilities = new Set<"interactive" | "blocks">(); + if (listSlackMessageActions(cfg).includes("send")) { + capabilities.add("blocks"); + } + if (isSlackInteractiveRepliesEnabled({ cfg })) { + capabilities.add("interactive"); + } + return Array.from(capabilities); + }, extractToolSend: ({ args }) => extractSlackToolSend(args), handleAction: async (ctx) => await handleSlackMessageAction({ diff --git a/extensions/telegram/src/channel-actions.ts b/extensions/telegram/src/channel-actions.ts index 8466cd3226e..0055308cb45 100644 --- a/extensions/telegram/src/channel-actions.ts +++ b/extensions/telegram/src/channel-actions.ts @@ -24,8 +24,8 @@ import { listEnabledTelegramAccounts, resolveTelegramPollActionGateState, } from "./accounts.js"; +import { buildTelegramInteractiveButtons } from "./button-types.js"; import { isTelegramInlineButtonsEnabled } from "./inline-buttons.js"; -import { buildTelegramInteractiveButtons } from "./shared-interactive.js"; const providerId = "telegram"; @@ -124,23 +124,15 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { } return Array.from(actions); }, - supportsInteractive: ({ cfg }) => { + getCapabilities: ({ cfg }) => { const accounts = listTokenSourcedAccounts(listEnabledTelegramAccounts(cfg)); if (accounts.length === 0) { - return false; + return []; } - return accounts.some((account) => - isTelegramInlineButtonsEnabled({ cfg, accountId: account.accountId }), - ); - }, - supportsButtons: ({ cfg }) => { - const accounts = listTokenSourcedAccounts(listEnabledTelegramAccounts(cfg)); - if (accounts.length === 0) { - return false; - } - return accounts.some((account) => + const buttonsEnabled = accounts.some((account) => isTelegramInlineButtonsEnabled({ cfg, accountId: account.accountId }), ); + return buttonsEnabled ? (["interactive", "buttons"] as const) : []; }, extractToolSend: ({ args }) => { return extractToolSend(args, "sendMessage"); diff --git a/extensions/zalo/src/actions.ts b/extensions/zalo/src/actions.ts index 4604cc77310..4f6108fa892 100644 --- a/extensions/zalo/src/actions.ts +++ b/extensions/zalo/src/actions.ts @@ -24,7 +24,7 @@ export const zaloMessageActions: ChannelMessageActionAdapter = { const actions = new Set(["send"]); return Array.from(actions); }, - supportsButtons: () => false, + getCapabilities: () => [], extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"), handleAction: async ({ action, params, cfg, accountId }) => { if (action === "send") { diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts index 930f8d95a25..a148494c8de 100644 --- a/src/agents/tools/message-tool.test.ts +++ b/src/agents/tools/message-tool.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import type { ChannelMessageCapability } from "../../channels/plugins/message-capabilities.js"; import type { ChannelMessageActionName, ChannelPlugin } from "../../channels/plugins/types.js"; import type { MessageActionRunResult } from "../../infra/outbound/message-action-runner.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; @@ -47,9 +48,10 @@ function createChannelPlugin(params: { blurb: string; actions?: ChannelMessageActionName[]; listActions?: NonNullable["listActions"]>; - supportsButtons?: boolean; + capabilities?: readonly ChannelMessageCapability[]; messaging?: ChannelPlugin["messaging"]; }): ChannelPlugin { + const actionCapabilities = params.capabilities; return { id: params.id as ChannelPlugin["id"], meta: { @@ -71,7 +73,9 @@ function createChannelPlugin(params: { (() => { return (params.actions ?? []) as never; }), - ...(params.supportsButtons ? { supportsButtons: () => true } : {}), + ...(actionCapabilities + ? { getCapabilities: (_params: { cfg: unknown }) => actionCapabilities } + : {}), }, }; } @@ -145,7 +149,7 @@ describe("message tool schema scoping", () => { docsPath: "/channels/telegram", blurb: "Telegram test plugin.", actions: ["send", "react", "poll"], - supportsButtons: true, + capabilities: ["interactive", "buttons"], }); const discordPlugin = createChannelPlugin({ @@ -154,6 +158,16 @@ describe("message tool schema scoping", () => { docsPath: "/channels/discord", blurb: "Discord test plugin.", actions: ["send", "poll", "poll-vote"], + capabilities: ["interactive", "components"], + }); + + const slackPlugin = createChannelPlugin({ + id: "slack", + label: "Slack", + docsPath: "/channels/slack", + blurb: "Slack test plugin.", + actions: ["send", "react"], + capabilities: ["interactive", "blocks"], }); afterEach(() => { @@ -164,6 +178,7 @@ describe("message tool schema scoping", () => { { provider: "telegram", expectComponents: false, + expectBlocks: false, expectButtons: true, expectButtonStyle: true, expectTelegramPollExtras: true, @@ -172,16 +187,27 @@ describe("message tool schema scoping", () => { { provider: "discord", expectComponents: true, + expectBlocks: false, expectButtons: false, expectButtonStyle: false, expectTelegramPollExtras: true, expectedActions: ["send", "poll", "poll-vote", "react"], }, + { + provider: "slack", + expectComponents: false, + expectBlocks: true, + expectButtons: false, + expectButtonStyle: false, + expectTelegramPollExtras: true, + expectedActions: ["send", "react", "poll", "poll-vote"], + }, ])( "scopes schema fields for $provider", ({ provider, expectComponents, + expectBlocks, expectButtons, expectButtonStyle, expectTelegramPollExtras, @@ -191,6 +217,7 @@ describe("message tool schema scoping", () => { createTestRegistry([ { pluginId: "telegram", source: "test", plugin: telegramPlugin }, { pluginId: "discord", source: "test", plugin: discordPlugin }, + { pluginId: "slack", source: "test", plugin: slackPlugin }, ]), ); @@ -206,6 +233,11 @@ describe("message tool schema scoping", () => { } else { expect(properties.components).toBeUndefined(); } + if (expectBlocks) { + expect(properties.blocks).toBeDefined(); + } else { + expect(properties.blocks).toBeUndefined(); + } if (expectButtons) { expect(properties.buttons).toBeDefined(); } else { @@ -263,7 +295,7 @@ describe("message tool schema scoping", () => { .channels?.telegram; return telegramCfg?.actions?.poll === false ? ["send", "react"] : ["send", "react", "poll"]; }, - supportsButtons: true, + capabilities: ["interactive", "buttons"], }); setActivePluginRegistry( diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index 299e182b460..e0711ecf8ae 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -2,14 +2,11 @@ import { Type } from "@sinclair/typebox"; import { BLUEBUBBLES_GROUP_ACTIONS } from "../../channels/plugins/bluebubbles-actions.js"; import { listChannelPlugins } from "../../channels/plugins/index.js"; import { - supportsChannelMessageInteractive, - supportsChannelMessageInteractiveForChannel, + channelSupportsMessageCapability, + channelSupportsMessageCapabilityForChannel, listChannelMessageActions, - supportsChannelMessageButtons, - supportsChannelMessageButtonsForChannel, - supportsChannelMessageCards, - supportsChannelMessageCardsForChannel, } from "../../channels/plugins/message-actions.js"; +import type { ChannelMessageCapability } from "../../channels/plugins/message-capabilities.js"; import { CHANNEL_MESSAGE_ACTION_NAMES, type ChannelMessageActionName, @@ -199,6 +196,7 @@ function buildSendSchema(options: { includeButtons: boolean; includeCards: boolean; includeComponents: boolean; + includeBlocks: boolean; }) { const props: Record = { message: Type.Optional(Type.String()), @@ -265,6 +263,17 @@ function buildSendSchema(options: { ), ), components: Type.Optional(discordComponentMessageSchema), + blocks: Type.Optional( + Type.Array( + Type.Object( + {}, + { + additionalProperties: true, + description: "Slack Block Kit payload blocks (Slack only).", + }, + ), + ), + ), }; if (!options.includeButtons) { delete props.buttons; @@ -278,6 +287,9 @@ function buildSendSchema(options: { if (!options.includeComponents) { delete props.components; } + if (!options.includeBlocks) { + delete props.blocks; + } return props; } @@ -487,6 +499,7 @@ function buildMessageToolSchemaProps(options: { includeButtons: boolean; includeCards: boolean; includeComponents: boolean; + includeBlocks: boolean; includeTelegramPollExtras: boolean; }) { return { @@ -513,6 +526,7 @@ function buildMessageToolSchemaFromActions( includeButtons: boolean; includeCards: boolean; includeComponents: boolean; + includeBlocks: boolean; includeTelegramPollExtras: boolean; }, ) { @@ -528,6 +542,7 @@ const MessageToolSchema = buildMessageToolSchemaFromActions(AllMessageActions, { includeButtons: true, includeCards: true, includeComponents: true, + includeBlocks: true, includeTelegramPollExtras: true, }); @@ -578,30 +593,59 @@ function resolveMessageToolSchemaActions(params: { return actions.length > 0 ? actions : ["send"]; } +function resolveIncludeCapability( + params: { + cfg: OpenClawConfig; + currentChannelProvider?: string; + }, + capability: ChannelMessageCapability, +): boolean { + const currentChannel = normalizeMessageChannel(params.currentChannelProvider); + if (currentChannel) { + return channelSupportsMessageCapabilityForChannel( + { + cfg: params.cfg, + channel: currentChannel, + }, + capability, + ); + } + return channelSupportsMessageCapability(params.cfg, capability); +} + function resolveIncludeComponents(params: { cfg: OpenClawConfig; currentChannelProvider?: string; }): boolean { - const currentChannel = normalizeMessageChannel(params.currentChannelProvider); - if (currentChannel) { - return currentChannel === "discord"; - } - // Components are currently Discord-specific. - return listChannelSupportedActions({ cfg: params.cfg, channel: "discord" }).length > 0; + return resolveIncludeCapability(params, "components"); } function resolveIncludeInteractive(params: { cfg: OpenClawConfig; currentChannelProvider?: string; }): boolean { - const currentChannel = normalizeMessageChannel(params.currentChannelProvider); - if (currentChannel) { - return supportsChannelMessageInteractiveForChannel({ - cfg: params.cfg, - channel: currentChannel, - }); - } - return supportsChannelMessageInteractive(params.cfg); + return resolveIncludeCapability(params, "interactive"); +} + +function resolveIncludeButtons(params: { + cfg: OpenClawConfig; + currentChannelProvider?: string; +}): boolean { + return resolveIncludeCapability(params, "buttons"); +} + +function resolveIncludeCards(params: { + cfg: OpenClawConfig; + currentChannelProvider?: string; +}): boolean { + return resolveIncludeCapability(params, "cards"); +} + +function resolveIncludeBlocks(params: { + cfg: OpenClawConfig; + currentChannelProvider?: string; +}): boolean { + return resolveIncludeCapability(params, "blocks"); } function resolveIncludeTelegramPollExtras(params: { @@ -619,22 +663,19 @@ function buildMessageToolSchema(params: { currentChannelProvider?: string; currentChannelId?: string; }) { - const currentChannel = normalizeMessageChannel(params.currentChannelProvider); const actions = resolveMessageToolSchemaActions(params); const includeInteractive = resolveIncludeInteractive(params); - const includeButtons = currentChannel - ? supportsChannelMessageButtonsForChannel({ cfg: params.cfg, channel: currentChannel }) - : supportsChannelMessageButtons(params.cfg); - const includeCards = currentChannel - ? supportsChannelMessageCardsForChannel({ cfg: params.cfg, channel: currentChannel }) - : supportsChannelMessageCards(params.cfg); + const includeButtons = resolveIncludeButtons(params); + const includeCards = resolveIncludeCards(params); const includeComponents = resolveIncludeComponents(params); + const includeBlocks = resolveIncludeBlocks(params); const includeTelegramPollExtras = resolveIncludeTelegramPollExtras(params); return buildMessageToolSchemaFromActions(actions.length > 0 ? actions : ["send"], { includeInteractive, includeButtons, includeCards, includeComponents, + includeBlocks, includeTelegramPollExtras, }); } diff --git a/src/channels/plugins/message-actions.test.ts b/src/channels/plugins/message-actions.test.ts index f9734e99dec..92af406e2f1 100644 --- a/src/channels/plugins/message-actions.test.ts +++ b/src/channels/plugins/message-actions.test.ts @@ -6,22 +6,19 @@ import { createTestRegistry, } from "../../test-utils/channel-plugins.js"; import { - supportsChannelMessageButtons, - supportsChannelMessageButtonsForChannel, - supportsChannelMessageCards, - supportsChannelMessageCardsForChannel, - supportsChannelMessageInteractive, - supportsChannelMessageInteractiveForChannel, + channelSupportsMessageCapability, + channelSupportsMessageCapabilityForChannel, + listChannelMessageCapabilities, + listChannelMessageCapabilitiesForChannel, } from "./message-actions.js"; +import type { ChannelMessageCapability } from "./message-capabilities.js"; import type { ChannelPlugin } from "./types.js"; const emptyRegistry = createTestRegistry([]); function createMessageActionsPlugin(params: { id: "discord" | "telegram"; - supportsInteractive: boolean; - supportsButtons: boolean; - supportsCards: boolean; + capabilities: readonly ChannelMessageCapability[]; }): ChannelPlugin { return { ...createChannelTestPluginBase({ @@ -34,25 +31,19 @@ function createMessageActionsPlugin(params: { }), actions: { listActions: () => ["send"], - supportsInteractive: () => params.supportsInteractive, - supportsButtons: () => params.supportsButtons, - supportsCards: () => params.supportsCards, + getCapabilities: () => params.capabilities, }, }; } const buttonsPlugin = createMessageActionsPlugin({ id: "discord", - supportsInteractive: true, - supportsButtons: true, - supportsCards: false, + capabilities: ["interactive", "buttons"], }); const cardsPlugin = createMessageActionsPlugin({ id: "telegram", - supportsInteractive: false, - supportsButtons: false, - supportsCards: true, + capabilities: ["cards"], }); function activateMessageActionTestRegistry() { @@ -69,38 +60,66 @@ describe("message action capability checks", () => { setActivePluginRegistry(emptyRegistry); }); - it("aggregates buttons/card support across plugins", () => { + it("aggregates capabilities across plugins", () => { activateMessageActionTestRegistry(); - expect(supportsChannelMessageInteractive({} as OpenClawConfig)).toBe(true); - expect(supportsChannelMessageButtons({} as OpenClawConfig)).toBe(true); - expect(supportsChannelMessageCards({} as OpenClawConfig)).toBe(true); + expect(listChannelMessageCapabilities({} as OpenClawConfig).toSorted()).toEqual([ + "buttons", + "cards", + "interactive", + ]); + expect(channelSupportsMessageCapability({} as OpenClawConfig, "interactive")).toBe(true); + expect(channelSupportsMessageCapability({} as OpenClawConfig, "buttons")).toBe(true); + expect(channelSupportsMessageCapability({} as OpenClawConfig, "cards")).toBe(true); }); it("checks per-channel capabilities", () => { activateMessageActionTestRegistry(); expect( - supportsChannelMessageInteractiveForChannel({ + listChannelMessageCapabilitiesForChannel({ cfg: {} as OpenClawConfig, channel: "discord", }), - ).toBe(true); + ).toEqual(["interactive", "buttons"]); expect( - supportsChannelMessageInteractiveForChannel({ + listChannelMessageCapabilitiesForChannel({ cfg: {} as OpenClawConfig, channel: "telegram", }), - ).toBe(false); + ).toEqual(["cards"]); expect( - supportsChannelMessageButtonsForChannel({ cfg: {} as OpenClawConfig, channel: "discord" }), + channelSupportsMessageCapabilityForChannel( + { cfg: {} as OpenClawConfig, channel: "discord" }, + "interactive", + ), ).toBe(true); expect( - supportsChannelMessageButtonsForChannel({ cfg: {} as OpenClawConfig, channel: "telegram" }), + channelSupportsMessageCapabilityForChannel( + { cfg: {} as OpenClawConfig, channel: "telegram" }, + "interactive", + ), ).toBe(false); expect( - supportsChannelMessageCardsForChannel({ cfg: {} as OpenClawConfig, channel: "telegram" }), + channelSupportsMessageCapabilityForChannel( + { cfg: {} as OpenClawConfig, channel: "discord" }, + "buttons", + ), ).toBe(true); - expect(supportsChannelMessageCardsForChannel({ cfg: {} as OpenClawConfig })).toBe(false); + expect( + channelSupportsMessageCapabilityForChannel( + { cfg: {} as OpenClawConfig, channel: "telegram" }, + "buttons", + ), + ).toBe(false); + expect( + channelSupportsMessageCapabilityForChannel( + { cfg: {} as OpenClawConfig, channel: "telegram" }, + "cards", + ), + ).toBe(true); + expect(channelSupportsMessageCapabilityForChannel({ cfg: {} as OpenClawConfig }, "cards")).toBe( + false, + ); }); }); diff --git a/src/channels/plugins/message-actions.ts b/src/channels/plugins/message-actions.ts index 8507b9ad0a0..e6488bff91f 100644 --- a/src/channels/plugins/message-actions.ts +++ b/src/channels/plugins/message-actions.ts @@ -1,6 +1,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { OpenClawConfig } from "../../config/config.js"; import { getChannelPlugin, listChannelPlugins } from "./index.js"; +import type { ChannelMessageCapability } from "./message-capabilities.js"; import type { ChannelMessageActionContext, ChannelMessageActionName } from "./types.js"; const trustedRequesterRequiredByChannel: Readonly< @@ -30,72 +31,52 @@ export function listChannelMessageActions(cfg: OpenClawConfig): ChannelMessageAc return Array.from(actions); } -export function supportsChannelMessageButtons(cfg: OpenClawConfig): boolean { - return supportsMessageFeature(cfg, (actions) => actions?.supportsButtons?.({ cfg }) === true); -} - -export function supportsChannelMessageInteractive(cfg: OpenClawConfig): boolean { - return supportsMessageFeature(cfg, (actions) => actions?.supportsInteractive?.({ cfg }) === true); -} - -export function supportsChannelMessageButtonsForChannel(params: { - cfg: OpenClawConfig; - channel?: string; -}): boolean { - return supportsMessageFeatureForChannel( - params, - (actions) => actions.supportsButtons?.(params) === true, - ); -} - -export function supportsChannelMessageInteractiveForChannel(params: { - cfg: OpenClawConfig; - channel?: string; -}): boolean { - return supportsMessageFeatureForChannel( - params, - (actions) => actions.supportsInteractive?.(params) === true, - ); -} - -export function supportsChannelMessageCards(cfg: OpenClawConfig): boolean { - return supportsMessageFeature(cfg, (actions) => actions?.supportsCards?.({ cfg }) === true); -} - -export function supportsChannelMessageCardsForChannel(params: { - cfg: OpenClawConfig; - channel?: string; -}): boolean { - return supportsMessageFeatureForChannel( - params, - (actions) => actions.supportsCards?.(params) === true, - ); -} - -function supportsMessageFeature( +function listCapabilities( + actions: ChannelActions, cfg: OpenClawConfig, - check: (actions: ChannelActions) => boolean, -): boolean { +): readonly ChannelMessageCapability[] { + return actions.getCapabilities?.({ cfg }) ?? []; +} + +export function listChannelMessageCapabilities(cfg: OpenClawConfig): ChannelMessageCapability[] { + const capabilities = new Set(); for (const plugin of listChannelPlugins()) { - if (plugin.actions && check(plugin.actions)) { - return true; + if (!plugin.actions) { + continue; + } + for (const capability of listCapabilities(plugin.actions, cfg)) { + capabilities.add(capability); } } - return false; + return Array.from(capabilities); } -function supportsMessageFeatureForChannel( +export function listChannelMessageCapabilitiesForChannel(params: { + cfg: OpenClawConfig; + channel?: string; +}): ChannelMessageCapability[] { + if (!params.channel) { + return []; + } + const plugin = getChannelPlugin(params.channel as Parameters[0]); + return plugin?.actions ? Array.from(listCapabilities(plugin.actions, params.cfg)) : []; +} + +export function channelSupportsMessageCapability( + cfg: OpenClawConfig, + capability: ChannelMessageCapability, +): boolean { + return listChannelMessageCapabilities(cfg).includes(capability); +} + +export function channelSupportsMessageCapabilityForChannel( params: { cfg: OpenClawConfig; channel?: string; }, - check: (actions: ChannelActions) => boolean, + capability: ChannelMessageCapability, ): boolean { - if (!params.channel) { - return false; - } - const plugin = getChannelPlugin(params.channel as Parameters[0]); - return plugin?.actions ? check(plugin.actions) : false; + return listChannelMessageCapabilitiesForChannel(params).includes(capability); } export async function dispatchChannelMessageAction( diff --git a/src/channels/plugins/message-capabilities.ts b/src/channels/plugins/message-capabilities.ts new file mode 100644 index 00000000000..b7c2abd2a24 --- /dev/null +++ b/src/channels/plugins/message-capabilities.ts @@ -0,0 +1,9 @@ +export const CHANNEL_MESSAGE_CAPABILITIES = [ + "interactive", + "buttons", + "cards", + "components", + "blocks", +] as const; + +export type ChannelMessageCapability = (typeof CHANNEL_MESSAGE_CAPABILITIES)[number]; diff --git a/src/channels/plugins/slack.actions.ts b/src/channels/plugins/slack.actions.ts index d11bba67cd3..66bc6679680 100644 --- a/src/channels/plugins/slack.actions.ts +++ b/src/channels/plugins/slack.actions.ts @@ -11,7 +11,16 @@ import type { ChannelMessageActionAdapter } from "./types.js"; export function createSlackActions(providerId: string): ChannelMessageActionAdapter { return { listActions: ({ cfg }) => listSlackMessageActions(cfg), - supportsInteractive: ({ cfg }) => isSlackInteractiveRepliesEnabled({ cfg }), + getCapabilities: ({ cfg }) => { + const capabilities = new Set<"interactive" | "blocks">(); + if (listSlackMessageActions(cfg).includes("send")) { + capabilities.add("blocks"); + } + if (isSlackInteractiveRepliesEnabled({ cfg })) { + capabilities.add("interactive"); + } + return Array.from(capabilities); + }, extractToolSend: ({ args }) => extractSlackToolSend(args), handleAction: async (ctx) => { return await handleSlackMessageAction({ diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index 0ec51b0f0db..fc6d1b91731 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -7,6 +7,7 @@ import type { GatewayClientMode, GatewayClientName } from "../../utils/message-c import type { ChatType } from "../chat-type.js"; import type { ChatChannelId } from "../registry.js"; import type { ChannelMessageActionName as ChannelMessageActionNameFromList } from "./message-action-names.js"; +import type { ChannelMessageCapability } from "./message-capabilities.js"; export type ChannelId = ChatChannelId | (string & {}); @@ -372,9 +373,7 @@ export type ChannelMessageActionAdapter = { */ listActions?: (params: { cfg: OpenClawConfig }) => ChannelMessageActionName[]; supportsAction?: (params: { action: ChannelMessageActionName }) => boolean; - supportsInteractive?: (params: { cfg: OpenClawConfig }) => boolean; - supportsButtons?: (params: { cfg: OpenClawConfig }) => boolean; - supportsCards?: (params: { cfg: OpenClawConfig }) => boolean; + getCapabilities?: (params: { cfg: OpenClawConfig }) => readonly ChannelMessageCapability[]; extractToolSend?: (params: { args: Record }) => ChannelToolSend | null; handleAction?: (ctx: ChannelMessageActionContext) => Promise>; }; diff --git a/src/channels/plugins/types.ts b/src/channels/plugins/types.ts index d3028e9970d..e56262efb41 100644 --- a/src/channels/plugins/types.ts +++ b/src/channels/plugins/types.ts @@ -1,8 +1,10 @@ import type { ChannelMessageActionName as ChannelMessageActionNameFromList } from "./message-action-names.js"; export { CHANNEL_MESSAGE_ACTION_NAMES } from "./message-action-names.js"; +export { CHANNEL_MESSAGE_CAPABILITIES } from "./message-capabilities.js"; export type ChannelMessageActionName = ChannelMessageActionNameFromList; +export type { ChannelMessageCapability } from "./message-capabilities.js"; export type { ChannelAuthAdapter,