feat: add discord reaction tool
This commit is contained in:
@@ -41,6 +41,7 @@ import {
|
||||
} from "../cli/nodes-screen.js";
|
||||
import { parseDurationMs } from "../cli/parse-duration.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { reactMessageDiscord } from "../discord/send.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { detectMime } from "../media/mime.js";
|
||||
import { sanitizeToolResultImages } from "./tool-images.js";
|
||||
@@ -1422,6 +1423,48 @@ const GatewayToolSchema = Type.Union([
|
||||
}),
|
||||
]);
|
||||
|
||||
const DiscordToolSchema = Type.Union([
|
||||
Type.Object({
|
||||
action: Type.Literal("react"),
|
||||
channelId: Type.String(),
|
||||
messageId: Type.String(),
|
||||
emoji: Type.String(),
|
||||
}),
|
||||
]);
|
||||
|
||||
function createDiscordTool(): AnyAgentTool {
|
||||
return {
|
||||
label: "Clawdis Discord",
|
||||
name: "clawdis_discord",
|
||||
description:
|
||||
"React to Discord messages. Requires discord.enableReactions=true in config.",
|
||||
parameters: DiscordToolSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = args as Record<string, unknown>;
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
if (action !== "react") throw new Error(`Unknown action: ${action}`);
|
||||
|
||||
const cfg = loadConfig();
|
||||
if (!cfg.discord?.enableReactions) {
|
||||
throw new Error(
|
||||
"Discord reactions are disabled (set discord.enableReactions=true).",
|
||||
);
|
||||
}
|
||||
|
||||
const channelId = readStringParam(params, "channelId", {
|
||||
required: true,
|
||||
});
|
||||
const messageId = readStringParam(params, "messageId", {
|
||||
required: true,
|
||||
});
|
||||
const emoji = readStringParam(params, "emoji", { required: true });
|
||||
|
||||
await reactMessageDiscord(channelId, messageId, emoji);
|
||||
return jsonResult({ ok: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createGatewayTool(): AnyAgentTool {
|
||||
return {
|
||||
label: "Clawdis Gateway",
|
||||
@@ -1470,6 +1513,7 @@ export function createClawdisTools(): AnyAgentTool[] {
|
||||
createCanvasTool(),
|
||||
createNodesTool(),
|
||||
createCronTool(),
|
||||
createDiscordTool(),
|
||||
createGatewayTool(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -171,6 +171,8 @@ export type DiscordConfig = {
|
||||
mediaMaxMb?: number;
|
||||
/** Number of recent guild messages to include for context (default: 20). */
|
||||
historyLimit?: number;
|
||||
/** Allow agent-triggered Discord reactions (default: false). */
|
||||
enableReactions?: boolean;
|
||||
};
|
||||
|
||||
export type SignalConfig = {
|
||||
@@ -879,6 +881,7 @@ const ClawdisSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
enableReactions: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
signal: z
|
||||
|
||||
@@ -44,6 +44,7 @@ type DiscordHistoryEntry = {
|
||||
sender: string;
|
||||
body: string;
|
||||
timestamp?: number;
|
||||
messageId?: string;
|
||||
};
|
||||
|
||||
export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
@@ -122,6 +123,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
sender: message.member?.displayName ?? message.author.tag,
|
||||
body: baseText,
|
||||
timestamp: message.createdTimestamp,
|
||||
messageId: message.id,
|
||||
});
|
||||
while (history.length > historyLimit) history.shift();
|
||||
guildHistories.set(message.channelId, history);
|
||||
@@ -196,11 +198,12 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
const fromLabel = isDirectMessage
|
||||
? buildDirectLabel(message)
|
||||
: buildGuildLabel(message);
|
||||
const textWithId = `${text}\n[discord message id: ${message.id} channel: ${message.channelId}]`;
|
||||
let combinedBody = formatAgentEnvelope({
|
||||
surface: "Discord",
|
||||
from: fromLabel,
|
||||
timestamp: message.createdTimestamp,
|
||||
body: text,
|
||||
body: textWithId,
|
||||
});
|
||||
let shouldClearHistory = false;
|
||||
if (!isDirectMessage) {
|
||||
@@ -215,7 +218,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
surface: "Discord",
|
||||
from: fromLabel,
|
||||
timestamp: entry.timestamp,
|
||||
body: `${entry.sender}: ${entry.body}`,
|
||||
body: `${entry.sender}: ${entry.body} [id:${entry.messageId ?? "unknown"} channel:${message.channelId}]`,
|
||||
}),
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
@@ -29,6 +29,11 @@ export type DiscordSendResult = {
|
||||
channelId: string;
|
||||
};
|
||||
|
||||
export type DiscordReactOpts = {
|
||||
token?: string;
|
||||
rest?: REST;
|
||||
};
|
||||
|
||||
function resolveToken(explicit?: string) {
|
||||
const cfgToken = loadConfig().discord?.token;
|
||||
const token = normalizeDiscordToken(
|
||||
@@ -42,6 +47,16 @@ function resolveToken(explicit?: string) {
|
||||
return token;
|
||||
}
|
||||
|
||||
function normalizeReactionEmoji(raw: string) {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("emoji required");
|
||||
}
|
||||
const customMatch = trimmed.match(/^<a?:([^:>]+):(\d+)>$/);
|
||||
const identifier = customMatch ? `${customMatch[1]}:${customMatch[2]}` : trimmed;
|
||||
return encodeURIComponent(identifier);
|
||||
}
|
||||
|
||||
function parseRecipient(raw: string): DiscordRecipient {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
@@ -164,3 +179,16 @@ export async function sendMessageDiscord(
|
||||
channelId: String(result.channel_id ?? channelId),
|
||||
};
|
||||
}
|
||||
|
||||
export async function reactMessageDiscord(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const encoded = normalizeReactionEmoji(emoji);
|
||||
await rest.put(Routes.channelMessageReaction(channelId, messageId, encoded));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user