Plugins: move message tool schemas into channel plugins

This commit is contained in:
Gustavo Madeira Santana
2026-03-19 00:53:52 -04:00
parent 8467fb6601
commit b48194a07e
8 changed files with 157 additions and 141 deletions

View File

@@ -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<typeof listEnabledDiscordAccounts>[0]) {
const accounts = listTokenSourcedAccounts(listEnabledDiscordAccounts(cfg));

View File

@@ -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.",
},
);
}

View File

@@ -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";

View File

@@ -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).",
},
),
);
}

View File

@@ -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,

View File

@@ -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()),
};
}

View File

@@ -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<DescribeMessageTool>[0];
type MessageToolSchema = NonNullable<ReturnType<DescribeMessageTool>>["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(() => ({})),

View File

@@ -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<string, TSchema> {
return {
pollDurationSeconds: Type.Optional(Type.Number()),
pollAnonymous: Type.Optional(Type.Boolean()),
pollPublic: Type.Optional(Type.Boolean()),
};
}