diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f6368dc372..08d3e591aa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai - Agents/context engine: expose prompt-cache runtime context to context engines and keep current-turn prompt-cache usage aligned with the active attempt instead of stale prior-turn assistant state. (#62179) Thanks @jalehman. - Tools/media: document per-provider music and video generation capabilities, and add shared live video-to-video sweep coverage for providers that support local reference clips. - Compaction: add pluggable compaction provider registry so plugins can replace the built-in summarization pipeline. Configure via `agents.defaults.compaction.provider`; falls back to LLM summarization on provider failure. (#56224) Thanks @DhruvBhatia0. +- Discord/events: allow `event-create` to accept a cover image URL or local file path, load and validate PNG/JPG/GIF event cover media, and pass the encoded image payload through Discord admin action/runtime paths. (#60883) Thanks @bittoby. ### Fixes diff --git a/extensions/discord/src/actions/handle-action.guild-admin.ts b/extensions/discord/src/actions/handle-action.guild-admin.ts index cc45cff5265..867b20a5cbd 100644 --- a/extensions/discord/src/actions/handle-action.guild-admin.ts +++ b/extensions/discord/src/actions/handle-action.guild-admin.ts @@ -15,7 +15,7 @@ import { type Ctx = Pick< ChannelMessageActionContext, - "action" | "params" | "cfg" | "accountId" | "requesterSenderId" + "action" | "params" | "cfg" | "accountId" | "requesterSenderId" | "mediaLocalRoots" >; export async function tryHandleDiscordMessageActionGuildAdmin(params: { @@ -336,6 +336,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { const channelId = readStringParam(actionParams, "channelId"); const location = readStringParam(actionParams, "location"); const entityType = readStringParam(actionParams, "eventType"); + const image = readStringParam(actionParams, "image", { trim: false }); return await handleDiscordAction( { action: "eventCreate", @@ -348,8 +349,10 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { channelId, location, entityType, + image, }, cfg, + { mediaLocalRoots: ctx.mediaLocalRoots }, ); } diff --git a/extensions/discord/src/actions/runtime.guild.ts b/extensions/discord/src/actions/runtime.guild.ts index 4514e2a7eab..a5c159c5e85 100644 --- a/extensions/discord/src/actions/runtime.guild.ts +++ b/extensions/discord/src/actions/runtime.guild.ts @@ -30,6 +30,7 @@ import { setChannelPermissionDiscord, uploadEmojiDiscord, uploadStickerDiscord, + resolveEventCoverImage, } from "../send.js"; import { readDiscordParentIdParam } from "./runtime.shared.js"; @@ -37,6 +38,7 @@ export const discordGuildActionRuntime = { addRoleDiscord, createChannelDiscord, createScheduledEventDiscord, + resolveEventCoverImage, deleteChannelDiscord, editChannelDiscord, fetchChannelInfoDiscord, @@ -95,6 +97,7 @@ export async function handleDiscordGuildAction( params: Record, isActionEnabled: ActionGate, cfg?: OpenClawConfig, + options?: { mediaLocalRoots?: readonly string[] }, ): Promise> { const accountId = readStringParam(params, "accountId"); switch (action) { @@ -299,8 +302,14 @@ export async function handleDiscordGuildAction( const description = readStringParam(params, "description"); const channelId = readStringParam(params, "channelId"); const location = readStringParam(params, "location"); + const imageUrl = readStringParam(params, "image", { trim: false }); const entityTypeRaw = readStringParam(params, "entityType"); const entityType = entityTypeRaw === "stage" ? 1 : entityTypeRaw === "external" ? 3 : 2; + const image = imageUrl + ? await discordGuildActionRuntime.resolveEventCoverImage(imageUrl, { + localRoots: options?.mediaLocalRoots, + }) + : undefined; const payload = { name, description, @@ -309,6 +318,7 @@ export async function handleDiscordGuildAction( entity_type: entityType, channel_id: channelId, entity_metadata: entityType === 3 && location ? { location } : undefined, + image, privacy_level: 2, }; const event = accountId diff --git a/extensions/discord/src/actions/runtime.ts b/extensions/discord/src/actions/runtime.ts index 8e9a9435bea..a4f9da17541 100644 --- a/extensions/discord/src/actions/runtime.ts +++ b/extensions/discord/src/actions/runtime.ts @@ -69,7 +69,7 @@ export async function handleDiscordAction( return await handleDiscordMessagingAction(action, params, isActionEnabled, options, cfg); } if (guildActions.has(action)) { - return await handleDiscordGuildAction(action, params, isActionEnabled, cfg); + return await handleDiscordGuildAction(action, params, isActionEnabled, cfg, options); } if (moderationActions.has(action)) { return await handleDiscordModerationAction(action, params, isActionEnabled); diff --git a/extensions/discord/src/send.guild.ts b/extensions/discord/src/send.guild.ts index fab1aff9ace..2c6ff5ee09e 100644 --- a/extensions/discord/src/send.guild.ts +++ b/extensions/discord/src/send.guild.ts @@ -7,6 +7,7 @@ import type { RESTPostAPIGuildScheduledEventJSONBody, } from "discord-api-types/v10"; import { Routes } from "discord-api-types/v10"; +import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media"; import { resolveDiscordRest } from "./send.shared.js"; import type { DiscordModerationTarget, @@ -14,6 +15,7 @@ import type { DiscordRoleChange, DiscordTimeoutTarget, } from "./send.types.js"; +import { DISCORD_MAX_EVENT_COVER_BYTES } from "./send.types.js"; export async function fetchMemberInfoDiscord( guildId: string, @@ -77,6 +79,25 @@ export async function listScheduledEventsDiscord( return (await rest.get(Routes.guildScheduledEvents(guildId))) as APIGuildScheduledEvent[]; } +const ALLOWED_EVENT_COVER_TYPES = new Set(["image/png", "image/jpeg", "image/jpg", "image/gif"]); + +// Loads an image from a URL or path and returns a data URI suitable for the Discord API. +export async function resolveEventCoverImage( + imageUrl: string, + opts?: { localRoots?: readonly string[] }, +): Promise { + const media = await loadWebMediaRaw(imageUrl, DISCORD_MAX_EVENT_COVER_BYTES, { + localRoots: opts?.localRoots, + }); + const contentType = media.contentType?.toLowerCase(); + if (!contentType || !ALLOWED_EVENT_COVER_TYPES.has(contentType)) { + throw new Error( + `Discord event cover images must be PNG, JPG, or GIF (got ${contentType ?? "unknown"})`, + ); + } + return `data:${contentType};base64,${media.buffer.toString("base64")}`; +} + export async function createScheduledEventDiscord( guildId: string, payload: RESTPostAPIGuildScheduledEventJSONBody, diff --git a/extensions/discord/src/send.ts b/extensions/discord/src/send.ts index 57fe3529da0..d31269da23e 100644 --- a/extensions/discord/src/send.ts +++ b/extensions/discord/src/send.ts @@ -15,6 +15,7 @@ export { addRoleDiscord, banMemberDiscord, createScheduledEventDiscord, + resolveEventCoverImage, fetchChannelInfoDiscord, fetchMemberInfoDiscord, fetchRoleInfoDiscord, diff --git a/extensions/discord/src/send.types.ts b/extensions/discord/src/send.types.ts index 31f1f7ce184..f4e637ecace 100644 --- a/extensions/discord/src/send.types.ts +++ b/extensions/discord/src/send.types.ts @@ -22,6 +22,7 @@ export class DiscordSendError extends Error { export const DISCORD_MAX_EMOJI_BYTES = 256 * 1024; export const DISCORD_MAX_STICKER_BYTES = 512 * 1024; +export const DISCORD_MAX_EVENT_COVER_BYTES = 8 * 1024 * 1024; export type DiscordSendResult = { messageId: string; diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index 218fe295b97..f98f5490e30 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -283,6 +283,9 @@ function buildEventSchema() { endTime: Type.Optional(Type.String()), desc: Type.Optional(Type.String()), location: Type.Optional(Type.String()), + image: Type.Optional( + Type.String({ description: "Cover image URL or local file path for the event." }), + ), durationMin: Type.Optional(Type.Number()), until: Type.Optional(Type.String()), }; diff --git a/src/cli/program/message/register.discord-admin.ts b/src/cli/program/message/register.discord-admin.ts index de806dfede9..1994d3c53b5 100644 --- a/src/cli/program/message/register.discord-admin.ts +++ b/src/cli/program/message/register.discord-admin.ts @@ -109,6 +109,7 @@ export function registerMessageDiscordAdminCommands(message: Command, helpers: M .option("--channel-id ", "Channel id") .option("--location ", "Event location") .option("--event-type ", "Event type") + .option("--image ", "Cover image URL or local file path") .action(async (opts) => { await helpers.runMessageAction("event-create", opts); });