fix: honor accountId in message actions

This commit is contained in:
Peter Steinberger
2026-01-23 08:42:48 +00:00
parent c5546f0d5b
commit 13d1712850
14 changed files with 688 additions and 136 deletions

View File

@@ -188,4 +188,26 @@ describe("cron tool", () => {
const text = cronCall.params?.payload?.text ?? ""; const text = cronCall.params?.payload?.text ?? "";
expect(text).not.toContain("Recent context:"); expect(text).not.toContain("Recent context:");
}); });
it("preserves explicit agentId null on add", async () => {
callGatewayMock.mockResolvedValueOnce({ ok: true });
const tool = createCronTool({ agentSessionKey: "main" });
await tool.execute("call6", {
action: "add",
job: {
name: "reminder",
schedule: { atMs: 123 },
agentId: null,
payload: { kind: "systemEvent", text: "Reminder: the thing." },
},
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
method?: string;
params?: { agentId?: string | null };
};
expect(call.method).toBe("cron.add");
expect(call.params?.agentId).toBeNull();
});
}); });

View File

@@ -3,6 +3,7 @@ import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normal
import { loadConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js";
import { truncateUtf16Safe } from "../../utils.js"; import { truncateUtf16Safe } from "../../utils.js";
import { optionalStringEnum, stringEnum } from "../schema/typebox.js"; import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
import { resolveSessionAgentId } from "../agent-scope.js";
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js"; import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js"; import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js"; import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
@@ -158,6 +159,15 @@ export function createCronTool(opts?: CronToolOptions): AnyAgentTool {
throw new Error("job required"); throw new Error("job required");
} }
const job = normalizeCronJobCreate(params.job) ?? params.job; const job = normalizeCronJobCreate(params.job) ?? params.job;
if (job && typeof job === "object" && !("agentId" in job)) {
const cfg = loadConfig();
const agentId = opts?.agentSessionKey
? resolveSessionAgentId({ sessionKey: opts.agentSessionKey, config: cfg })
: undefined;
if (agentId) {
(job as { agentId?: string }).agentId = agentId;
}
}
const contextMessages = const contextMessages =
typeof params.contextMessages === "number" && Number.isFinite(params.contextMessages) typeof params.contextMessages === "number" && Number.isFinite(params.contextMessages)
? params.contextMessages ? params.contextMessages

View File

@@ -39,6 +39,7 @@ export async function handleDiscordGuildAction(
params: Record<string, unknown>, params: Record<string, unknown>,
isActionEnabled: ActionGate<DiscordActionConfig>, isActionEnabled: ActionGate<DiscordActionConfig>,
): Promise<AgentToolResult<unknown>> { ): Promise<AgentToolResult<unknown>> {
const accountId = readStringParam(params, "accountId");
switch (action) { switch (action) {
case "memberInfo": { case "memberInfo": {
if (!isActionEnabled("memberInfo")) { if (!isActionEnabled("memberInfo")) {
@@ -50,7 +51,9 @@ export async function handleDiscordGuildAction(
const userId = readStringParam(params, "userId", { const userId = readStringParam(params, "userId", {
required: true, required: true,
}); });
const member = await fetchMemberInfoDiscord(guildId, userId); const member = accountId
? await fetchMemberInfoDiscord(guildId, userId, { accountId })
: await fetchMemberInfoDiscord(guildId, userId);
return jsonResult({ ok: true, member }); return jsonResult({ ok: true, member });
} }
case "roleInfo": { case "roleInfo": {
@@ -60,7 +63,9 @@ export async function handleDiscordGuildAction(
const guildId = readStringParam(params, "guildId", { const guildId = readStringParam(params, "guildId", {
required: true, required: true,
}); });
const roles = await fetchRoleInfoDiscord(guildId); const roles = accountId
? await fetchRoleInfoDiscord(guildId, { accountId })
: await fetchRoleInfoDiscord(guildId);
return jsonResult({ ok: true, roles }); return jsonResult({ ok: true, roles });
} }
case "emojiList": { case "emojiList": {
@@ -70,7 +75,9 @@ export async function handleDiscordGuildAction(
const guildId = readStringParam(params, "guildId", { const guildId = readStringParam(params, "guildId", {
required: true, required: true,
}); });
const emojis = await listGuildEmojisDiscord(guildId); const emojis = accountId
? await listGuildEmojisDiscord(guildId, { accountId })
: await listGuildEmojisDiscord(guildId);
return jsonResult({ ok: true, emojis }); return jsonResult({ ok: true, emojis });
} }
case "emojiUpload": { case "emojiUpload": {
@@ -85,12 +92,22 @@ export async function handleDiscordGuildAction(
required: true, required: true,
}); });
const roleIds = readStringArrayParam(params, "roleIds"); const roleIds = readStringArrayParam(params, "roleIds");
const emoji = await uploadEmojiDiscord({ const emoji = accountId
guildId, ? await uploadEmojiDiscord(
name, {
mediaUrl, guildId,
roleIds: roleIds?.length ? roleIds : undefined, name,
}); mediaUrl,
roleIds: roleIds?.length ? roleIds : undefined,
},
{ accountId },
)
: await uploadEmojiDiscord({
guildId,
name,
mediaUrl,
roleIds: roleIds?.length ? roleIds : undefined,
});
return jsonResult({ ok: true, emoji }); return jsonResult({ ok: true, emoji });
} }
case "stickerUpload": { case "stickerUpload": {
@@ -108,13 +125,24 @@ export async function handleDiscordGuildAction(
const mediaUrl = readStringParam(params, "mediaUrl", { const mediaUrl = readStringParam(params, "mediaUrl", {
required: true, required: true,
}); });
const sticker = await uploadStickerDiscord({ const sticker = accountId
guildId, ? await uploadStickerDiscord(
name, {
description, guildId,
tags, name,
mediaUrl, description,
}); tags,
mediaUrl,
},
{ accountId },
)
: await uploadStickerDiscord({
guildId,
name,
description,
tags,
mediaUrl,
});
return jsonResult({ ok: true, sticker }); return jsonResult({ ok: true, sticker });
} }
case "roleAdd": { case "roleAdd": {
@@ -128,7 +156,11 @@ export async function handleDiscordGuildAction(
required: true, required: true,
}); });
const roleId = readStringParam(params, "roleId", { required: true }); const roleId = readStringParam(params, "roleId", { required: true });
await addRoleDiscord({ guildId, userId, roleId }); if (accountId) {
await addRoleDiscord({ guildId, userId, roleId }, { accountId });
} else {
await addRoleDiscord({ guildId, userId, roleId });
}
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
case "roleRemove": { case "roleRemove": {
@@ -142,7 +174,11 @@ export async function handleDiscordGuildAction(
required: true, required: true,
}); });
const roleId = readStringParam(params, "roleId", { required: true }); const roleId = readStringParam(params, "roleId", { required: true });
await removeRoleDiscord({ guildId, userId, roleId }); if (accountId) {
await removeRoleDiscord({ guildId, userId, roleId }, { accountId });
} else {
await removeRoleDiscord({ guildId, userId, roleId });
}
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
case "channelInfo": { case "channelInfo": {
@@ -152,7 +188,9 @@ export async function handleDiscordGuildAction(
const channelId = readStringParam(params, "channelId", { const channelId = readStringParam(params, "channelId", {
required: true, required: true,
}); });
const channel = await fetchChannelInfoDiscord(channelId); const channel = accountId
? await fetchChannelInfoDiscord(channelId, { accountId })
: await fetchChannelInfoDiscord(channelId);
return jsonResult({ ok: true, channel }); return jsonResult({ ok: true, channel });
} }
case "channelList": { case "channelList": {
@@ -162,7 +200,9 @@ export async function handleDiscordGuildAction(
const guildId = readStringParam(params, "guildId", { const guildId = readStringParam(params, "guildId", {
required: true, required: true,
}); });
const channels = await listGuildChannelsDiscord(guildId); const channels = accountId
? await listGuildChannelsDiscord(guildId, { accountId })
: await listGuildChannelsDiscord(guildId);
return jsonResult({ ok: true, channels }); return jsonResult({ ok: true, channels });
} }
case "voiceStatus": { case "voiceStatus": {
@@ -175,7 +215,9 @@ export async function handleDiscordGuildAction(
const userId = readStringParam(params, "userId", { const userId = readStringParam(params, "userId", {
required: true, required: true,
}); });
const voice = await fetchVoiceStatusDiscord(guildId, userId); const voice = accountId
? await fetchVoiceStatusDiscord(guildId, userId, { accountId })
: await fetchVoiceStatusDiscord(guildId, userId);
return jsonResult({ ok: true, voice }); return jsonResult({ ok: true, voice });
} }
case "eventList": { case "eventList": {
@@ -185,7 +227,9 @@ export async function handleDiscordGuildAction(
const guildId = readStringParam(params, "guildId", { const guildId = readStringParam(params, "guildId", {
required: true, required: true,
}); });
const events = await listScheduledEventsDiscord(guildId); const events = accountId
? await listScheduledEventsDiscord(guildId, { accountId })
: await listScheduledEventsDiscord(guildId);
return jsonResult({ ok: true, events }); return jsonResult({ ok: true, events });
} }
case "eventCreate": { case "eventCreate": {
@@ -215,7 +259,9 @@ export async function handleDiscordGuildAction(
entity_metadata: entityType === 3 && location ? { location } : undefined, entity_metadata: entityType === 3 && location ? { location } : undefined,
privacy_level: 2, privacy_level: 2,
}; };
const event = await createScheduledEventDiscord(guildId, payload); const event = accountId
? await createScheduledEventDiscord(guildId, payload, { accountId })
: await createScheduledEventDiscord(guildId, payload);
return jsonResult({ ok: true, event }); return jsonResult({ ok: true, event });
} }
case "channelCreate": { case "channelCreate": {
@@ -229,15 +275,28 @@ export async function handleDiscordGuildAction(
const topic = readStringParam(params, "topic"); const topic = readStringParam(params, "topic");
const position = readNumberParam(params, "position", { integer: true }); const position = readNumberParam(params, "position", { integer: true });
const nsfw = params.nsfw as boolean | undefined; const nsfw = params.nsfw as boolean | undefined;
const channel = await createChannelDiscord({ const channel = accountId
guildId, ? await createChannelDiscord(
name, {
type: type ?? undefined, guildId,
parentId: parentId ?? undefined, name,
topic: topic ?? undefined, type: type ?? undefined,
position: position ?? undefined, parentId: parentId ?? undefined,
nsfw, topic: topic ?? undefined,
}); position: position ?? undefined,
nsfw,
},
{ accountId },
)
: await createChannelDiscord({
guildId,
name,
type: type ?? undefined,
parentId: parentId ?? undefined,
topic: topic ?? undefined,
position: position ?? undefined,
nsfw,
});
return jsonResult({ ok: true, channel }); return jsonResult({ ok: true, channel });
} }
case "channelEdit": { case "channelEdit": {
@@ -255,15 +314,28 @@ export async function handleDiscordGuildAction(
const rateLimitPerUser = readNumberParam(params, "rateLimitPerUser", { const rateLimitPerUser = readNumberParam(params, "rateLimitPerUser", {
integer: true, integer: true,
}); });
const channel = await editChannelDiscord({ const channel = accountId
channelId, ? await editChannelDiscord(
name: name ?? undefined, {
topic: topic ?? undefined, channelId,
position: position ?? undefined, name: name ?? undefined,
parentId, topic: topic ?? undefined,
nsfw, position: position ?? undefined,
rateLimitPerUser: rateLimitPerUser ?? undefined, parentId,
}); nsfw,
rateLimitPerUser: rateLimitPerUser ?? undefined,
},
{ accountId },
)
: await editChannelDiscord({
channelId,
name: name ?? undefined,
topic: topic ?? undefined,
position: position ?? undefined,
parentId,
nsfw,
rateLimitPerUser: rateLimitPerUser ?? undefined,
});
return jsonResult({ ok: true, channel }); return jsonResult({ ok: true, channel });
} }
case "channelDelete": { case "channelDelete": {
@@ -273,7 +345,9 @@ export async function handleDiscordGuildAction(
const channelId = readStringParam(params, "channelId", { const channelId = readStringParam(params, "channelId", {
required: true, required: true,
}); });
const result = await deleteChannelDiscord(channelId); const result = accountId
? await deleteChannelDiscord(channelId, { accountId })
: await deleteChannelDiscord(channelId);
return jsonResult(result); return jsonResult(result);
} }
case "channelMove": { case "channelMove": {
@@ -286,12 +360,24 @@ export async function handleDiscordGuildAction(
}); });
const parentId = readParentIdParam(params); const parentId = readParentIdParam(params);
const position = readNumberParam(params, "position", { integer: true }); const position = readNumberParam(params, "position", { integer: true });
await moveChannelDiscord({ if (accountId) {
guildId, await moveChannelDiscord(
channelId, {
parentId, guildId,
position: position ?? undefined, channelId,
}); parentId,
position: position ?? undefined,
},
{ accountId },
);
} else {
await moveChannelDiscord({
guildId,
channelId,
parentId,
position: position ?? undefined,
});
}
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
case "categoryCreate": { case "categoryCreate": {
@@ -301,12 +387,22 @@ export async function handleDiscordGuildAction(
const guildId = readStringParam(params, "guildId", { required: true }); const guildId = readStringParam(params, "guildId", { required: true });
const name = readStringParam(params, "name", { required: true }); const name = readStringParam(params, "name", { required: true });
const position = readNumberParam(params, "position", { integer: true }); const position = readNumberParam(params, "position", { integer: true });
const channel = await createChannelDiscord({ const channel = accountId
guildId, ? await createChannelDiscord(
name, {
type: 4, guildId,
position: position ?? undefined, name,
}); type: 4,
position: position ?? undefined,
},
{ accountId },
)
: await createChannelDiscord({
guildId,
name,
type: 4,
position: position ?? undefined,
});
return jsonResult({ ok: true, category: channel }); return jsonResult({ ok: true, category: channel });
} }
case "categoryEdit": { case "categoryEdit": {
@@ -318,11 +414,20 @@ export async function handleDiscordGuildAction(
}); });
const name = readStringParam(params, "name"); const name = readStringParam(params, "name");
const position = readNumberParam(params, "position", { integer: true }); const position = readNumberParam(params, "position", { integer: true });
const channel = await editChannelDiscord({ const channel = accountId
channelId: categoryId, ? await editChannelDiscord(
name: name ?? undefined, {
position: position ?? undefined, channelId: categoryId,
}); name: name ?? undefined,
position: position ?? undefined,
},
{ accountId },
)
: await editChannelDiscord({
channelId: categoryId,
name: name ?? undefined,
position: position ?? undefined,
});
return jsonResult({ ok: true, category: channel }); return jsonResult({ ok: true, category: channel });
} }
case "categoryDelete": { case "categoryDelete": {
@@ -332,7 +437,9 @@ export async function handleDiscordGuildAction(
const categoryId = readStringParam(params, "categoryId", { const categoryId = readStringParam(params, "categoryId", {
required: true, required: true,
}); });
const result = await deleteChannelDiscord(categoryId); const result = accountId
? await deleteChannelDiscord(categoryId, { accountId })
: await deleteChannelDiscord(categoryId);
return jsonResult(result); return jsonResult(result);
} }
case "channelPermissionSet": { case "channelPermissionSet": {
@@ -349,13 +456,26 @@ export async function handleDiscordGuildAction(
const targetType = targetTypeRaw === "member" ? 1 : 0; const targetType = targetTypeRaw === "member" ? 1 : 0;
const allow = readStringParam(params, "allow"); const allow = readStringParam(params, "allow");
const deny = readStringParam(params, "deny"); const deny = readStringParam(params, "deny");
await setChannelPermissionDiscord({ if (accountId) {
channelId, await setChannelPermissionDiscord(
targetId, {
targetType, channelId,
allow: allow ?? undefined, targetId,
deny: deny ?? undefined, targetType,
}); allow: allow ?? undefined,
deny: deny ?? undefined,
},
{ accountId },
);
} else {
await setChannelPermissionDiscord({
channelId,
targetId,
targetType,
allow: allow ?? undefined,
deny: deny ?? undefined,
});
}
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
case "channelPermissionRemove": { case "channelPermissionRemove": {
@@ -366,7 +486,11 @@ export async function handleDiscordGuildAction(
required: true, required: true,
}); });
const targetId = readStringParam(params, "targetId", { required: true }); const targetId = readStringParam(params, "targetId", { required: true });
await removeChannelPermissionDiscord(channelId, targetId); if (accountId) {
await removeChannelPermissionDiscord(channelId, targetId, { accountId });
} else {
await removeChannelPermissionDiscord(channelId, targetId);
}
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
default: default:

View File

@@ -58,6 +58,7 @@ export async function handleDiscordMessagingAction(
required: true, required: true,
}), }),
); );
const accountId = readStringParam(params, "accountId");
const normalizeMessage = (message: unknown) => { const normalizeMessage = (message: unknown) => {
if (!message || typeof message !== "object") return message; if (!message || typeof message !== "object") return message;
return withNormalizedTimestamp( return withNormalizedTimestamp(
@@ -78,14 +79,24 @@ export async function handleDiscordMessagingAction(
removeErrorMessage: "Emoji is required to remove a Discord reaction.", removeErrorMessage: "Emoji is required to remove a Discord reaction.",
}); });
if (remove) { if (remove) {
await removeReactionDiscord(channelId, messageId, emoji); if (accountId) {
await removeReactionDiscord(channelId, messageId, emoji, { accountId });
} else {
await removeReactionDiscord(channelId, messageId, emoji);
}
return jsonResult({ ok: true, removed: emoji }); return jsonResult({ ok: true, removed: emoji });
} }
if (isEmpty) { if (isEmpty) {
const removed = await removeOwnReactionsDiscord(channelId, messageId); const removed = accountId
? await removeOwnReactionsDiscord(channelId, messageId, { accountId })
: await removeOwnReactionsDiscord(channelId, messageId);
return jsonResult({ ok: true, removed: removed.removed }); return jsonResult({ ok: true, removed: removed.removed });
} }
await reactMessageDiscord(channelId, messageId, emoji); if (accountId) {
await reactMessageDiscord(channelId, messageId, emoji, { accountId });
} else {
await reactMessageDiscord(channelId, messageId, emoji);
}
return jsonResult({ ok: true, added: emoji }); return jsonResult({ ok: true, added: emoji });
} }
case "reactions": { case "reactions": {
@@ -100,6 +111,7 @@ export async function handleDiscordMessagingAction(
const limit = const limit =
typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined; typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined;
const reactions = await fetchReactionsDiscord(channelId, messageId, { const reactions = await fetchReactionsDiscord(channelId, messageId, {
...(accountId ? { accountId } : {}),
limit, limit,
}); });
return jsonResult({ ok: true, reactions }); return jsonResult({ ok: true, reactions });
@@ -114,7 +126,10 @@ export async function handleDiscordMessagingAction(
required: true, required: true,
label: "stickerIds", label: "stickerIds",
}); });
await sendStickerDiscord(to, stickerIds, { content }); await sendStickerDiscord(to, stickerIds, {
...(accountId ? { accountId } : {}),
content,
});
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
case "poll": { case "poll": {
@@ -140,7 +155,7 @@ export async function handleDiscordMessagingAction(
await sendPollDiscord( await sendPollDiscord(
to, to,
{ question, options: answers, maxSelections, durationHours }, { question, options: answers, maxSelections, durationHours },
{ content }, { ...(accountId ? { accountId } : {}), content },
); );
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
@@ -149,7 +164,9 @@ export async function handleDiscordMessagingAction(
throw new Error("Discord permissions are disabled."); throw new Error("Discord permissions are disabled.");
} }
const channelId = resolveChannelId(); const channelId = resolveChannelId();
const permissions = await fetchChannelPermissionsDiscord(channelId); const permissions = accountId
? await fetchChannelPermissionsDiscord(channelId, { accountId })
: await fetchChannelPermissionsDiscord(channelId);
return jsonResult({ ok: true, permissions }); return jsonResult({ ok: true, permissions });
} }
case "fetchMessage": { case "fetchMessage": {
@@ -171,7 +188,9 @@ export async function handleDiscordMessagingAction(
"Discord message fetch requires guildId, channelId, and messageId (or a valid messageLink).", "Discord message fetch requires guildId, channelId, and messageId (or a valid messageLink).",
); );
} }
const message = await fetchMessageDiscord(channelId, messageId); const message = accountId
? await fetchMessageDiscord(channelId, messageId, { accountId })
: await fetchMessageDiscord(channelId, messageId);
return jsonResult({ return jsonResult({
ok: true, ok: true,
message: normalizeMessage(message), message: normalizeMessage(message),
@@ -185,7 +204,7 @@ export async function handleDiscordMessagingAction(
throw new Error("Discord message reads are disabled."); throw new Error("Discord message reads are disabled.");
} }
const channelId = resolveChannelId(); const channelId = resolveChannelId();
const messages = await readMessagesDiscord(channelId, { const query = {
limit: limit:
typeof params.limit === "number" && Number.isFinite(params.limit) typeof params.limit === "number" && Number.isFinite(params.limit)
? params.limit ? params.limit
@@ -193,7 +212,10 @@ export async function handleDiscordMessagingAction(
before: readStringParam(params, "before"), before: readStringParam(params, "before"),
after: readStringParam(params, "after"), after: readStringParam(params, "after"),
around: readStringParam(params, "around"), around: readStringParam(params, "around"),
}); };
const messages = accountId
? await readMessagesDiscord(channelId, query, { accountId })
: await readMessagesDiscord(channelId, query);
return jsonResult({ return jsonResult({
ok: true, ok: true,
messages: messages.map((message) => normalizeMessage(message)), messages: messages.map((message) => normalizeMessage(message)),
@@ -212,6 +234,7 @@ export async function handleDiscordMessagingAction(
const embeds = const embeds =
Array.isArray(params.embeds) && params.embeds.length > 0 ? params.embeds : undefined; Array.isArray(params.embeds) && params.embeds.length > 0 ? params.embeds : undefined;
const result = await sendMessageDiscord(to, content, { const result = await sendMessageDiscord(to, content, {
...(accountId ? { accountId } : {}),
mediaUrl, mediaUrl,
replyTo, replyTo,
embeds, embeds,
@@ -229,9 +252,9 @@ export async function handleDiscordMessagingAction(
const content = readStringParam(params, "content", { const content = readStringParam(params, "content", {
required: true, required: true,
}); });
const message = await editMessageDiscord(channelId, messageId, { const message = accountId
content, ? await editMessageDiscord(channelId, messageId, { content }, { accountId })
}); : await editMessageDiscord(channelId, messageId, { content });
return jsonResult({ ok: true, message }); return jsonResult({ ok: true, message });
} }
case "deleteMessage": { case "deleteMessage": {
@@ -242,7 +265,11 @@ export async function handleDiscordMessagingAction(
const messageId = readStringParam(params, "messageId", { const messageId = readStringParam(params, "messageId", {
required: true, required: true,
}); });
await deleteMessageDiscord(channelId, messageId); if (accountId) {
await deleteMessageDiscord(channelId, messageId, { accountId });
} else {
await deleteMessageDiscord(channelId, messageId);
}
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
case "threadCreate": { case "threadCreate": {
@@ -257,11 +284,13 @@ export async function handleDiscordMessagingAction(
typeof autoArchiveMinutesRaw === "number" && Number.isFinite(autoArchiveMinutesRaw) typeof autoArchiveMinutesRaw === "number" && Number.isFinite(autoArchiveMinutesRaw)
? autoArchiveMinutesRaw ? autoArchiveMinutesRaw
: undefined; : undefined;
const thread = await createThreadDiscord(channelId, { const thread = accountId
name, ? await createThreadDiscord(
messageId, channelId,
autoArchiveMinutes, { name, messageId, autoArchiveMinutes },
}); { accountId },
)
: await createThreadDiscord(channelId, { name, messageId, autoArchiveMinutes });
return jsonResult({ ok: true, thread }); return jsonResult({ ok: true, thread });
} }
case "threadList": { case "threadList": {
@@ -279,13 +308,24 @@ export async function handleDiscordMessagingAction(
typeof params.limit === "number" && Number.isFinite(params.limit) typeof params.limit === "number" && Number.isFinite(params.limit)
? params.limit ? params.limit
: undefined; : undefined;
const threads = await listThreadsDiscord({ const threads = accountId
guildId, ? await listThreadsDiscord(
channelId, {
includeArchived, guildId,
before, channelId,
limit, includeArchived,
}); before,
limit,
},
{ accountId },
)
: await listThreadsDiscord({
guildId,
channelId,
includeArchived,
before,
limit,
});
return jsonResult({ ok: true, threads }); return jsonResult({ ok: true, threads });
} }
case "threadReply": { case "threadReply": {
@@ -299,6 +339,7 @@ export async function handleDiscordMessagingAction(
const mediaUrl = readStringParam(params, "mediaUrl"); const mediaUrl = readStringParam(params, "mediaUrl");
const replyTo = readStringParam(params, "replyTo"); const replyTo = readStringParam(params, "replyTo");
const result = await sendMessageDiscord(`channel:${channelId}`, content, { const result = await sendMessageDiscord(`channel:${channelId}`, content, {
...(accountId ? { accountId } : {}),
mediaUrl, mediaUrl,
replyTo, replyTo,
}); });
@@ -312,7 +353,11 @@ export async function handleDiscordMessagingAction(
const messageId = readStringParam(params, "messageId", { const messageId = readStringParam(params, "messageId", {
required: true, required: true,
}); });
await pinMessageDiscord(channelId, messageId); if (accountId) {
await pinMessageDiscord(channelId, messageId, { accountId });
} else {
await pinMessageDiscord(channelId, messageId);
}
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
case "unpinMessage": { case "unpinMessage": {
@@ -323,7 +368,11 @@ export async function handleDiscordMessagingAction(
const messageId = readStringParam(params, "messageId", { const messageId = readStringParam(params, "messageId", {
required: true, required: true,
}); });
await unpinMessageDiscord(channelId, messageId); if (accountId) {
await unpinMessageDiscord(channelId, messageId, { accountId });
} else {
await unpinMessageDiscord(channelId, messageId);
}
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
case "listPins": { case "listPins": {
@@ -331,7 +380,9 @@ export async function handleDiscordMessagingAction(
throw new Error("Discord pins are disabled."); throw new Error("Discord pins are disabled.");
} }
const channelId = resolveChannelId(); const channelId = resolveChannelId();
const pins = await listPinsDiscord(channelId); const pins = accountId
? await listPinsDiscord(channelId, { accountId })
: await listPinsDiscord(channelId);
return jsonResult({ ok: true, pins: pins.map((pin) => normalizeMessage(pin)) }); return jsonResult({ ok: true, pins: pins.map((pin) => normalizeMessage(pin)) });
} }
case "searchMessages": { case "searchMessages": {
@@ -354,13 +405,24 @@ export async function handleDiscordMessagingAction(
: undefined; : undefined;
const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])]; const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])];
const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])]; const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])];
const results = await searchMessagesDiscord({ const results = accountId
guildId, ? await searchMessagesDiscord(
content, {
channelIds: channelIdList.length ? channelIdList : undefined, guildId,
authorIds: authorIdList.length ? authorIdList : undefined, content,
limit, channelIds: channelIdList.length ? channelIdList : undefined,
}); authorIds: authorIdList.length ? authorIdList : undefined,
limit,
},
{ accountId },
)
: await searchMessagesDiscord({
guildId,
content,
channelIds: channelIdList.length ? channelIdList : undefined,
authorIds: authorIdList.length ? authorIdList : undefined,
limit,
});
if (!results || typeof results !== "object") { if (!results || typeof results !== "object") {
return jsonResult({ ok: true, results }); return jsonResult({ ok: true, results });
} }

View File

@@ -8,6 +8,7 @@ export async function handleDiscordModerationAction(
params: Record<string, unknown>, params: Record<string, unknown>,
isActionEnabled: ActionGate<DiscordActionConfig>, isActionEnabled: ActionGate<DiscordActionConfig>,
): Promise<AgentToolResult<unknown>> { ): Promise<AgentToolResult<unknown>> {
const accountId = readStringParam(params, "accountId");
switch (action) { switch (action) {
case "timeout": { case "timeout": {
if (!isActionEnabled("moderation", false)) { if (!isActionEnabled("moderation", false)) {
@@ -25,13 +26,24 @@ export async function handleDiscordModerationAction(
: undefined; : undefined;
const until = readStringParam(params, "until"); const until = readStringParam(params, "until");
const reason = readStringParam(params, "reason"); const reason = readStringParam(params, "reason");
const member = await timeoutMemberDiscord({ const member = accountId
guildId, ? await timeoutMemberDiscord(
userId, {
durationMinutes, guildId,
until, userId,
reason, durationMinutes,
}); until,
reason,
},
{ accountId },
)
: await timeoutMemberDiscord({
guildId,
userId,
durationMinutes,
until,
reason,
});
return jsonResult({ ok: true, member }); return jsonResult({ ok: true, member });
} }
case "kick": { case "kick": {
@@ -45,7 +57,11 @@ export async function handleDiscordModerationAction(
required: true, required: true,
}); });
const reason = readStringParam(params, "reason"); const reason = readStringParam(params, "reason");
await kickMemberDiscord({ guildId, userId, reason }); if (accountId) {
await kickMemberDiscord({ guildId, userId, reason }, { accountId });
} else {
await kickMemberDiscord({ guildId, userId, reason });
}
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
case "ban": { case "ban": {
@@ -63,12 +79,24 @@ export async function handleDiscordModerationAction(
typeof params.deleteMessageDays === "number" && Number.isFinite(params.deleteMessageDays) typeof params.deleteMessageDays === "number" && Number.isFinite(params.deleteMessageDays)
? params.deleteMessageDays ? params.deleteMessageDays
: undefined; : undefined;
await banMemberDiscord({ if (accountId) {
guildId, await banMemberDiscord(
userId, {
reason, guildId,
deleteMessageDays, userId,
}); reason,
deleteMessageDays,
},
{ accountId },
);
} else {
await banMemberDiscord({
guildId,
userId,
reason,
deleteMessageDays,
});
}
return jsonResult({ ok: true }); return jsonResult({ ok: true });
} }
default: default:

View File

@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest";
import type { DiscordActionConfig } from "../../config/config.js"; import type { DiscordActionConfig } from "../../config/config.js";
import { handleDiscordGuildAction } from "./discord-actions-guild.js"; import { handleDiscordGuildAction } from "./discord-actions-guild.js";
import { handleDiscordMessagingAction } from "./discord-actions-messaging.js"; import { handleDiscordMessagingAction } from "./discord-actions-messaging.js";
import { handleDiscordModerationAction } from "./discord-actions-moderation.js";
const createChannelDiscord = vi.fn(async () => ({ const createChannelDiscord = vi.fn(async () => ({
id: "new-channel", id: "new-channel",
@@ -35,8 +36,12 @@ const sendPollDiscord = vi.fn(async () => ({}));
const sendStickerDiscord = vi.fn(async () => ({})); const sendStickerDiscord = vi.fn(async () => ({}));
const setChannelPermissionDiscord = vi.fn(async () => ({ ok: true })); const setChannelPermissionDiscord = vi.fn(async () => ({ ok: true }));
const unpinMessageDiscord = vi.fn(async () => ({})); const unpinMessageDiscord = vi.fn(async () => ({}));
const timeoutMemberDiscord = vi.fn(async () => ({}));
const kickMemberDiscord = vi.fn(async () => ({}));
const banMemberDiscord = vi.fn(async () => ({}));
vi.mock("../../discord/send.js", () => ({ vi.mock("../../discord/send.js", () => ({
banMemberDiscord: (...args: unknown[]) => banMemberDiscord(...args),
createChannelDiscord: (...args: unknown[]) => createChannelDiscord(...args), createChannelDiscord: (...args: unknown[]) => createChannelDiscord(...args),
createThreadDiscord: (...args: unknown[]) => createThreadDiscord(...args), createThreadDiscord: (...args: unknown[]) => createThreadDiscord(...args),
deleteChannelDiscord: (...args: unknown[]) => deleteChannelDiscord(...args), deleteChannelDiscord: (...args: unknown[]) => deleteChannelDiscord(...args),
@@ -46,6 +51,7 @@ vi.mock("../../discord/send.js", () => ({
fetchMessageDiscord: (...args: unknown[]) => fetchMessageDiscord(...args), fetchMessageDiscord: (...args: unknown[]) => fetchMessageDiscord(...args),
fetchChannelPermissionsDiscord: (...args: unknown[]) => fetchChannelPermissionsDiscord(...args), fetchChannelPermissionsDiscord: (...args: unknown[]) => fetchChannelPermissionsDiscord(...args),
fetchReactionsDiscord: (...args: unknown[]) => fetchReactionsDiscord(...args), fetchReactionsDiscord: (...args: unknown[]) => fetchReactionsDiscord(...args),
kickMemberDiscord: (...args: unknown[]) => kickMemberDiscord(...args),
listPinsDiscord: (...args: unknown[]) => listPinsDiscord(...args), listPinsDiscord: (...args: unknown[]) => listPinsDiscord(...args),
listThreadsDiscord: (...args: unknown[]) => listThreadsDiscord(...args), listThreadsDiscord: (...args: unknown[]) => listThreadsDiscord(...args),
moveChannelDiscord: (...args: unknown[]) => moveChannelDiscord(...args), moveChannelDiscord: (...args: unknown[]) => moveChannelDiscord(...args),
@@ -60,12 +66,15 @@ vi.mock("../../discord/send.js", () => ({
sendPollDiscord: (...args: unknown[]) => sendPollDiscord(...args), sendPollDiscord: (...args: unknown[]) => sendPollDiscord(...args),
sendStickerDiscord: (...args: unknown[]) => sendStickerDiscord(...args), sendStickerDiscord: (...args: unknown[]) => sendStickerDiscord(...args),
setChannelPermissionDiscord: (...args: unknown[]) => setChannelPermissionDiscord(...args), setChannelPermissionDiscord: (...args: unknown[]) => setChannelPermissionDiscord(...args),
timeoutMemberDiscord: (...args: unknown[]) => timeoutMemberDiscord(...args),
unpinMessageDiscord: (...args: unknown[]) => unpinMessageDiscord(...args), unpinMessageDiscord: (...args: unknown[]) => unpinMessageDiscord(...args),
})); }));
const enableAllActions = () => true; const enableAllActions = () => true;
const disabledActions = (key: keyof DiscordActionConfig) => key !== "reactions"; const disabledActions = (key: keyof DiscordActionConfig) => key !== "reactions";
const channelInfoEnabled = (key: keyof DiscordActionConfig) => key === "channelInfo";
const moderationEnabled = (key: keyof DiscordActionConfig) => key === "moderation";
describe("handleDiscordMessagingAction", () => { describe("handleDiscordMessagingAction", () => {
it("adds reactions", async () => { it("adds reactions", async () => {
@@ -81,6 +90,20 @@ describe("handleDiscordMessagingAction", () => {
expect(reactMessageDiscord).toHaveBeenCalledWith("C1", "M1", "✅"); expect(reactMessageDiscord).toHaveBeenCalledWith("C1", "M1", "✅");
}); });
it("forwards accountId for reactions", async () => {
await handleDiscordMessagingAction(
"react",
{
channelId: "C1",
messageId: "M1",
emoji: "✅",
accountId: "ops",
},
enableAllActions,
);
expect(reactMessageDiscord).toHaveBeenCalledWith("C1", "M1", "✅", { accountId: "ops" });
});
it("removes reactions on empty emoji", async () => { it("removes reactions on empty emoji", async () => {
await handleDiscordMessagingAction( await handleDiscordMessagingAction(
"react", "react",
@@ -245,6 +268,15 @@ describe("handleDiscordGuildAction - channel management", () => {
).rejects.toThrow(/Discord channel management is disabled/); ).rejects.toThrow(/Discord channel management is disabled/);
}); });
it("forwards accountId for channelList", async () => {
await handleDiscordGuildAction(
"channelList",
{ guildId: "G1", accountId: "ops" },
channelInfoEnabled,
);
expect(listGuildChannelsDiscord).toHaveBeenCalledWith("G1", { accountId: "ops" });
});
it("edits a channel", async () => { it("edits a channel", async () => {
await handleDiscordGuildAction( await handleDiscordGuildAction(
"channelEdit", "channelEdit",
@@ -448,3 +480,26 @@ describe("handleDiscordGuildAction - channel management", () => {
expect(removeChannelPermissionDiscord).toHaveBeenCalledWith("C1", "R1"); expect(removeChannelPermissionDiscord).toHaveBeenCalledWith("C1", "R1");
}); });
}); });
describe("handleDiscordModerationAction", () => {
it("forwards accountId for timeout", async () => {
await handleDiscordModerationAction(
"timeout",
{
guildId: "G1",
userId: "U1",
durationMinutes: 5,
accountId: "ops",
},
moderationEnabled,
);
expect(timeoutMemberDiscord).toHaveBeenCalledWith(
expect.objectContaining({
guildId: "G1",
userId: "U1",
durationMinutes: 5,
}),
{ accountId: "ops" },
);
});
});

View File

@@ -342,6 +342,9 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
}) as ChannelMessageActionName; }) as ChannelMessageActionName;
const accountId = readStringParam(params, "accountId") ?? agentAccountId; const accountId = readStringParam(params, "accountId") ?? agentAccountId;
if (accountId) {
params.accountId = accountId;
}
const gateway = { const gateway = {
url: readStringParam(params, "gatewayUrl", { trim: false }), url: readStringParam(params, "gatewayUrl", { trim: false }),

View File

@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it, vi } from "vitest";
import type { ClawdbotConfig } from "../../../config/config.js"; import type { ClawdbotConfig } from "../../../config/config.js";
type SendMessageDiscord = typeof import("../../../discord/send.js").sendMessageDiscord; type SendMessageDiscord = typeof import("../../../discord/send.js").sendMessageDiscord;
@@ -32,6 +32,32 @@ const loadDiscordMessageActions = async () => {
return mod.discordMessageActions; return mod.discordMessageActions;
}; };
type SendMessageDiscord = typeof import("../../../discord/send.js").sendMessageDiscord;
type SendPollDiscord = typeof import("../../../discord/send.js").sendPollDiscord;
const sendMessageDiscord = vi.fn<Parameters<SendMessageDiscord>, ReturnType<SendMessageDiscord>>(
async () => ({ ok: true }) as Awaited<ReturnType<SendMessageDiscord>>,
);
const sendPollDiscord = vi.fn<Parameters<SendPollDiscord>, ReturnType<SendPollDiscord>>(
async () => ({ ok: true }) as Awaited<ReturnType<SendPollDiscord>>,
);
vi.mock("../../../discord/send.js", async () => {
const actual = await vi.importActual<typeof import("../../../discord/send.js")>(
"../../../discord/send.js",
);
return {
...actual,
sendMessageDiscord: (...args: Parameters<SendMessageDiscord>) => sendMessageDiscord(...args),
sendPollDiscord: (...args: Parameters<SendPollDiscord>) => sendPollDiscord(...args),
};
});
const loadHandleDiscordMessageAction = async () => {
const mod = await import("./discord/handle-action.js");
return mod.handleDiscordMessageAction;
};
describe("discord message actions", () => { describe("discord message actions", () => {
it("lists channel and upload actions by default", async () => { it("lists channel and upload actions by default", async () => {
const cfg = { channels: { discord: { token: "d0" } } } as ClawdbotConfig; const cfg = { channels: { discord: { token: "d0" } } } as ClawdbotConfig;
@@ -53,3 +79,78 @@ describe("discord message actions", () => {
expect(actions).not.toContain("channel-create"); expect(actions).not.toContain("channel-create");
}); });
}); });
describe("handleDiscordMessageAction", () => {
it("forwards context accountId for send", async () => {
sendMessageDiscord.mockClear();
const handleDiscordMessageAction = await loadHandleDiscordMessageAction();
await handleDiscordMessageAction({
action: "send",
params: {
to: "channel:123",
message: "hi",
},
cfg: {} as ClawdbotConfig,
accountId: "ops",
});
expect(sendMessageDiscord).toHaveBeenCalledWith(
"channel:123",
"hi",
expect.objectContaining({
accountId: "ops",
}),
);
});
it("falls back to params accountId when context missing", async () => {
sendPollDiscord.mockClear();
const handleDiscordMessageAction = await loadHandleDiscordMessageAction();
await handleDiscordMessageAction({
action: "poll",
params: {
to: "channel:123",
pollQuestion: "Ready?",
pollOption: ["Yes", "No"],
accountId: "marve",
},
cfg: {} as ClawdbotConfig,
});
expect(sendPollDiscord).toHaveBeenCalledWith(
"channel:123",
expect.objectContaining({
question: "Ready?",
options: ["Yes", "No"],
}),
expect.objectContaining({
accountId: "marve",
}),
);
});
it("forwards accountId for thread replies", async () => {
sendMessageDiscord.mockClear();
const handleDiscordMessageAction = await loadHandleDiscordMessageAction();
await handleDiscordMessageAction({
action: "thread-reply",
params: {
channelId: "123",
message: "hi",
},
cfg: {} as ClawdbotConfig,
accountId: "ops",
});
expect(sendMessageDiscord).toHaveBeenCalledWith(
"channel:123",
"hi",
expect.objectContaining({
accountId: "ops",
}),
);
});
});

View File

@@ -80,7 +80,7 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
} }
return null; return null;
}, },
handleAction: async ({ action, params, cfg }) => { handleAction: async ({ action, params, cfg, accountId }) => {
return await handleDiscordMessageAction({ action, params, cfg }); return await handleDiscordMessageAction({ action, params, cfg, accountId });
}, },
}; };

View File

@@ -7,7 +7,7 @@ import {
import { handleDiscordAction } from "../../../../agents/tools/discord-actions.js"; import { handleDiscordAction } from "../../../../agents/tools/discord-actions.js";
import type { ChannelMessageActionContext } from "../../types.js"; import type { ChannelMessageActionContext } from "../../types.js";
type Ctx = Pick<ChannelMessageActionContext, "action" | "params" | "cfg">; type Ctx = Pick<ChannelMessageActionContext, "action" | "params" | "cfg" | "accountId">;
export async function tryHandleDiscordMessageActionGuildAdmin(params: { export async function tryHandleDiscordMessageActionGuildAdmin(params: {
ctx: Ctx; ctx: Ctx;
@@ -16,27 +16,37 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
}): Promise<AgentToolResult<unknown> | undefined> { }): Promise<AgentToolResult<unknown> | undefined> {
const { ctx, resolveChannelId, readParentIdParam } = params; const { ctx, resolveChannelId, readParentIdParam } = params;
const { action, params: actionParams, cfg } = ctx; const { action, params: actionParams, cfg } = ctx;
const accountId = ctx.accountId ?? readStringParam(actionParams, "accountId");
if (action === "member-info") { if (action === "member-info") {
const userId = readStringParam(actionParams, "userId", { required: true }); const userId = readStringParam(actionParams, "userId", { required: true });
const guildId = readStringParam(actionParams, "guildId", { const guildId = readStringParam(actionParams, "guildId", {
required: true, required: true,
}); });
return await handleDiscordAction({ action: "memberInfo", guildId, userId }, cfg); return await handleDiscordAction(
{ action: "memberInfo", accountId: accountId ?? undefined, guildId, userId },
cfg,
);
} }
if (action === "role-info") { if (action === "role-info") {
const guildId = readStringParam(actionParams, "guildId", { const guildId = readStringParam(actionParams, "guildId", {
required: true, required: true,
}); });
return await handleDiscordAction({ action: "roleInfo", guildId }, cfg); return await handleDiscordAction(
{ action: "roleInfo", accountId: accountId ?? undefined, guildId },
cfg,
);
} }
if (action === "emoji-list") { if (action === "emoji-list") {
const guildId = readStringParam(actionParams, "guildId", { const guildId = readStringParam(actionParams, "guildId", {
required: true, required: true,
}); });
return await handleDiscordAction({ action: "emojiList", guildId }, cfg); return await handleDiscordAction(
{ action: "emojiList", accountId: accountId ?? undefined, guildId },
cfg,
);
} }
if (action === "emoji-upload") { if (action === "emoji-upload") {
@@ -50,7 +60,14 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
}); });
const roleIds = readStringArrayParam(actionParams, "roleIds"); const roleIds = readStringArrayParam(actionParams, "roleIds");
return await handleDiscordAction( return await handleDiscordAction(
{ action: "emojiUpload", guildId, name, mediaUrl, roleIds }, {
action: "emojiUpload",
accountId: accountId ?? undefined,
guildId,
name,
mediaUrl,
roleIds,
},
cfg, cfg,
); );
} }
@@ -73,7 +90,15 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
trim: false, trim: false,
}); });
return await handleDiscordAction( return await handleDiscordAction(
{ action: "stickerUpload", guildId, name, description, tags, mediaUrl }, {
action: "stickerUpload",
accountId: accountId ?? undefined,
guildId,
name,
description,
tags,
mediaUrl,
},
cfg, cfg,
); );
} }
@@ -87,6 +112,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: action === "role-add" ? "roleAdd" : "roleRemove", action: action === "role-add" ? "roleAdd" : "roleRemove",
accountId: accountId ?? undefined,
guildId, guildId,
userId, userId,
roleId, roleId,
@@ -99,14 +125,20 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
const channelId = readStringParam(actionParams, "channelId", { const channelId = readStringParam(actionParams, "channelId", {
required: true, required: true,
}); });
return await handleDiscordAction({ action: "channelInfo", channelId }, cfg); return await handleDiscordAction(
{ action: "channelInfo", accountId: accountId ?? undefined, channelId },
cfg,
);
} }
if (action === "channel-list") { if (action === "channel-list") {
const guildId = readStringParam(actionParams, "guildId", { const guildId = readStringParam(actionParams, "guildId", {
required: true, required: true,
}); });
return await handleDiscordAction({ action: "channelList", guildId }, cfg); return await handleDiscordAction(
{ action: "channelList", accountId: accountId ?? undefined, guildId },
cfg,
);
} }
if (action === "channel-create") { if (action === "channel-create") {
@@ -124,6 +156,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "channelCreate", action: "channelCreate",
accountId: accountId ?? undefined,
guildId, guildId,
name, name,
type: type ?? undefined, type: type ?? undefined,
@@ -153,6 +186,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "channelEdit", action: "channelEdit",
accountId: accountId ?? undefined,
channelId, channelId,
name: name ?? undefined, name: name ?? undefined,
topic: topic ?? undefined, topic: topic ?? undefined,
@@ -169,7 +203,10 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
const channelId = readStringParam(actionParams, "channelId", { const channelId = readStringParam(actionParams, "channelId", {
required: true, required: true,
}); });
return await handleDiscordAction({ action: "channelDelete", channelId }, cfg); return await handleDiscordAction(
{ action: "channelDelete", accountId: accountId ?? undefined, channelId },
cfg,
);
} }
if (action === "channel-move") { if (action === "channel-move") {
@@ -186,6 +223,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "channelMove", action: "channelMove",
accountId: accountId ?? undefined,
guildId, guildId,
channelId, channelId,
parentId: parentId === undefined ? undefined : parentId, parentId: parentId === undefined ? undefined : parentId,
@@ -206,6 +244,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "categoryCreate", action: "categoryCreate",
accountId: accountId ?? undefined,
guildId, guildId,
name, name,
position: position ?? undefined, position: position ?? undefined,
@@ -225,6 +264,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "categoryEdit", action: "categoryEdit",
accountId: accountId ?? undefined,
categoryId, categoryId,
name: name ?? undefined, name: name ?? undefined,
position: position ?? undefined, position: position ?? undefined,
@@ -237,7 +277,10 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
const categoryId = readStringParam(actionParams, "categoryId", { const categoryId = readStringParam(actionParams, "categoryId", {
required: true, required: true,
}); });
return await handleDiscordAction({ action: "categoryDelete", categoryId }, cfg); return await handleDiscordAction(
{ action: "categoryDelete", accountId: accountId ?? undefined, categoryId },
cfg,
);
} }
if (action === "voice-status") { if (action === "voice-status") {
@@ -245,14 +288,20 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
required: true, required: true,
}); });
const userId = readStringParam(actionParams, "userId", { required: true }); const userId = readStringParam(actionParams, "userId", { required: true });
return await handleDiscordAction({ action: "voiceStatus", guildId, userId }, cfg); return await handleDiscordAction(
{ action: "voiceStatus", accountId: accountId ?? undefined, guildId, userId },
cfg,
);
} }
if (action === "event-list") { if (action === "event-list") {
const guildId = readStringParam(actionParams, "guildId", { const guildId = readStringParam(actionParams, "guildId", {
required: true, required: true,
}); });
return await handleDiscordAction({ action: "eventList", guildId }, cfg); return await handleDiscordAction(
{ action: "eventList", accountId: accountId ?? undefined, guildId },
cfg,
);
} }
if (action === "event-create") { if (action === "event-create") {
@@ -271,6 +320,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "eventCreate", action: "eventCreate",
accountId: accountId ?? undefined,
guildId, guildId,
name, name,
startTime, startTime,
@@ -301,6 +351,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: discordAction, action: discordAction,
accountId: accountId ?? undefined,
guildId, guildId,
userId, userId,
durationMinutes, durationMinutes,
@@ -325,6 +376,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "threadList", action: "threadList",
accountId: accountId ?? undefined,
guildId, guildId,
channelId, channelId,
includeArchived, includeArchived,
@@ -344,6 +396,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "threadReply", action: "threadReply",
accountId: accountId ?? undefined,
channelId: resolveChannelId(), channelId: resolveChannelId(),
content, content,
mediaUrl: mediaUrl ?? undefined, mediaUrl: mediaUrl ?? undefined,
@@ -361,6 +414,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "searchMessages", action: "searchMessages",
accountId: accountId ?? undefined,
guildId, guildId,
content: query, content: query,
channelId: readStringParam(actionParams, "channelId"), channelId: readStringParam(actionParams, "channelId"),

View File

@@ -18,9 +18,10 @@ function readParentIdParam(params: Record<string, unknown>): string | null | und
} }
export async function handleDiscordMessageAction( export async function handleDiscordMessageAction(
ctx: Pick<ChannelMessageActionContext, "action" | "params" | "cfg">, ctx: Pick<ChannelMessageActionContext, "action" | "params" | "cfg" | "accountId">,
): Promise<AgentToolResult<unknown>> { ): Promise<AgentToolResult<unknown>> {
const { action, params, cfg } = ctx; const { action, params, cfg } = ctx;
const accountId = ctx.accountId ?? readStringParam(params, "accountId");
const resolveChannelId = () => const resolveChannelId = () =>
resolveDiscordChannelId( resolveDiscordChannelId(
@@ -39,6 +40,7 @@ export async function handleDiscordMessageAction(
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "sendMessage", action: "sendMessage",
accountId: accountId ?? undefined,
to, to,
content, content,
mediaUrl: mediaUrl ?? undefined, mediaUrl: mediaUrl ?? undefined,
@@ -62,6 +64,7 @@ export async function handleDiscordMessageAction(
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "poll", action: "poll",
accountId: accountId ?? undefined,
to, to,
question, question,
answers, answers,
@@ -80,6 +83,7 @@ export async function handleDiscordMessageAction(
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "react", action: "react",
accountId: accountId ?? undefined,
channelId: resolveChannelId(), channelId: resolveChannelId(),
messageId, messageId,
emoji, emoji,
@@ -93,7 +97,13 @@ export async function handleDiscordMessageAction(
const messageId = readStringParam(params, "messageId", { required: true }); const messageId = readStringParam(params, "messageId", { required: true });
const limit = readNumberParam(params, "limit", { integer: true }); const limit = readNumberParam(params, "limit", { integer: true });
return await handleDiscordAction( return await handleDiscordAction(
{ action: "reactions", channelId: resolveChannelId(), messageId, limit }, {
action: "reactions",
accountId: accountId ?? undefined,
channelId: resolveChannelId(),
messageId,
limit,
},
cfg, cfg,
); );
} }
@@ -103,6 +113,7 @@ export async function handleDiscordMessageAction(
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "readMessages", action: "readMessages",
accountId: accountId ?? undefined,
channelId: resolveChannelId(), channelId: resolveChannelId(),
limit, limit,
before: readStringParam(params, "before"), before: readStringParam(params, "before"),
@@ -119,6 +130,7 @@ export async function handleDiscordMessageAction(
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "editMessage", action: "editMessage",
accountId: accountId ?? undefined,
channelId: resolveChannelId(), channelId: resolveChannelId(),
messageId, messageId,
content, content,
@@ -130,7 +142,12 @@ export async function handleDiscordMessageAction(
if (action === "delete") { if (action === "delete") {
const messageId = readStringParam(params, "messageId", { required: true }); const messageId = readStringParam(params, "messageId", { required: true });
return await handleDiscordAction( return await handleDiscordAction(
{ action: "deleteMessage", channelId: resolveChannelId(), messageId }, {
action: "deleteMessage",
accountId: accountId ?? undefined,
channelId: resolveChannelId(),
messageId,
},
cfg, cfg,
); );
} }
@@ -141,6 +158,7 @@ export async function handleDiscordMessageAction(
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: action === "pin" ? "pinMessage" : action === "unpin" ? "unpinMessage" : "listPins", action: action === "pin" ? "pinMessage" : action === "unpin" ? "unpinMessage" : "listPins",
accountId: accountId ?? undefined,
channelId: resolveChannelId(), channelId: resolveChannelId(),
messageId, messageId,
}, },
@@ -149,7 +167,14 @@ export async function handleDiscordMessageAction(
} }
if (action === "permissions") { if (action === "permissions") {
return await handleDiscordAction({ action: "permissions", channelId: resolveChannelId() }, cfg); return await handleDiscordAction(
{
action: "permissions",
accountId: accountId ?? undefined,
channelId: resolveChannelId(),
},
cfg,
);
} }
if (action === "thread-create") { if (action === "thread-create") {
@@ -161,6 +186,7 @@ export async function handleDiscordMessageAction(
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "threadCreate", action: "threadCreate",
accountId: accountId ?? undefined,
channelId: resolveChannelId(), channelId: resolveChannelId(),
name, name,
messageId, messageId,
@@ -179,6 +205,7 @@ export async function handleDiscordMessageAction(
return await handleDiscordAction( return await handleDiscordAction(
{ {
action: "sticker", action: "sticker",
accountId: accountId ?? undefined,
to: readStringParam(params, "to", { required: true }), to: readStringParam(params, "to", { required: true }),
stickerIds, stickerIds,
content: readStringParam(params, "message"), content: readStringParam(params, "message"),

View File

@@ -299,6 +299,7 @@ export async function runCronIsolatedAgentTurn(params: {
sessionId: cronSession.sessionEntry.sessionId, sessionId: cronSession.sessionEntry.sessionId,
sessionKey: agentSessionKey, sessionKey: agentSessionKey,
messageChannel, messageChannel,
agentAccountId: resolvedDelivery.accountId,
sessionFile, sessionFile,
workspaceDir, workspaceDir,
config: cfgWithAgentDefaults, config: cfgWithAgentDefaults,

View File

@@ -410,3 +410,65 @@ describe("runMessageAction sendAttachment hydration", () => {
); );
}); });
}); });
describe("runMessageAction accountId defaults", () => {
const handleAction = vi.fn(async () => jsonResult({ ok: true }));
const accountPlugin: ChannelPlugin = {
id: "discord",
meta: {
id: "discord",
label: "Discord",
selectionLabel: "Discord",
docsPath: "/channels/discord",
blurb: "Discord test plugin.",
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
},
actions: {
listActions: () => ["send"],
handleAction,
},
};
beforeEach(() => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "discord",
source: "test",
plugin: accountPlugin,
},
]),
);
handleAction.mockClear();
});
afterEach(() => {
setActivePluginRegistry(createTestRegistry([]));
vi.clearAllMocks();
});
it("propagates defaultAccountId into params", async () => {
await runMessageAction({
cfg: {} as ClawdbotConfig,
action: "send",
params: {
channel: "discord",
target: "channel:123",
message: "hi",
},
defaultAccountId: "ops",
});
expect(handleAction).toHaveBeenCalled();
const ctx = handleAction.mock.calls[0]?.[0] as {
accountId?: string | null;
params: Record<string, unknown>;
};
expect(ctx.accountId).toBe("ops");
expect(ctx.params.accountId).toBe("ops");
});
});

View File

@@ -803,6 +803,9 @@ export async function runMessageAction(
const channel = await resolveChannel(cfg, params); const channel = await resolveChannel(cfg, params);
const accountId = readStringParam(params, "accountId") ?? input.defaultAccountId; const accountId = readStringParam(params, "accountId") ?? input.defaultAccountId;
if (accountId) {
params.accountId = accountId;
}
const dryRun = Boolean(input.dryRun ?? readBooleanParam(params, "dryRun")); const dryRun = Boolean(input.dryRun ?? readBooleanParam(params, "dryRun"));
await hydrateSendAttachmentParams({ await hydrateSendAttachmentParams({