mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 13:07:39 +00:00
fix(outbound): unify resolved cfg threading across send paths (#33987)
This commit is contained in:
@@ -847,7 +847,10 @@ describe("signalMessageActions", () => {
|
||||
cfg: createSignalAccountOverrideCfg(),
|
||||
accountId: "work",
|
||||
params: { to: "+15550001111", messageId: "123", emoji: "👍" },
|
||||
expectedArgs: ["+15550001111", 123, "👍", { accountId: "work" }],
|
||||
expectedRecipient: "+15550001111",
|
||||
expectedTimestamp: 123,
|
||||
expectedEmoji: "👍",
|
||||
expectedOptions: { accountId: "work" },
|
||||
},
|
||||
{
|
||||
name: "normalizes uuid recipients",
|
||||
@@ -858,7 +861,10 @@ describe("signalMessageActions", () => {
|
||||
messageId: "123",
|
||||
emoji: "🔥",
|
||||
},
|
||||
expectedArgs: ["123e4567-e89b-12d3-a456-426614174000", 123, "🔥", { accountId: undefined }],
|
||||
expectedRecipient: "123e4567-e89b-12d3-a456-426614174000",
|
||||
expectedTimestamp: 123,
|
||||
expectedEmoji: "🔥",
|
||||
expectedOptions: {},
|
||||
},
|
||||
{
|
||||
name: "passes groupId and targetAuthor for group reactions",
|
||||
@@ -870,17 +876,13 @@ describe("signalMessageActions", () => {
|
||||
messageId: "123",
|
||||
emoji: "✅",
|
||||
},
|
||||
expectedArgs: [
|
||||
"",
|
||||
123,
|
||||
"✅",
|
||||
{
|
||||
accountId: undefined,
|
||||
groupId: "group-id",
|
||||
targetAuthor: "uuid:123e4567-e89b-12d3-a456-426614174000",
|
||||
targetAuthorUuid: undefined,
|
||||
},
|
||||
],
|
||||
expectedRecipient: "",
|
||||
expectedTimestamp: 123,
|
||||
expectedEmoji: "✅",
|
||||
expectedOptions: {
|
||||
groupId: "group-id",
|
||||
targetAuthor: "uuid:123e4567-e89b-12d3-a456-426614174000",
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -890,7 +892,15 @@ describe("signalMessageActions", () => {
|
||||
cfg: testCase.cfg,
|
||||
accountId: testCase.accountId,
|
||||
});
|
||||
expect(sendReactionSignal, testCase.name).toHaveBeenCalledWith(...testCase.expectedArgs);
|
||||
expect(sendReactionSignal, testCase.name).toHaveBeenCalledWith(
|
||||
testCase.expectedRecipient,
|
||||
testCase.expectedTimestamp,
|
||||
testCase.expectedEmoji,
|
||||
expect.objectContaining({
|
||||
cfg: testCase.cfg,
|
||||
...testCase.expectedOptions,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ function resolveSignalReactionTarget(raw: string): { recipient?: string; groupId
|
||||
}
|
||||
|
||||
async function mutateSignalReaction(params: {
|
||||
cfg: Parameters<typeof resolveSignalAccount>[0]["cfg"];
|
||||
accountId?: string;
|
||||
target: { recipient?: string; groupId?: string };
|
||||
timestamp: number;
|
||||
@@ -49,6 +50,7 @@ async function mutateSignalReaction(params: {
|
||||
targetAuthorUuid?: string;
|
||||
}) {
|
||||
const options = {
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
groupId: params.target.groupId,
|
||||
targetAuthor: params.targetAuthor,
|
||||
@@ -153,6 +155,7 @@ export const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
throw new Error("Emoji required to remove reaction.");
|
||||
}
|
||||
return await mutateSignalReaction({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
target,
|
||||
timestamp,
|
||||
@@ -167,6 +170,7 @@ export const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
throw new Error("Emoji required to add reaction.");
|
||||
}
|
||||
return await mutateSignalReaction({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
target,
|
||||
timestamp,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { resolveChannelMediaMaxBytes } from "../media-limits.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
|
||||
type DirectSendOptions = {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
replyToId?: string | null;
|
||||
mediaUrl?: string;
|
||||
@@ -121,6 +122,7 @@ export function createDirectTextMediaOutbound<
|
||||
sendParams.to,
|
||||
sendParams.text,
|
||||
sendParams.buildOptions({
|
||||
cfg: sendParams.cfg,
|
||||
mediaUrl: sendParams.mediaUrl,
|
||||
mediaLocalRoots: sendParams.mediaLocalRoots,
|
||||
accountId: sendParams.accountId,
|
||||
|
||||
@@ -143,9 +143,16 @@ describe("discordOutbound", () => {
|
||||
|
||||
it("uses webhook persona delivery for bound thread text replies", async () => {
|
||||
mockBoundThreadManager();
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "resolved-token",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await discordOutbound.sendText?.({
|
||||
cfg: {},
|
||||
cfg,
|
||||
to: "channel:parent-1",
|
||||
text: "hello from persona",
|
||||
accountId: "default",
|
||||
@@ -169,6 +176,10 @@ describe("discordOutbound", () => {
|
||||
avatarUrl: "https://example.com/avatar.png",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(hoisted.sendWebhookMessageDiscordMock.mock.calls[0]?.[1] as { cfg?: unknown } | undefined)
|
||||
?.cfg,
|
||||
).toBe(cfg);
|
||||
expect(hoisted.sendMessageDiscordMock).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
channel: "discord",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import {
|
||||
getThreadBindingManager,
|
||||
type ThreadBindingRecord,
|
||||
@@ -38,6 +39,7 @@ function resolveDiscordWebhookIdentity(params: {
|
||||
}
|
||||
|
||||
async function maybeSendDiscordWebhookText(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
text: string;
|
||||
threadId?: string | number | null;
|
||||
accountId?: string | null;
|
||||
@@ -68,6 +70,7 @@ async function maybeSendDiscordWebhookText(params: {
|
||||
webhookToken: binding.webhookToken,
|
||||
accountId: binding.accountId,
|
||||
threadId: binding.threadId,
|
||||
cfg: params.cfg,
|
||||
replyTo: params.replyToId ?? undefined,
|
||||
username: persona.username,
|
||||
avatarUrl: persona.avatarUrl,
|
||||
@@ -83,9 +86,10 @@ export const discordOutbound: ChannelOutboundAdapter = {
|
||||
resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to),
|
||||
sendPayload: async (ctx) =>
|
||||
await sendTextMediaPayload({ channel: "discord", ctx, adapter: discordOutbound }),
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, threadId, identity, silent }) => {
|
||||
sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, identity, silent }) => {
|
||||
if (!silent) {
|
||||
const webhookResult = await maybeSendDiscordWebhookText({
|
||||
cfg,
|
||||
text,
|
||||
threadId,
|
||||
accountId,
|
||||
@@ -103,10 +107,12 @@ export const discordOutbound: ChannelOutboundAdapter = {
|
||||
replyTo: replyToId ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
silent: silent ?? undefined,
|
||||
cfg,
|
||||
});
|
||||
return { channel: "discord", ...result };
|
||||
},
|
||||
sendMedia: async ({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
@@ -126,14 +132,16 @@ export const discordOutbound: ChannelOutboundAdapter = {
|
||||
replyTo: replyToId ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
silent: silent ?? undefined,
|
||||
cfg,
|
||||
});
|
||||
return { channel: "discord", ...result };
|
||||
},
|
||||
sendPoll: async ({ to, poll, accountId, threadId, silent }) => {
|
||||
sendPoll: async ({ cfg, to, poll, accountId, threadId, silent }) => {
|
||||
const target = resolveDiscordOutboundTarget({ to, threadId });
|
||||
return await sendPollDiscord(target, poll, {
|
||||
accountId: accountId ?? undefined,
|
||||
silent: silent ?? undefined,
|
||||
cfg,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,12 +13,14 @@ export const imessageOutbound = createDirectTextMediaOutbound({
|
||||
channel: "imessage",
|
||||
resolveSender: resolveIMessageSender,
|
||||
resolveMaxBytes: createScopedChannelMediaMaxBytesResolver("imessage"),
|
||||
buildTextOptions: ({ maxBytes, accountId, replyToId }) => ({
|
||||
buildTextOptions: ({ cfg, maxBytes, accountId, replyToId }) => ({
|
||||
config: cfg,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
replyToId: replyToId ?? undefined,
|
||||
}),
|
||||
buildMediaOptions: ({ mediaUrl, maxBytes, accountId, replyToId, mediaLocalRoots }) => ({
|
||||
buildMediaOptions: ({ cfg, mediaUrl, maxBytes, accountId, replyToId, mediaLocalRoots }) => ({
|
||||
config: cfg,
|
||||
mediaUrl,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
|
||||
@@ -13,11 +13,13 @@ export const signalOutbound = createDirectTextMediaOutbound({
|
||||
channel: "signal",
|
||||
resolveSender: resolveSignalSender,
|
||||
resolveMaxBytes: createScopedChannelMediaMaxBytesResolver("signal"),
|
||||
buildTextOptions: ({ maxBytes, accountId }) => ({
|
||||
buildTextOptions: ({ cfg, maxBytes, accountId }) => ({
|
||||
cfg,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
}),
|
||||
buildMediaOptions: ({ mediaUrl, maxBytes, accountId, mediaLocalRoots }) => ({
|
||||
buildMediaOptions: ({ cfg, mediaUrl, maxBytes, accountId, mediaLocalRoots }) => ({
|
||||
cfg,
|
||||
mediaUrl,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
|
||||
@@ -58,11 +58,13 @@ const expectSlackSendCalledWith = (
|
||||
};
|
||||
},
|
||||
) => {
|
||||
expect(sendMessageSlack).toHaveBeenCalledWith("C123", text, {
|
||||
const expected = {
|
||||
threadTs: "1111.2222",
|
||||
accountId: "default",
|
||||
...options,
|
||||
});
|
||||
cfg: expect.any(Object),
|
||||
...(options?.identity ? { identity: expect.objectContaining(options.identity) } : {}),
|
||||
};
|
||||
expect(sendMessageSlack).toHaveBeenCalledWith("C123", text, expect.objectContaining(expected));
|
||||
};
|
||||
|
||||
describe("slack outbound hook wiring", () => {
|
||||
|
||||
@@ -48,6 +48,7 @@ async function applySlackMessageSendingHooks(params: {
|
||||
}
|
||||
|
||||
async function sendSlackOutboundMessage(params: {
|
||||
cfg: NonNullable<Parameters<typeof sendMessageSlack>[2]>["cfg"];
|
||||
to: string;
|
||||
text: string;
|
||||
mediaUrl?: string;
|
||||
@@ -80,6 +81,7 @@ async function sendSlackOutboundMessage(params: {
|
||||
|
||||
const slackIdentity = resolveSlackSendIdentity(params.identity);
|
||||
const result = await send(params.to, hookResult.text, {
|
||||
cfg: params.cfg,
|
||||
threadTs,
|
||||
accountId: params.accountId ?? undefined,
|
||||
...(params.mediaUrl
|
||||
@@ -96,8 +98,9 @@ export const slackOutbound: ChannelOutboundAdapter = {
|
||||
textChunkLimit: 4000,
|
||||
sendPayload: async (ctx) =>
|
||||
await sendTextMediaPayload({ channel: "slack", ctx, adapter: slackOutbound }),
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, threadId, identity }) => {
|
||||
sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, identity }) => {
|
||||
return await sendSlackOutboundMessage({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
accountId,
|
||||
@@ -108,6 +111,7 @@ export const slackOutbound: ChannelOutboundAdapter = {
|
||||
});
|
||||
},
|
||||
sendMedia: async ({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
@@ -119,6 +123,7 @@ export const slackOutbound: ChannelOutboundAdapter = {
|
||||
identity,
|
||||
}) => {
|
||||
return await sendSlackOutboundMessage({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { sendMessageTelegram } from "../../../telegram/send.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
|
||||
function resolveTelegramSendContext(params: {
|
||||
cfg: NonNullable<Parameters<typeof sendMessageTelegram>[2]>["cfg"];
|
||||
deps?: OutboundSendDeps;
|
||||
accountId?: string | null;
|
||||
replyToId?: string | null;
|
||||
@@ -16,6 +17,7 @@ function resolveTelegramSendContext(params: {
|
||||
}): {
|
||||
send: typeof sendMessageTelegram;
|
||||
baseOpts: {
|
||||
cfg: NonNullable<Parameters<typeof sendMessageTelegram>[2]>["cfg"];
|
||||
verbose: false;
|
||||
textMode: "html";
|
||||
messageThreadId?: number;
|
||||
@@ -29,6 +31,7 @@ function resolveTelegramSendContext(params: {
|
||||
baseOpts: {
|
||||
verbose: false,
|
||||
textMode: "html",
|
||||
cfg: params.cfg,
|
||||
messageThreadId: parseTelegramThreadId(params.threadId),
|
||||
replyToMessageId: parseTelegramReplyToMessageId(params.replyToId),
|
||||
accountId: params.accountId ?? undefined,
|
||||
@@ -41,8 +44,9 @@ export const telegramOutbound: ChannelOutboundAdapter = {
|
||||
chunker: markdownToTelegramHtmlChunks,
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, threadId }) => {
|
||||
sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId }) => {
|
||||
const { send, baseOpts } = resolveTelegramSendContext({
|
||||
cfg,
|
||||
deps,
|
||||
accountId,
|
||||
replyToId,
|
||||
@@ -54,6 +58,7 @@ export const telegramOutbound: ChannelOutboundAdapter = {
|
||||
return { channel: "telegram", ...result };
|
||||
},
|
||||
sendMedia: async ({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
@@ -64,6 +69,7 @@ export const telegramOutbound: ChannelOutboundAdapter = {
|
||||
threadId,
|
||||
}) => {
|
||||
const { send, baseOpts } = resolveTelegramSendContext({
|
||||
cfg,
|
||||
deps,
|
||||
accountId,
|
||||
replyToId,
|
||||
@@ -76,8 +82,18 @@ export const telegramOutbound: ChannelOutboundAdapter = {
|
||||
});
|
||||
return { channel: "telegram", ...result };
|
||||
},
|
||||
sendPayload: async ({ to, payload, mediaLocalRoots, accountId, deps, replyToId, threadId }) => {
|
||||
sendPayload: async ({
|
||||
cfg,
|
||||
to,
|
||||
payload,
|
||||
mediaLocalRoots,
|
||||
accountId,
|
||||
deps,
|
||||
replyToId,
|
||||
threadId,
|
||||
}) => {
|
||||
const { send, baseOpts: contextOpts } = resolveTelegramSendContext({
|
||||
cfg,
|
||||
deps,
|
||||
accountId,
|
||||
replyToId,
|
||||
|
||||
41
src/channels/plugins/outbound/whatsapp.poll.test.ts
Normal file
41
src/channels/plugins/outbound/whatsapp.poll.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
sendPollWhatsApp: vi.fn(async () => ({ messageId: "poll-1", toJid: "1555@s.whatsapp.net" })),
|
||||
}));
|
||||
|
||||
vi.mock("../../../globals.js", () => ({
|
||||
shouldLogVerbose: () => false,
|
||||
}));
|
||||
|
||||
vi.mock("../../../web/outbound.js", () => ({
|
||||
sendPollWhatsApp: hoisted.sendPollWhatsApp,
|
||||
}));
|
||||
|
||||
import { whatsappOutbound } from "./whatsapp.js";
|
||||
|
||||
describe("whatsappOutbound sendPoll", () => {
|
||||
it("threads cfg through poll send options", async () => {
|
||||
const cfg = { marker: "resolved-cfg" } as OpenClawConfig;
|
||||
const poll = {
|
||||
question: "Lunch?",
|
||||
options: ["Pizza", "Sushi"],
|
||||
maxSelections: 1,
|
||||
};
|
||||
|
||||
const result = await whatsappOutbound.sendPoll!({
|
||||
cfg,
|
||||
to: "+1555",
|
||||
poll,
|
||||
accountId: "work",
|
||||
});
|
||||
|
||||
expect(hoisted.sendPollWhatsApp).toHaveBeenCalledWith("+1555", poll, {
|
||||
verbose: false,
|
||||
accountId: "work",
|
||||
cfg,
|
||||
});
|
||||
expect(result).toEqual({ messageId: "poll-1", toJid: "1555@s.whatsapp.net" });
|
||||
});
|
||||
});
|
||||
@@ -15,21 +15,23 @@ export const whatsappOutbound: ChannelOutboundAdapter = {
|
||||
resolveWhatsAppOutboundTarget({ to, allowFrom, mode }),
|
||||
sendPayload: async (ctx) =>
|
||||
await sendTextMediaPayload({ channel: "whatsapp", ctx, adapter: whatsappOutbound }),
|
||||
sendText: async ({ to, text, accountId, deps, gifPlayback }) => {
|
||||
sendText: async ({ cfg, to, text, accountId, deps, gifPlayback }) => {
|
||||
const send =
|
||||
deps?.sendWhatsApp ?? (await import("../../../web/outbound.js")).sendMessageWhatsApp;
|
||||
const result = await send(to, text, {
|
||||
verbose: false,
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
gifPlayback,
|
||||
});
|
||||
return { channel: "whatsapp", ...result };
|
||||
},
|
||||
sendMedia: async ({ to, text, mediaUrl, mediaLocalRoots, accountId, deps, gifPlayback }) => {
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId, deps, gifPlayback }) => {
|
||||
const send =
|
||||
deps?.sendWhatsApp ?? (await import("../../../web/outbound.js")).sendMessageWhatsApp;
|
||||
const result = await send(to, text, {
|
||||
verbose: false,
|
||||
cfg,
|
||||
mediaUrl,
|
||||
mediaLocalRoots,
|
||||
accountId: accountId ?? undefined,
|
||||
@@ -37,9 +39,10 @@ export const whatsappOutbound: ChannelOutboundAdapter = {
|
||||
});
|
||||
return { channel: "whatsapp", ...result };
|
||||
},
|
||||
sendPoll: async ({ to, poll, accountId }) =>
|
||||
sendPoll: async ({ cfg, to, poll, accountId }) =>
|
||||
await sendPollWhatsApp(to, poll, {
|
||||
verbose: shouldLogVerbose(),
|
||||
accountId: accountId ?? undefined,
|
||||
cfg,
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user