refactor: dedupe channel outbound and monitor tests

This commit is contained in:
Peter Steinberger
2026-03-03 00:14:52 +00:00
parent 6a42d09129
commit d7dda4dd1a
18 changed files with 301 additions and 450 deletions

View File

@@ -20,6 +20,51 @@ type DirectSendFn<TOpts extends Record<string, unknown>, TResult extends DirectS
opts: TOpts,
) => Promise<TResult>;
type SendPayloadContext = Parameters<NonNullable<ChannelOutboundAdapter["sendPayload"]>>[0];
type SendPayloadResult = Awaited<ReturnType<NonNullable<ChannelOutboundAdapter["sendPayload"]>>>;
type SendPayloadAdapter = Pick<
ChannelOutboundAdapter,
"sendMedia" | "sendText" | "chunker" | "textChunkLimit"
>;
export async function sendTextMediaPayload(params: {
channel: string;
ctx: SendPayloadContext;
adapter: SendPayloadAdapter;
}): Promise<SendPayloadResult> {
const text = params.ctx.payload.text ?? "";
const urls = params.ctx.payload.mediaUrls?.length
? params.ctx.payload.mediaUrls
: params.ctx.payload.mediaUrl
? [params.ctx.payload.mediaUrl]
: [];
if (!text && urls.length === 0) {
return { channel: params.channel, messageId: "" };
}
if (urls.length > 0) {
let lastResult = await params.adapter.sendMedia!({
...params.ctx,
text,
mediaUrl: urls[0],
});
for (let i = 1; i < urls.length; i++) {
lastResult = await params.adapter.sendMedia!({
...params.ctx,
text: "",
mediaUrl: urls[i],
});
}
return lastResult;
}
const limit = params.adapter.textChunkLimit;
const chunks = limit && params.adapter.chunker ? params.adapter.chunker(text, limit) : [text];
let lastResult: Awaited<ReturnType<NonNullable<typeof params.adapter.sendText>>>;
for (const chunk of chunks) {
lastResult = await params.adapter.sendText!({ ...params.ctx, text: chunk });
}
return lastResult!;
}
export function resolveScopedChannelMediaMaxBytes(params: {
cfg: OpenClawConfig;
accountId?: string | null;
@@ -91,39 +136,8 @@ export function createDirectTextMediaOutbound<
chunker: chunkText,
chunkerMode: "text",
textChunkLimit: 4000,
sendPayload: async (ctx) => {
const text = ctx.payload.text ?? "";
const urls = ctx.payload.mediaUrls?.length
? ctx.payload.mediaUrls
: ctx.payload.mediaUrl
? [ctx.payload.mediaUrl]
: [];
if (!text && urls.length === 0) {
return { channel: params.channel, messageId: "" };
}
if (urls.length > 0) {
let lastResult = await outbound.sendMedia!({
...ctx,
text,
mediaUrl: urls[0],
});
for (let i = 1; i < urls.length; i++) {
lastResult = await outbound.sendMedia!({
...ctx,
text: "",
mediaUrl: urls[i],
});
}
return lastResult;
}
const limit = outbound.textChunkLimit;
const chunks = limit && outbound.chunker ? outbound.chunker(text, limit) : [text];
let lastResult: Awaited<ReturnType<NonNullable<typeof outbound.sendText>>>;
for (const chunk of chunks) {
lastResult = await outbound.sendText!({ ...ctx, text: chunk });
}
return lastResult!;
},
sendPayload: async (ctx) =>
await sendTextMediaPayload({ channel: params.channel, ctx, adapter: outbound }),
sendText: async ({ cfg, to, text, accountId, deps, replyToId }) => {
return await sendDirect({
cfg,

View File

@@ -10,6 +10,7 @@ import {
import type { OutboundIdentity } from "../../../infra/outbound/identity.js";
import { normalizeDiscordOutboundTarget } from "../normalize/discord.js";
import type { ChannelOutboundAdapter } from "../types.js";
import { sendTextMediaPayload } from "./direct-text-media.js";
function resolveDiscordOutboundTarget(params: {
to: string;
@@ -80,39 +81,8 @@ export const discordOutbound: ChannelOutboundAdapter = {
textChunkLimit: 2000,
pollMaxOptions: 10,
resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to),
sendPayload: async (ctx) => {
const text = ctx.payload.text ?? "";
const urls = ctx.payload.mediaUrls?.length
? ctx.payload.mediaUrls
: ctx.payload.mediaUrl
? [ctx.payload.mediaUrl]
: [];
if (!text && urls.length === 0) {
return { channel: "discord", messageId: "" };
}
if (urls.length > 0) {
let lastResult = await discordOutbound.sendMedia!({
...ctx,
text,
mediaUrl: urls[0],
});
for (let i = 1; i < urls.length; i++) {
lastResult = await discordOutbound.sendMedia!({
...ctx,
text: "",
mediaUrl: urls[i],
});
}
return lastResult;
}
const limit = discordOutbound.textChunkLimit;
const chunks = limit && discordOutbound.chunker ? discordOutbound.chunker(text, limit) : [text];
let lastResult: Awaited<ReturnType<NonNullable<typeof discordOutbound.sendText>>>;
for (const chunk of chunks) {
lastResult = await discordOutbound.sendText!({ ...ctx, text: chunk });
}
return lastResult!;
},
sendPayload: async (ctx) =>
await sendTextMediaPayload({ channel: "discord", ctx, adapter: discordOutbound }),
sendText: async ({ to, text, accountId, deps, replyToId, threadId, identity, silent }) => {
if (!silent) {
const webhookResult = await maybeSendDiscordWebhookText({

View File

@@ -2,6 +2,7 @@ import type { OutboundIdentity } from "../../../infra/outbound/identity.js";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import { sendMessageSlack, type SlackSendIdentity } from "../../../slack/send.js";
import type { ChannelOutboundAdapter } from "../types.js";
import { sendTextMediaPayload } from "./direct-text-media.js";
function resolveSlackSendIdentity(identity?: OutboundIdentity): SlackSendIdentity | undefined {
if (!identity) {
@@ -93,39 +94,8 @@ export const slackOutbound: ChannelOutboundAdapter = {
deliveryMode: "direct",
chunker: null,
textChunkLimit: 4000,
sendPayload: async (ctx) => {
const text = ctx.payload.text ?? "";
const urls = ctx.payload.mediaUrls?.length
? ctx.payload.mediaUrls
: ctx.payload.mediaUrl
? [ctx.payload.mediaUrl]
: [];
if (!text && urls.length === 0) {
return { channel: "slack", messageId: "" };
}
if (urls.length > 0) {
let lastResult = await slackOutbound.sendMedia!({
...ctx,
text,
mediaUrl: urls[0],
});
for (let i = 1; i < urls.length; i++) {
lastResult = await slackOutbound.sendMedia!({
...ctx,
text: "",
mediaUrl: urls[i],
});
}
return lastResult;
}
const limit = slackOutbound.textChunkLimit;
const chunks = limit && slackOutbound.chunker ? slackOutbound.chunker(text, limit) : [text];
let lastResult: Awaited<ReturnType<NonNullable<typeof slackOutbound.sendText>>>;
for (const chunk of chunks) {
lastResult = await slackOutbound.sendText!({ ...ctx, text: chunk });
}
return lastResult!;
},
sendPayload: async (ctx) =>
await sendTextMediaPayload({ channel: "slack", ctx, adapter: slackOutbound }),
sendText: async ({ to, text, accountId, deps, replyToId, threadId, identity }) => {
return await sendSlackOutboundMessage({
to,

View File

@@ -3,6 +3,7 @@ import { shouldLogVerbose } from "../../../globals.js";
import { sendPollWhatsApp } from "../../../web/outbound.js";
import { resolveWhatsAppOutboundTarget } from "../../../whatsapp/resolve-outbound-target.js";
import type { ChannelOutboundAdapter } from "../types.js";
import { sendTextMediaPayload } from "./direct-text-media.js";
export const whatsappOutbound: ChannelOutboundAdapter = {
deliveryMode: "gateway",
@@ -12,40 +13,8 @@ export const whatsappOutbound: ChannelOutboundAdapter = {
pollMaxOptions: 12,
resolveTarget: ({ to, allowFrom, mode }) =>
resolveWhatsAppOutboundTarget({ to, allowFrom, mode }),
sendPayload: async (ctx) => {
const text = ctx.payload.text ?? "";
const urls = ctx.payload.mediaUrls?.length
? ctx.payload.mediaUrls
: ctx.payload.mediaUrl
? [ctx.payload.mediaUrl]
: [];
if (!text && urls.length === 0) {
return { channel: "whatsapp", messageId: "" };
}
if (urls.length > 0) {
let lastResult = await whatsappOutbound.sendMedia!({
...ctx,
text,
mediaUrl: urls[0],
});
for (let i = 1; i < urls.length; i++) {
lastResult = await whatsappOutbound.sendMedia!({
...ctx,
text: "",
mediaUrl: urls[i],
});
}
return lastResult;
}
const limit = whatsappOutbound.textChunkLimit;
const chunks =
limit && whatsappOutbound.chunker ? whatsappOutbound.chunker(text, limit) : [text];
let lastResult: Awaited<ReturnType<NonNullable<typeof whatsappOutbound.sendText>>>;
for (const chunk of chunks) {
lastResult = await whatsappOutbound.sendText!({ ...ctx, text: chunk });
}
return lastResult!;
},
sendPayload: async (ctx) =>
await sendTextMediaPayload({ channel: "whatsapp", ctx, adapter: whatsappOutbound }),
sendText: async ({ to, text, accountId, deps, gifPlayback }) => {
const send =
deps?.sendWhatsApp ?? (await import("../../../web/outbound.js")).sendMessageWhatsApp;