Discord: preserve accountId in message actions (refs #1489)
This commit is contained in:
@@ -5,6 +5,7 @@ import { truncateUtf16Safe } from "../../utils.js";
|
|||||||
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
|
import { optionalStringEnum, stringEnum } from "../schema/typebox.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 { resolveSessionAgentId } from "../agent-scope.js";
|
||||||
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
|
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
|
||||||
|
|
||||||
// NOTE: We use Type.Object({}, { additionalProperties: true }) for job/patch
|
// NOTE: We use Type.Object({}, { additionalProperties: true }) for job/patch
|
||||||
@@ -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") {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const agentId = opts?.agentSessionKey
|
||||||
|
? resolveSessionAgentId({ sessionKey: opts.agentSessionKey, config: cfg })
|
||||||
|
: undefined;
|
||||||
|
if (agentId && !(job as { agentId?: unknown }).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
|
||||||
|
|||||||
@@ -114,7 +114,8 @@ export async function handleDiscordMessagingAction(
|
|||||||
required: true,
|
required: true,
|
||||||
label: "stickerIds",
|
label: "stickerIds",
|
||||||
});
|
});
|
||||||
await sendStickerDiscord(to, stickerIds, { content });
|
const accountId = readStringParam(params, "accountId");
|
||||||
|
await sendStickerDiscord(to, stickerIds, { content, accountId: accountId ?? undefined });
|
||||||
return jsonResult({ ok: true });
|
return jsonResult({ ok: true });
|
||||||
}
|
}
|
||||||
case "poll": {
|
case "poll": {
|
||||||
@@ -137,10 +138,11 @@ export async function handleDiscordMessagingAction(
|
|||||||
const durationHours =
|
const durationHours =
|
||||||
typeof durationRaw === "number" && Number.isFinite(durationRaw) ? durationRaw : undefined;
|
typeof durationRaw === "number" && Number.isFinite(durationRaw) ? durationRaw : undefined;
|
||||||
const maxSelections = allowMultiselect ? Math.max(2, answers.length) : 1;
|
const maxSelections = allowMultiselect ? Math.max(2, answers.length) : 1;
|
||||||
|
const accountId = readStringParam(params, "accountId");
|
||||||
await sendPollDiscord(
|
await sendPollDiscord(
|
||||||
to,
|
to,
|
||||||
{ question, options: answers, maxSelections, durationHours },
|
{ question, options: answers, maxSelections, durationHours },
|
||||||
{ content },
|
{ content, accountId: accountId ?? undefined },
|
||||||
);
|
);
|
||||||
return jsonResult({ ok: true });
|
return jsonResult({ ok: true });
|
||||||
}
|
}
|
||||||
@@ -211,7 +213,10 @@ export async function handleDiscordMessagingAction(
|
|||||||
const replyTo = readStringParam(params, "replyTo");
|
const replyTo = readStringParam(params, "replyTo");
|
||||||
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 accountId = readStringParam(params, "accountId");
|
||||||
|
|
||||||
const result = await sendMessageDiscord(to, content, {
|
const result = await sendMessageDiscord(to, content, {
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
mediaUrl,
|
mediaUrl,
|
||||||
replyTo,
|
replyTo,
|
||||||
embeds,
|
embeds,
|
||||||
@@ -298,7 +303,9 @@ 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 accountId = readStringParam(params, "accountId");
|
||||||
const result = await sendMessageDiscord(`channel:${channelId}`, content, {
|
const result = await sendMessageDiscord(`channel:${channelId}`, content, {
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
mediaUrl,
|
mediaUrl,
|
||||||
replyTo,
|
replyTo,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -1,11 +1,41 @@
|
|||||||
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";
|
||||||
import { discordMessageActions } from "./discord.js";
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadDiscordMessageActions = async () => {
|
||||||
|
const mod = await import("./discord.js");
|
||||||
|
return mod.discordMessageActions;
|
||||||
|
};
|
||||||
|
|
||||||
describe("discord message actions", () => {
|
describe("discord message actions", () => {
|
||||||
it("lists channel and upload actions by default", () => {
|
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;
|
||||||
|
const discordMessageActions = await loadDiscordMessageActions();
|
||||||
const actions = discordMessageActions.listActions?.({ cfg }) ?? [];
|
const actions = discordMessageActions.listActions?.({ cfg }) ?? [];
|
||||||
|
|
||||||
expect(actions).toContain("emoji-upload");
|
expect(actions).toContain("emoji-upload");
|
||||||
@@ -13,12 +43,65 @@ describe("discord message actions", () => {
|
|||||||
expect(actions).toContain("channel-create");
|
expect(actions).toContain("channel-create");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("respects disabled channel actions", () => {
|
it("respects disabled channel actions", async () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
channels: { discord: { token: "d0", actions: { channels: false } } },
|
channels: { discord: { token: "d0", actions: { channels: false } } },
|
||||||
} as ClawdbotConfig;
|
} as ClawdbotConfig;
|
||||||
|
const discordMessageActions = await loadDiscordMessageActions();
|
||||||
const actions = discordMessageActions.listActions?.({ cfg }) ?? [];
|
const actions = discordMessageActions.listActions?.({ cfg }) ?? [];
|
||||||
|
|
||||||
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",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user