mirror of https://github.com/openclaw/openclaw.git
refactor: make OutboundSendDeps dynamic with channel-ID keys (#45517)
* refactor: make OutboundSendDeps dynamic with channel-ID keys
Replace hardcoded per-channel send fields (sendTelegram, sendDiscord,
etc.) with a dynamic index-signature type keyed by channel ID. This
unblocks moving channel implementations to extensions without breaking
the outbound dispatch contract.
- OutboundSendDeps and CliDeps are now { [channelId: string]: unknown }
- Each outbound adapter resolves its send fn via bracket access with cast
- Lazy-loading preserved via createLazySender with module cache
- Delete 6 deps-send-*.runtime.ts one-liner re-export files
- Harden guardrail scan against deleted-but-tracked files
* fix: preserve outbound send-deps compatibility
* style: fix formatting issues (import order, extra bracket, trailing whitespace)
* fix: resolve type errors from dynamic OutboundSendDeps in tests and extension
* fix: remove unused OutboundSendDeps import from deliver.test-helpers
This commit is contained in:
parent
0c926a2c5e
commit
7764f717e9
|
|
@ -37,8 +37,13 @@ import {
|
|||
type ChannelPlugin,
|
||||
type ResolvedDiscordAccount,
|
||||
} from "openclaw/plugin-sdk/discord";
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
|
||||
import { getDiscordRuntime } from "./runtime.js";
|
||||
|
||||
type DiscordSendFn = ReturnType<
|
||||
typeof getDiscordRuntime
|
||||
>["channel"]["discord"]["sendMessageDiscord"];
|
||||
|
||||
const meta = getChatChannelMeta("discord");
|
||||
|
||||
const discordMessageActions: ChannelMessageActionAdapter = {
|
||||
|
|
@ -300,7 +305,9 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
|||
pollMaxOptions: 10,
|
||||
resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to),
|
||||
sendText: async ({ cfg, to, text, accountId, deps, replyToId, silent }) => {
|
||||
const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
|
||||
const send =
|
||||
resolveOutboundSendDep<DiscordSendFn>(deps, "discord") ??
|
||||
getDiscordRuntime().channel.discord.sendMessageDiscord;
|
||||
const result = await send(to, text, {
|
||||
verbose: false,
|
||||
cfg,
|
||||
|
|
@ -321,7 +328,9 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
|||
replyToId,
|
||||
silent,
|
||||
}) => {
|
||||
const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
|
||||
const send =
|
||||
resolveOutboundSendDep<DiscordSendFn>(deps, "discord") ??
|
||||
getDiscordRuntime().channel.discord.sendMessageDiscord;
|
||||
const result = await send(to, text, {
|
||||
verbose: false,
|
||||
cfg,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
type ChannelPlugin,
|
||||
type ResolvedIMessageAccount,
|
||||
} from "openclaw/plugin-sdk/imessage";
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
|
||||
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
|
||||
import { getIMessageRuntime } from "./runtime.js";
|
||||
|
||||
|
|
@ -59,11 +60,12 @@ async function sendIMessageOutbound(params: {
|
|||
mediaUrl?: string;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
accountId?: string;
|
||||
deps?: { sendIMessage?: IMessageSendFn };
|
||||
deps?: { [channelId: string]: unknown };
|
||||
replyToId?: string;
|
||||
}) {
|
||||
const send =
|
||||
params.deps?.sendIMessage ?? getIMessageRuntime().channel.imessage.sendMessageIMessage;
|
||||
resolveOutboundSendDep<IMessageSendFn>(params.deps, "imessage") ??
|
||||
getIMessageRuntime().channel.imessage.sendMessageIMessage;
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg: params.cfg,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ describe("matrixOutbound cfg threading", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("passes resolved cfg through injected deps.sendMatrix", async () => {
|
||||
it("passes resolved cfg through injected deps.matrix", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
|
|
@ -96,7 +96,7 @@ describe("matrixOutbound cfg threading", () => {
|
|||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const sendMatrix = vi.fn(async () => ({
|
||||
const matrix = vi.fn(async () => ({
|
||||
messageId: "evt-injected",
|
||||
roomId: "!room:example",
|
||||
}));
|
||||
|
|
@ -105,13 +105,13 @@ describe("matrixOutbound cfg threading", () => {
|
|||
cfg,
|
||||
to: "room:!room:example",
|
||||
text: "hello via deps",
|
||||
deps: { sendMatrix },
|
||||
deps: { matrix },
|
||||
accountId: "default",
|
||||
threadId: "$thread",
|
||||
replyToId: "$reply",
|
||||
});
|
||||
|
||||
expect(sendMatrix).toHaveBeenCalledWith(
|
||||
expect(matrix).toHaveBeenCalledWith(
|
||||
"room:!room:example",
|
||||
"hello via deps",
|
||||
expect.objectContaining({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/matrix";
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
|
||||
import { sendMessageMatrix, sendPollMatrix } from "./matrix/send.js";
|
||||
import { getMatrixRuntime } from "./runtime.js";
|
||||
|
||||
|
|
@ -8,7 +9,8 @@ export const matrixOutbound: ChannelOutboundAdapter = {
|
|||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ cfg, to, text, deps, replyToId, threadId, accountId }) => {
|
||||
const send = deps?.sendMatrix ?? sendMessageMatrix;
|
||||
const send =
|
||||
resolveOutboundSendDep<typeof sendMessageMatrix>(deps, "matrix") ?? sendMessageMatrix;
|
||||
const resolvedThreadId =
|
||||
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
|
||||
const result = await send(to, text, {
|
||||
|
|
@ -24,7 +26,8 @@ export const matrixOutbound: ChannelOutboundAdapter = {
|
|||
};
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, deps, replyToId, threadId, accountId }) => {
|
||||
const send = deps?.sendMatrix ?? sendMessageMatrix;
|
||||
const send =
|
||||
resolveOutboundSendDep<typeof sendMessageMatrix>(deps, "matrix") ?? sendMessageMatrix;
|
||||
const resolvedThreadId =
|
||||
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
|
||||
const result = await send(to, text, {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/msteams";
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
|
||||
import { createMSTeamsPollStoreFs } from "./polls.js";
|
||||
import { getMSTeamsRuntime } from "./runtime.js";
|
||||
import { sendMessageMSTeams, sendPollMSTeams } from "./send.js";
|
||||
|
|
@ -10,13 +11,24 @@ export const msteamsOutbound: ChannelOutboundAdapter = {
|
|||
textChunkLimit: 4000,
|
||||
pollMaxOptions: 12,
|
||||
sendText: async ({ cfg, to, text, deps }) => {
|
||||
const send = deps?.sendMSTeams ?? ((to, text) => sendMessageMSTeams({ cfg, to, text }));
|
||||
type SendFn = (
|
||||
to: string,
|
||||
text: string,
|
||||
) => Promise<{ messageId: string; conversationId: string }>;
|
||||
const send =
|
||||
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
|
||||
((to, text) => sendMessageMSTeams({ cfg, to, text }));
|
||||
const result = await send(to, text);
|
||||
return { channel: "msteams", ...result };
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, deps }) => {
|
||||
type SendFn = (
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: { mediaUrl?: string; mediaLocalRoots?: readonly string[] },
|
||||
) => Promise<{ messageId: string; conversationId: string }>;
|
||||
const send =
|
||||
deps?.sendMSTeams ??
|
||||
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
|
||||
((to, text, opts) =>
|
||||
sendMessageMSTeams({
|
||||
cfg,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
type ChannelPlugin,
|
||||
type ResolvedSignalAccount,
|
||||
} from "openclaw/plugin-sdk/signal";
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
|
||||
import { getSignalRuntime } from "./runtime.js";
|
||||
|
||||
const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
|
|
@ -84,9 +85,11 @@ async function sendSignalOutbound(params: {
|
|||
mediaUrl?: string;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
accountId?: string;
|
||||
deps?: { sendSignal?: SignalSendFn };
|
||||
deps?: { [channelId: string]: unknown };
|
||||
}) {
|
||||
const send = params.deps?.sendSignal ?? getSignalRuntime().channel.signal.sendMessageSignal;
|
||||
const send =
|
||||
resolveOutboundSendDep<SignalSendFn>(params.deps, "signal") ??
|
||||
getSignalRuntime().channel.signal.sendMessageSignal;
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg: params.cfg,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
type ChannelPlugin,
|
||||
type ResolvedSlackAccount,
|
||||
} from "openclaw/plugin-sdk/slack";
|
||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
|
||||
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
|
||||
import { getSlackRuntime } from "./runtime.js";
|
||||
|
||||
|
|
@ -77,11 +78,13 @@ type SlackSendFn = ReturnType<typeof getSlackRuntime>["channel"]["slack"]["sendM
|
|||
function resolveSlackSendContext(params: {
|
||||
cfg: Parameters<typeof resolveSlackAccount>[0]["cfg"];
|
||||
accountId?: string;
|
||||
deps?: { sendSlack?: SlackSendFn };
|
||||
deps?: { [channelId: string]: unknown };
|
||||
replyToId?: string | number | null;
|
||||
threadId?: string | number | null;
|
||||
}) {
|
||||
const send = params.deps?.sendSlack ?? getSlackRuntime().channel.slack.sendMessageSlack;
|
||||
const send =
|
||||
resolveOutboundSendDep<SlackSendFn>(params.deps, "slack") ??
|
||||
getSlackRuntime().channel.slack.sendMessageSlack;
|
||||
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const token = getTokenForOperation(account, "write");
|
||||
const botToken = account.botToken?.trim();
|
||||
|
|
|
|||
|
|
@ -40,8 +40,16 @@ import {
|
|||
type ResolvedTelegramAccount,
|
||||
type TelegramProbe,
|
||||
} from "openclaw/plugin-sdk/telegram";
|
||||
import {
|
||||
type OutboundSendDeps,
|
||||
resolveOutboundSendDep,
|
||||
} from "../../../src/infra/outbound/deliver.js";
|
||||
import { getTelegramRuntime } from "./runtime.js";
|
||||
|
||||
type TelegramSendFn = ReturnType<
|
||||
typeof getTelegramRuntime
|
||||
>["channel"]["telegram"]["sendMessageTelegram"];
|
||||
|
||||
const meta = getChatChannelMeta("telegram");
|
||||
|
||||
function findTelegramTokenOwnerAccountId(params: {
|
||||
|
|
@ -78,9 +86,6 @@ function formatDuplicateTelegramTokenReason(params: {
|
|||
);
|
||||
}
|
||||
|
||||
type TelegramSendFn = ReturnType<
|
||||
typeof getTelegramRuntime
|
||||
>["channel"]["telegram"]["sendMessageTelegram"];
|
||||
type TelegramSendOptions = NonNullable<Parameters<TelegramSendFn>[2]>;
|
||||
|
||||
function buildTelegramSendOptions(params: {
|
||||
|
|
@ -111,13 +116,14 @@ async function sendTelegramOutbound(params: {
|
|||
mediaUrl?: string | null;
|
||||
mediaLocalRoots?: readonly string[] | null;
|
||||
accountId?: string | null;
|
||||
deps?: { sendTelegram?: TelegramSendFn };
|
||||
deps?: OutboundSendDeps;
|
||||
replyToId?: string | null;
|
||||
threadId?: string | number | null;
|
||||
silent?: boolean | null;
|
||||
}) {
|
||||
const send =
|
||||
params.deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram;
|
||||
resolveOutboundSendDep<TelegramSendFn>(params.deps, "telegram") ??
|
||||
getTelegramRuntime().channel.telegram.sendMessageTelegram;
|
||||
return await send(
|
||||
params.to,
|
||||
params.text,
|
||||
|
|
@ -381,7 +387,9 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
|||
threadId,
|
||||
silent,
|
||||
}) => {
|
||||
const send = deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram;
|
||||
const send =
|
||||
resolveOutboundSendDep<TelegramSendFn>(deps, "telegram") ??
|
||||
getTelegramRuntime().channel.telegram.sendMessageTelegram;
|
||||
const result = await sendTelegramPayloadMessages({
|
||||
send,
|
||||
to,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
sendPollDiscord,
|
||||
sendWebhookMessageDiscord,
|
||||
} from "../../../discord/send.js";
|
||||
import { resolveOutboundSendDep } from "../../../infra/outbound/deliver.js";
|
||||
import type { OutboundIdentity } from "../../../infra/outbound/identity.js";
|
||||
import { normalizeDiscordOutboundTarget } from "../normalize/discord.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
|
|
@ -100,7 +101,8 @@ export const discordOutbound: ChannelOutboundAdapter = {
|
|||
return { channel: "discord", ...webhookResult };
|
||||
}
|
||||
}
|
||||
const send = deps?.sendDiscord ?? sendMessageDiscord;
|
||||
const send =
|
||||
resolveOutboundSendDep<typeof sendMessageDiscord>(deps, "discord") ?? sendMessageDiscord;
|
||||
const target = resolveDiscordOutboundTarget({ to, threadId });
|
||||
const result = await send(target, text, {
|
||||
verbose: false,
|
||||
|
|
@ -123,7 +125,8 @@ export const discordOutbound: ChannelOutboundAdapter = {
|
|||
threadId,
|
||||
silent,
|
||||
}) => {
|
||||
const send = deps?.sendDiscord ?? sendMessageDiscord;
|
||||
const send =
|
||||
resolveOutboundSendDep<typeof sendMessageDiscord>(deps, "discord") ?? sendMessageDiscord;
|
||||
const target = resolveDiscordOutboundTarget({ to, threadId });
|
||||
const result = await send(target, text, {
|
||||
verbose: false,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ describe("imessageOutbound", () => {
|
|||
text: "hello",
|
||||
accountId: "default",
|
||||
replyToId: "msg-123",
|
||||
deps: { sendIMessage },
|
||||
deps: { imessage: sendIMessage },
|
||||
});
|
||||
|
||||
expect(sendIMessage).toHaveBeenCalledWith(
|
||||
|
|
@ -50,7 +50,7 @@ describe("imessageOutbound", () => {
|
|||
mediaLocalRoots: ["/tmp"],
|
||||
accountId: "acct-1",
|
||||
replyToId: "msg-456",
|
||||
deps: { sendIMessage },
|
||||
deps: { imessage: sendIMessage },
|
||||
});
|
||||
|
||||
expect(sendIMessage).toHaveBeenCalledWith(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { sendMessageIMessage } from "../../../imessage/send.js";
|
||||
import type { OutboundSendDeps } from "../../../infra/outbound/deliver.js";
|
||||
import { resolveOutboundSendDep, type OutboundSendDeps } from "../../../infra/outbound/deliver.js";
|
||||
import {
|
||||
createScopedChannelMediaMaxBytesResolver,
|
||||
createDirectTextMediaOutbound,
|
||||
} from "./direct-text-media.js";
|
||||
|
||||
function resolveIMessageSender(deps: OutboundSendDeps | undefined) {
|
||||
return deps?.sendIMessage ?? sendMessageIMessage;
|
||||
return (
|
||||
resolveOutboundSendDep<typeof sendMessageIMessage>(deps, "imessage") ?? sendMessageIMessage
|
||||
);
|
||||
}
|
||||
|
||||
export const imessageOutbound = createDirectTextMediaOutbound({
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ describe("signalOutbound", () => {
|
|||
to: "+15555550123",
|
||||
text: "hello",
|
||||
accountId: "work",
|
||||
deps: { sendSignal },
|
||||
deps: { signal: sendSignal },
|
||||
});
|
||||
|
||||
expect(sendSignal).toHaveBeenCalledWith(
|
||||
|
|
@ -52,7 +52,7 @@ describe("signalOutbound", () => {
|
|||
mediaUrl: "https://example.com/file.jpg",
|
||||
mediaLocalRoots: ["/tmp/media"],
|
||||
accountId: "default",
|
||||
deps: { sendSignal },
|
||||
deps: { signal: sendSignal },
|
||||
});
|
||||
|
||||
expect(sendSignal).toHaveBeenCalledWith(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { OutboundSendDeps } from "../../../infra/outbound/deliver.js";
|
||||
import { resolveOutboundSendDep, type OutboundSendDeps } from "../../../infra/outbound/deliver.js";
|
||||
import { sendMessageSignal } from "../../../signal/send.js";
|
||||
import {
|
||||
createScopedChannelMediaMaxBytesResolver,
|
||||
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from "./direct-text-media.js";
|
||||
|
||||
function resolveSignalSender(deps: OutboundSendDeps | undefined) {
|
||||
return deps?.sendSignal ?? sendMessageSignal;
|
||||
return resolveOutboundSendDep<typeof sendMessageSignal>(deps, "signal") ?? sendMessageSignal;
|
||||
}
|
||||
|
||||
export const signalOutbound = createDirectTextMediaOutbound({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { resolveOutboundSendDep } from "../../../infra/outbound/deliver.js";
|
||||
import type { OutboundIdentity } from "../../../infra/outbound/identity.js";
|
||||
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
|
||||
import { parseSlackBlocksInput } from "../../../slack/blocks-input.js";
|
||||
|
|
@ -56,12 +57,13 @@ async function sendSlackOutboundMessage(params: {
|
|||
mediaLocalRoots?: readonly string[];
|
||||
blocks?: NonNullable<Parameters<typeof sendMessageSlack>[2]>["blocks"];
|
||||
accountId?: string | null;
|
||||
deps?: { sendSlack?: typeof sendMessageSlack } | null;
|
||||
deps?: { [channelId: string]: unknown } | null;
|
||||
replyToId?: string | null;
|
||||
threadId?: string | number | null;
|
||||
identity?: OutboundIdentity;
|
||||
}) {
|
||||
const send = params.deps?.sendSlack ?? sendMessageSlack;
|
||||
const send =
|
||||
resolveOutboundSendDep<typeof sendMessageSlack>(params.deps, "slack") ?? sendMessageSlack;
|
||||
// Use threadId fallback so routed tool notifications stay in the Slack thread.
|
||||
const threadTs =
|
||||
params.replyToId ?? (params.threadId != null ? String(params.threadId) : undefined);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ describe("telegramOutbound", () => {
|
|||
accountId: "work",
|
||||
replyToId: "44",
|
||||
threadId: "55",
|
||||
deps: { sendTelegram },
|
||||
deps: { telegram: sendTelegram },
|
||||
});
|
||||
|
||||
expect(sendTelegram).toHaveBeenCalledWith(
|
||||
|
|
@ -43,7 +43,7 @@ describe("telegramOutbound", () => {
|
|||
text: "<b>hello</b>",
|
||||
accountId: "work",
|
||||
threadId: "12345:99",
|
||||
deps: { sendTelegram },
|
||||
deps: { telegram: sendTelegram },
|
||||
});
|
||||
|
||||
expect(sendTelegram).toHaveBeenCalledWith(
|
||||
|
|
@ -70,7 +70,7 @@ describe("telegramOutbound", () => {
|
|||
mediaUrl: "https://example.com/a.jpg",
|
||||
mediaLocalRoots: ["/tmp/media"],
|
||||
accountId: "default",
|
||||
deps: { sendTelegram },
|
||||
deps: { telegram: sendTelegram },
|
||||
});
|
||||
|
||||
expect(sendTelegram).toHaveBeenCalledWith(
|
||||
|
|
@ -112,7 +112,7 @@ describe("telegramOutbound", () => {
|
|||
payload,
|
||||
mediaLocalRoots: ["/tmp/media"],
|
||||
accountId: "default",
|
||||
deps: { sendTelegram },
|
||||
deps: { telegram: sendTelegram },
|
||||
});
|
||||
|
||||
expect(sendTelegram).toHaveBeenCalledTimes(2);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ReplyPayload } from "../../../auto-reply/types.js";
|
||||
import type { OutboundSendDeps } from "../../../infra/outbound/deliver.js";
|
||||
import { resolveOutboundSendDep, type OutboundSendDeps } from "../../../infra/outbound/deliver.js";
|
||||
import type { TelegramInlineButtons } from "../../../telegram/button-types.js";
|
||||
import { markdownToTelegramHtmlChunks } from "../../../telegram/format.js";
|
||||
import {
|
||||
|
|
@ -30,7 +30,9 @@ function resolveTelegramSendContext(params: {
|
|||
accountId?: string;
|
||||
};
|
||||
} {
|
||||
const send = params.deps?.sendTelegram ?? sendMessageTelegram;
|
||||
const send =
|
||||
resolveOutboundSendDep<typeof sendMessageTelegram>(params.deps, "telegram") ??
|
||||
sendMessageTelegram;
|
||||
return {
|
||||
send,
|
||||
baseOpts: {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ describe("telegramOutbound.sendPayload", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
deps: { sendTelegram },
|
||||
deps: { telegram: sendTelegram },
|
||||
});
|
||||
|
||||
expect(sendTelegram).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -121,7 +121,7 @@ describe("telegramOutbound.sendPayload", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
deps: { sendTelegram },
|
||||
deps: { telegram: sendTelegram },
|
||||
});
|
||||
|
||||
expect(sendTelegram).toHaveBeenCalledTimes(2);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { resolveOutboundSendDep } from "../../infra/outbound/deliver.js";
|
||||
import type { PluginRuntimeChannel } from "../../plugins/runtime/types-channel.js";
|
||||
import { escapeRegExp } from "../../utils.js";
|
||||
import { resolveWhatsAppOutboundTarget } from "../../whatsapp/resolve-outbound-target.js";
|
||||
|
|
@ -66,7 +67,8 @@ export function createWhatsAppOutboundBase({
|
|||
if (skipEmptyText && !normalizedText) {
|
||||
return { channel: "whatsapp", messageId: "" };
|
||||
}
|
||||
const send = deps?.sendWhatsApp ?? sendMessageWhatsApp;
|
||||
const send =
|
||||
resolveOutboundSendDep<WhatsAppSendMessage>(deps, "whatsapp") ?? sendMessageWhatsApp;
|
||||
const result = await send(to, normalizedText, {
|
||||
verbose: false,
|
||||
cfg,
|
||||
|
|
@ -85,7 +87,8 @@ export function createWhatsAppOutboundBase({
|
|||
deps,
|
||||
gifPlayback,
|
||||
}) => {
|
||||
const send = deps?.sendWhatsApp ?? sendMessageWhatsApp;
|
||||
const send =
|
||||
resolveOutboundSendDep<WhatsAppSendMessage>(deps, "whatsapp") ?? sendMessageWhatsApp;
|
||||
const result = await send(to, normalizeText(text), {
|
||||
verbose: false,
|
||||
cfg,
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { sendMessageDiscord } from "../discord/send.js";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { sendMessageIMessage } from "../imessage/send.js";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { sendMessageSignal } from "../signal/send.js";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { sendMessageSlack } from "../slack/send.js";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { sendMessageTelegram } from "../telegram/send.js";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { sendMessageWhatsApp } from "../channels/web/index.js";
|
||||
|
|
@ -74,9 +74,7 @@ describe("createDefaultDeps", () => {
|
|||
expect(moduleLoads.signal).not.toHaveBeenCalled();
|
||||
expect(moduleLoads.imessage).not.toHaveBeenCalled();
|
||||
|
||||
const sendTelegram = deps.sendMessageTelegram as unknown as (
|
||||
...args: unknown[]
|
||||
) => Promise<unknown>;
|
||||
const sendTelegram = deps["telegram"] as (...args: unknown[]) => Promise<unknown>;
|
||||
await sendTelegram("chat", "hello", { verbose: false });
|
||||
|
||||
expect(moduleLoads.telegram).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -86,9 +84,7 @@ describe("createDefaultDeps", () => {
|
|||
|
||||
it("reuses module cache after first dynamic import", async () => {
|
||||
const deps = createDefaultDeps();
|
||||
const sendDiscord = deps.sendMessageDiscord as unknown as (
|
||||
...args: unknown[]
|
||||
) => Promise<unknown>;
|
||||
const sendDiscord = deps["discord"] as (...args: unknown[]) => Promise<unknown>;
|
||||
|
||||
await sendDiscord("channel", "first", { verbose: false });
|
||||
await sendDiscord("channel", "second", { verbose: false });
|
||||
|
|
|
|||
135
src/cli/deps.ts
135
src/cli/deps.ts
|
|
@ -1,89 +1,68 @@
|
|||
import type { sendMessageWhatsApp } from "../channels/web/index.js";
|
||||
import type { sendMessageDiscord } from "../discord/send.js";
|
||||
import type { sendMessageIMessage } from "../imessage/send.js";
|
||||
import type { OutboundSendDeps } from "../infra/outbound/deliver.js";
|
||||
import type { sendMessageSignal } from "../signal/send.js";
|
||||
import type { sendMessageSlack } from "../slack/send.js";
|
||||
import type { sendMessageTelegram } from "../telegram/send.js";
|
||||
import { createOutboundSendDepsFromCliSource } from "./outbound-send-mapping.js";
|
||||
|
||||
export type CliDeps = {
|
||||
sendMessageWhatsApp: typeof sendMessageWhatsApp;
|
||||
sendMessageTelegram: typeof sendMessageTelegram;
|
||||
sendMessageDiscord: typeof sendMessageDiscord;
|
||||
sendMessageSlack: typeof sendMessageSlack;
|
||||
sendMessageSignal: typeof sendMessageSignal;
|
||||
sendMessageIMessage: typeof sendMessageIMessage;
|
||||
/**
|
||||
* Lazy-loaded per-channel send functions, keyed by channel ID.
|
||||
* Values are proxy functions that dynamically import the real module on first use.
|
||||
*/
|
||||
export type CliDeps = { [channelId: string]: unknown };
|
||||
|
||||
// Per-channel module caches for lazy loading.
|
||||
const senderCache = new Map<string, Promise<Record<string, unknown>>>();
|
||||
|
||||
/**
|
||||
* Create a lazy-loading send function proxy for a channel.
|
||||
* The channel's module is loaded on first call and cached for reuse.
|
||||
*/
|
||||
function createLazySender(
|
||||
channelId: string,
|
||||
loader: () => Promise<Record<string, unknown>>,
|
||||
exportName: string,
|
||||
): (...args: unknown[]) => Promise<unknown> {
|
||||
return async (...args: unknown[]) => {
|
||||
let cached = senderCache.get(channelId);
|
||||
if (!cached) {
|
||||
cached = loader();
|
||||
senderCache.set(channelId, cached);
|
||||
}
|
||||
const mod = await cached;
|
||||
const fn = mod[exportName] as (...a: unknown[]) => Promise<unknown>;
|
||||
return await fn(...args);
|
||||
};
|
||||
|
||||
let whatsappSenderRuntimePromise: Promise<typeof import("./deps-send-whatsapp.runtime.js")> | null =
|
||||
null;
|
||||
let telegramSenderRuntimePromise: Promise<typeof import("./deps-send-telegram.runtime.js")> | null =
|
||||
null;
|
||||
let discordSenderRuntimePromise: Promise<typeof import("./deps-send-discord.runtime.js")> | null =
|
||||
null;
|
||||
let slackSenderRuntimePromise: Promise<typeof import("./deps-send-slack.runtime.js")> | null = null;
|
||||
let signalSenderRuntimePromise: Promise<typeof import("./deps-send-signal.runtime.js")> | null =
|
||||
null;
|
||||
let imessageSenderRuntimePromise: Promise<typeof import("./deps-send-imessage.runtime.js")> | null =
|
||||
null;
|
||||
|
||||
function loadWhatsAppSenderRuntime() {
|
||||
whatsappSenderRuntimePromise ??= import("./deps-send-whatsapp.runtime.js");
|
||||
return whatsappSenderRuntimePromise;
|
||||
}
|
||||
|
||||
function loadTelegramSenderRuntime() {
|
||||
telegramSenderRuntimePromise ??= import("./deps-send-telegram.runtime.js");
|
||||
return telegramSenderRuntimePromise;
|
||||
}
|
||||
|
||||
function loadDiscordSenderRuntime() {
|
||||
discordSenderRuntimePromise ??= import("./deps-send-discord.runtime.js");
|
||||
return discordSenderRuntimePromise;
|
||||
}
|
||||
|
||||
function loadSlackSenderRuntime() {
|
||||
slackSenderRuntimePromise ??= import("./deps-send-slack.runtime.js");
|
||||
return slackSenderRuntimePromise;
|
||||
}
|
||||
|
||||
function loadSignalSenderRuntime() {
|
||||
signalSenderRuntimePromise ??= import("./deps-send-signal.runtime.js");
|
||||
return signalSenderRuntimePromise;
|
||||
}
|
||||
|
||||
function loadIMessageSenderRuntime() {
|
||||
imessageSenderRuntimePromise ??= import("./deps-send-imessage.runtime.js");
|
||||
return imessageSenderRuntimePromise;
|
||||
}
|
||||
|
||||
export function createDefaultDeps(): CliDeps {
|
||||
return {
|
||||
sendMessageWhatsApp: async (...args) => {
|
||||
const { sendMessageWhatsApp } = await loadWhatsAppSenderRuntime();
|
||||
return await sendMessageWhatsApp(...args);
|
||||
},
|
||||
sendMessageTelegram: async (...args) => {
|
||||
const { sendMessageTelegram } = await loadTelegramSenderRuntime();
|
||||
return await sendMessageTelegram(...args);
|
||||
},
|
||||
sendMessageDiscord: async (...args) => {
|
||||
const { sendMessageDiscord } = await loadDiscordSenderRuntime();
|
||||
return await sendMessageDiscord(...args);
|
||||
},
|
||||
sendMessageSlack: async (...args) => {
|
||||
const { sendMessageSlack } = await loadSlackSenderRuntime();
|
||||
return await sendMessageSlack(...args);
|
||||
},
|
||||
sendMessageSignal: async (...args) => {
|
||||
const { sendMessageSignal } = await loadSignalSenderRuntime();
|
||||
return await sendMessageSignal(...args);
|
||||
},
|
||||
sendMessageIMessage: async (...args) => {
|
||||
const { sendMessageIMessage } = await loadIMessageSenderRuntime();
|
||||
return await sendMessageIMessage(...args);
|
||||
},
|
||||
whatsapp: createLazySender(
|
||||
"whatsapp",
|
||||
() => import("../channels/web/index.js") as Promise<Record<string, unknown>>,
|
||||
"sendMessageWhatsApp",
|
||||
),
|
||||
telegram: createLazySender(
|
||||
"telegram",
|
||||
() => import("../telegram/send.js") as Promise<Record<string, unknown>>,
|
||||
"sendMessageTelegram",
|
||||
),
|
||||
discord: createLazySender(
|
||||
"discord",
|
||||
() => import("../discord/send.js") as Promise<Record<string, unknown>>,
|
||||
"sendMessageDiscord",
|
||||
),
|
||||
slack: createLazySender(
|
||||
"slack",
|
||||
() => import("../slack/send.js") as Promise<Record<string, unknown>>,
|
||||
"sendMessageSlack",
|
||||
),
|
||||
signal: createLazySender(
|
||||
"signal",
|
||||
() => import("../signal/send.js") as Promise<Record<string, unknown>>,
|
||||
"sendMessageSignal",
|
||||
),
|
||||
imessage: createLazySender(
|
||||
"imessage",
|
||||
() => import("../imessage/send.js") as Promise<Record<string, unknown>>,
|
||||
"sendMessageIMessage",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
type CliOutboundSendSource,
|
||||
} from "./outbound-send-mapping.js";
|
||||
|
||||
export type CliDeps = Required<CliOutboundSendSource>;
|
||||
export type CliDeps = CliOutboundSendSource;
|
||||
|
||||
export function createOutboundSendDeps(deps: CliDeps): OutboundSendDeps {
|
||||
return createOutboundSendDepsFromCliSource(deps);
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createOutboundSendDepsFromCliSource,
|
||||
type CliOutboundSendSource,
|
||||
} from "./outbound-send-mapping.js";
|
||||
import { createOutboundSendDepsFromCliSource } from "./outbound-send-mapping.js";
|
||||
|
||||
describe("createOutboundSendDepsFromCliSource", () => {
|
||||
it("maps CLI send deps to outbound send deps", () => {
|
||||
const deps: CliOutboundSendSource = {
|
||||
sendMessageWhatsApp: vi.fn() as CliOutboundSendSource["sendMessageWhatsApp"],
|
||||
sendMessageTelegram: vi.fn() as CliOutboundSendSource["sendMessageTelegram"],
|
||||
sendMessageDiscord: vi.fn() as CliOutboundSendSource["sendMessageDiscord"],
|
||||
sendMessageSlack: vi.fn() as CliOutboundSendSource["sendMessageSlack"],
|
||||
sendMessageSignal: vi.fn() as CliOutboundSendSource["sendMessageSignal"],
|
||||
sendMessageIMessage: vi.fn() as CliOutboundSendSource["sendMessageIMessage"],
|
||||
it("adds legacy aliases for channel-keyed send deps", () => {
|
||||
const deps = {
|
||||
whatsapp: vi.fn(),
|
||||
telegram: vi.fn(),
|
||||
discord: vi.fn(),
|
||||
slack: vi.fn(),
|
||||
signal: vi.fn(),
|
||||
imessage: vi.fn(),
|
||||
};
|
||||
|
||||
const outbound = createOutboundSendDepsFromCliSource(deps);
|
||||
|
||||
expect(outbound).toEqual({
|
||||
sendWhatsApp: deps.sendMessageWhatsApp,
|
||||
sendTelegram: deps.sendMessageTelegram,
|
||||
sendDiscord: deps.sendMessageDiscord,
|
||||
sendSlack: deps.sendMessageSlack,
|
||||
sendSignal: deps.sendMessageSignal,
|
||||
sendIMessage: deps.sendMessageIMessage,
|
||||
whatsapp: deps.whatsapp,
|
||||
telegram: deps.telegram,
|
||||
discord: deps.discord,
|
||||
slack: deps.slack,
|
||||
signal: deps.signal,
|
||||
imessage: deps.imessage,
|
||||
sendWhatsApp: deps.whatsapp,
|
||||
sendTelegram: deps.telegram,
|
||||
sendDiscord: deps.discord,
|
||||
sendSlack: deps.slack,
|
||||
sendSignal: deps.signal,
|
||||
sendIMessage: deps.imessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,22 +1,49 @@
|
|||
import type { OutboundSendDeps } from "../infra/outbound/deliver.js";
|
||||
|
||||
export type CliOutboundSendSource = {
|
||||
sendMessageWhatsApp: OutboundSendDeps["sendWhatsApp"];
|
||||
sendMessageTelegram: OutboundSendDeps["sendTelegram"];
|
||||
sendMessageDiscord: OutboundSendDeps["sendDiscord"];
|
||||
sendMessageSlack: OutboundSendDeps["sendSlack"];
|
||||
sendMessageSignal: OutboundSendDeps["sendSignal"];
|
||||
sendMessageIMessage: OutboundSendDeps["sendIMessage"];
|
||||
};
|
||||
/**
|
||||
* CLI-internal send function sources, keyed by channel ID.
|
||||
* Each value is a lazily-loaded send function for that channel.
|
||||
*/
|
||||
export type CliOutboundSendSource = { [channelId: string]: unknown };
|
||||
|
||||
// Provider docking: extend this mapping when adding new outbound send deps.
|
||||
const LEGACY_SOURCE_TO_CHANNEL = {
|
||||
sendMessageWhatsApp: "whatsapp",
|
||||
sendMessageTelegram: "telegram",
|
||||
sendMessageDiscord: "discord",
|
||||
sendMessageSlack: "slack",
|
||||
sendMessageSignal: "signal",
|
||||
sendMessageIMessage: "imessage",
|
||||
} as const;
|
||||
|
||||
const CHANNEL_TO_LEGACY_DEP_KEY = {
|
||||
whatsapp: "sendWhatsApp",
|
||||
telegram: "sendTelegram",
|
||||
discord: "sendDiscord",
|
||||
slack: "sendSlack",
|
||||
signal: "sendSignal",
|
||||
imessage: "sendIMessage",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Pass CLI send sources through as-is — both CliOutboundSendSource and
|
||||
* OutboundSendDeps are now channel-ID-keyed records.
|
||||
*/
|
||||
export function createOutboundSendDepsFromCliSource(deps: CliOutboundSendSource): OutboundSendDeps {
|
||||
return {
|
||||
sendWhatsApp: deps.sendMessageWhatsApp,
|
||||
sendTelegram: deps.sendMessageTelegram,
|
||||
sendDiscord: deps.sendMessageDiscord,
|
||||
sendSlack: deps.sendMessageSlack,
|
||||
sendSignal: deps.sendMessageSignal,
|
||||
sendIMessage: deps.sendMessageIMessage,
|
||||
};
|
||||
const outbound: OutboundSendDeps = { ...deps };
|
||||
|
||||
for (const [legacySourceKey, channelId] of Object.entries(LEGACY_SOURCE_TO_CHANNEL)) {
|
||||
const sourceValue = deps[legacySourceKey];
|
||||
if (sourceValue !== undefined && outbound[channelId] === undefined) {
|
||||
outbound[channelId] = sourceValue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [channelId, legacyDepKey] of Object.entries(CHANNEL_TO_LEGACY_DEP_KEY)) {
|
||||
const sourceValue = outbound[channelId];
|
||||
if (sourceValue !== undefined && outbound[legacyDepKey] === undefined) {
|
||||
outbound[legacyDepKey] = sourceValue;
|
||||
}
|
||||
}
|
||||
|
||||
return outbound;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,16 +218,7 @@ async function expectDefaultThinkLevel(params: {
|
|||
function createTelegramOutboundPlugin() {
|
||||
const sendWithTelegram = async (
|
||||
ctx: {
|
||||
deps?: {
|
||||
sendTelegram?: (
|
||||
to: string,
|
||||
text: string,
|
||||
opts: Record<string, unknown>,
|
||||
) => Promise<{
|
||||
messageId: string;
|
||||
chatId: string;
|
||||
}>;
|
||||
};
|
||||
deps?: { [channelId: string]: unknown };
|
||||
to: string;
|
||||
text: string;
|
||||
accountId?: string | null;
|
||||
|
|
@ -235,7 +226,13 @@ function createTelegramOutboundPlugin() {
|
|||
},
|
||||
mediaUrl?: string,
|
||||
) => {
|
||||
const sendTelegram = ctx.deps?.sendTelegram;
|
||||
const sendTelegram = ctx.deps?.["telegram"] as
|
||||
| ((
|
||||
to: string,
|
||||
text: string,
|
||||
opts: Record<string, unknown>,
|
||||
) => Promise<{ messageId: string; chatId: string }>)
|
||||
| undefined;
|
||||
if (!sendTelegram) {
|
||||
throw new Error("sendTelegram dependency missing");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,8 @@ describe("runCronIsolatedAgentTurn", () => {
|
|||
await withTempHome(async (home) => {
|
||||
const { storePath, deps } = await createTelegramDeliveryFixture(home);
|
||||
|
||||
vi.mocked(runSubagentAnnounceFlow).mockClear();
|
||||
vi.mocked(deps.sendMessageTelegram as (...args: unknown[]) => unknown).mockClear();
|
||||
mockEmbeddedAgentPayloads([{ text: "HEARTBEAT_OK 🦞" }]);
|
||||
|
||||
const cfg = makeCfg(home, storePath);
|
||||
|
|
@ -215,6 +217,10 @@ describe("runCronIsolatedAgentTurn", () => {
|
|||
},
|
||||
};
|
||||
|
||||
vi.mocked(deps.sendMessageTelegram as (...args: unknown[]) => unknown).mockClear();
|
||||
vi.mocked(runSubagentAnnounceFlow).mockClear();
|
||||
vi.mocked(callGateway).mockClear();
|
||||
|
||||
const deleteRes = await runCronIsolatedAgentTurn({
|
||||
cfg,
|
||||
deps,
|
||||
|
|
|
|||
|
|
@ -51,18 +51,21 @@ beforeAll(async () => {
|
|||
const whatsappOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
sendText: async ({ deps, to, text }) => {
|
||||
if (!deps?.sendWhatsApp) {
|
||||
throw new Error("Missing sendWhatsApp dep");
|
||||
}
|
||||
return { channel: "whatsapp", ...(await deps.sendWhatsApp(to, text, { verbose: false })) };
|
||||
},
|
||||
sendMedia: async ({ deps, to, text, mediaUrl }) => {
|
||||
if (!deps?.sendWhatsApp) {
|
||||
if (!deps?.["whatsapp"]) {
|
||||
throw new Error("Missing sendWhatsApp dep");
|
||||
}
|
||||
return {
|
||||
channel: "whatsapp",
|
||||
...(await deps.sendWhatsApp(to, text, { verbose: false, mediaUrl })),
|
||||
...(await (deps["whatsapp"] as Function)(to, text, { verbose: false })),
|
||||
};
|
||||
},
|
||||
sendMedia: async ({ deps, to, text, mediaUrl }) => {
|
||||
if (!deps?.["whatsapp"]) {
|
||||
throw new Error("Missing sendWhatsApp dep");
|
||||
}
|
||||
return {
|
||||
channel: "whatsapp",
|
||||
...(await (deps["whatsapp"] as Function)(to, text, { verbose: false, mediaUrl })),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
|
|||
agentId: "main",
|
||||
reason: params.reason,
|
||||
deps: {
|
||||
sendTelegram,
|
||||
telegram: sendTelegram,
|
||||
},
|
||||
});
|
||||
const calledCtx = (getReplySpy.mock.calls[0]?.[0] ?? null) as {
|
||||
|
|
|
|||
|
|
@ -48,9 +48,7 @@ describe("runHeartbeatOnce ack handling", () => {
|
|||
} = {},
|
||||
) {
|
||||
return {
|
||||
...(params.sendWhatsApp
|
||||
? { sendWhatsApp: params.sendWhatsApp as unknown as HeartbeatDeps["sendWhatsApp"] }
|
||||
: {}),
|
||||
...(params.sendWhatsApp ? { whatsapp: params.sendWhatsApp as unknown } : {}),
|
||||
getQueueSize: params.getQueueSize ?? (() => 0),
|
||||
nowMs: params.nowMs ?? (() => 0),
|
||||
webAuthExists: params.webAuthExists ?? (async () => true),
|
||||
|
|
@ -66,9 +64,7 @@ describe("runHeartbeatOnce ack handling", () => {
|
|||
} = {},
|
||||
) {
|
||||
return {
|
||||
...(params.sendTelegram
|
||||
? { sendTelegram: params.sendTelegram as unknown as HeartbeatDeps["sendTelegram"] }
|
||||
: {}),
|
||||
...(params.sendTelegram ? { telegram: params.sendTelegram as unknown } : {}),
|
||||
getQueueSize: params.getQueueSize ?? (() => 0),
|
||||
nowMs: params.nowMs ?? (() => 0),
|
||||
} satisfies HeartbeatDeps;
|
||||
|
|
|
|||
|
|
@ -59,20 +59,20 @@ beforeAll(async () => {
|
|||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
sendText: async ({ to, text, deps, accountId }) => {
|
||||
if (!deps?.sendTelegram) {
|
||||
if (!deps?.["telegram"]) {
|
||||
throw new Error("sendTelegram missing");
|
||||
}
|
||||
const res = await deps.sendTelegram(to, text, {
|
||||
const res = await (deps["telegram"] as Function)(to, text, {
|
||||
verbose: false,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { channel: "telegram", messageId: res.messageId, chatId: res.chatId };
|
||||
},
|
||||
sendMedia: async ({ to, text, mediaUrl, deps, accountId }) => {
|
||||
if (!deps?.sendTelegram) {
|
||||
if (!deps?.["telegram"]) {
|
||||
throw new Error("sendTelegram missing");
|
||||
}
|
||||
const res = await deps.sendTelegram(to, text, {
|
||||
const res = await (deps["telegram"] as Function)(to, text, {
|
||||
verbose: false,
|
||||
accountId: accountId ?? undefined,
|
||||
mediaUrl,
|
||||
|
|
@ -468,10 +468,14 @@ describe("resolveHeartbeatSenderContext", () => {
|
|||
|
||||
describe("runHeartbeatOnce", () => {
|
||||
const createHeartbeatDeps = (
|
||||
sendWhatsApp: NonNullable<HeartbeatDeps["sendWhatsApp"]>,
|
||||
sendWhatsApp: (
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: unknown,
|
||||
) => Promise<{ messageId: string; toJid: string }>,
|
||||
nowMs = 0,
|
||||
): HeartbeatDeps => ({
|
||||
sendWhatsApp,
|
||||
whatsapp: sendWhatsApp,
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => nowMs,
|
||||
webAuthExists: async () => true,
|
||||
|
|
@ -547,7 +551,15 @@ describe("runHeartbeatOnce", () => {
|
|||
);
|
||||
|
||||
replySpy.mockResolvedValue([{ text: "Let me check..." }, { text: "Final alert" }]);
|
||||
const sendWhatsApp = vi.fn<NonNullable<HeartbeatDeps["sendWhatsApp"]>>().mockResolvedValue({
|
||||
const sendWhatsApp = vi
|
||||
.fn<
|
||||
(
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: unknown,
|
||||
) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValue({
|
||||
messageId: "m1",
|
||||
toJid: "jid",
|
||||
});
|
||||
|
|
@ -604,7 +616,15 @@ describe("runHeartbeatOnce", () => {
|
|||
}),
|
||||
);
|
||||
replySpy.mockResolvedValue([{ text: "Final alert" }]);
|
||||
const sendWhatsApp = vi.fn<NonNullable<HeartbeatDeps["sendWhatsApp"]>>().mockResolvedValue({
|
||||
const sendWhatsApp = vi
|
||||
.fn<
|
||||
(
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: unknown,
|
||||
) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValue({
|
||||
messageId: "m1",
|
||||
toJid: "jid",
|
||||
});
|
||||
|
|
@ -682,7 +702,15 @@ describe("runHeartbeatOnce", () => {
|
|||
);
|
||||
|
||||
replySpy.mockResolvedValue([{ text: "Final alert" }]);
|
||||
const sendWhatsApp = vi.fn<NonNullable<HeartbeatDeps["sendWhatsApp"]>>().mockResolvedValue({
|
||||
const sendWhatsApp = vi
|
||||
.fn<
|
||||
(
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: unknown,
|
||||
) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValue({
|
||||
messageId: "m1",
|
||||
toJid: "jid",
|
||||
});
|
||||
|
|
@ -799,7 +827,13 @@ describe("runHeartbeatOnce", () => {
|
|||
replySpy.mockClear();
|
||||
replySpy.mockResolvedValue([{ text: testCase.message }]);
|
||||
const sendWhatsApp = vi
|
||||
.fn<NonNullable<HeartbeatDeps["sendWhatsApp"]>>()
|
||||
.fn<
|
||||
(
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: unknown,
|
||||
) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
|
||||
|
||||
await runHeartbeatOnce({
|
||||
|
|
@ -863,7 +897,13 @@ describe("runHeartbeatOnce", () => {
|
|||
|
||||
replySpy.mockResolvedValue([{ text: "Final alert" }]);
|
||||
const sendWhatsApp = vi
|
||||
.fn<NonNullable<HeartbeatDeps["sendWhatsApp"]>>()
|
||||
.fn<
|
||||
(
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: unknown,
|
||||
) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
|
||||
|
||||
await runHeartbeatOnce({
|
||||
|
|
@ -935,7 +975,13 @@ describe("runHeartbeatOnce", () => {
|
|||
replySpy.mockClear();
|
||||
replySpy.mockResolvedValue(testCase.replies);
|
||||
const sendWhatsApp = vi
|
||||
.fn<NonNullable<HeartbeatDeps["sendWhatsApp"]>>()
|
||||
.fn<
|
||||
(
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: unknown,
|
||||
) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
|
||||
|
||||
await runHeartbeatOnce({
|
||||
|
|
@ -990,7 +1036,15 @@ describe("runHeartbeatOnce", () => {
|
|||
);
|
||||
|
||||
replySpy.mockResolvedValue({ text: "Hello from heartbeat" });
|
||||
const sendWhatsApp = vi.fn<NonNullable<HeartbeatDeps["sendWhatsApp"]>>().mockResolvedValue({
|
||||
const sendWhatsApp = vi
|
||||
.fn<
|
||||
(
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: unknown,
|
||||
) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValue({
|
||||
messageId: "m1",
|
||||
toJid: "jid",
|
||||
});
|
||||
|
|
@ -1073,7 +1127,9 @@ describe("runHeartbeatOnce", () => {
|
|||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
replySpy.mockResolvedValue({ text: params.replyText ?? "Checked logs and PRs" });
|
||||
const sendWhatsApp = vi
|
||||
.fn<NonNullable<HeartbeatDeps["sendWhatsApp"]>>()
|
||||
.fn<
|
||||
(to: string, text: string, opts?: unknown) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
|
||||
const res = await runHeartbeatOnce({
|
||||
cfg,
|
||||
|
|
@ -1239,7 +1295,9 @@ describe("runHeartbeatOnce", () => {
|
|||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
replySpy.mockResolvedValue({ text: "Handled internally" });
|
||||
const sendWhatsApp = vi
|
||||
.fn<NonNullable<HeartbeatDeps["sendWhatsApp"]>>()
|
||||
.fn<
|
||||
(to: string, text: string, opts?: unknown) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
|
||||
|
||||
try {
|
||||
|
|
@ -1292,7 +1350,9 @@ describe("runHeartbeatOnce", () => {
|
|||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
replySpy.mockResolvedValue({ text: "Handled internally" });
|
||||
const sendWhatsApp = vi
|
||||
.fn<NonNullable<HeartbeatDeps["sendWhatsApp"]>>()
|
||||
.fn<
|
||||
(to: string, text: string, opts?: unknown) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ describe("runHeartbeatOnce", () => {
|
|||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: {
|
||||
sendSlack,
|
||||
slack: sendSlack,
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
|||
import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js";
|
||||
import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js";
|
||||
import type {
|
||||
DeliverOutboundPayloadsParams,
|
||||
OutboundDeliveryResult,
|
||||
OutboundSendDeps,
|
||||
} from "./deliver.js";
|
||||
import type { DeliverOutboundPayloadsParams, OutboundDeliveryResult } from "./deliver.js";
|
||||
|
||||
type DeliverMockState = {
|
||||
sessions: {
|
||||
|
|
@ -215,7 +211,9 @@ export async function runChunkedWhatsAppDelivery(params: {
|
|||
mirror?: DeliverOutboundPayloadsParams["mirror"];
|
||||
}) {
|
||||
const sendWhatsApp = vi
|
||||
.fn<NonNullable<OutboundSendDeps["sendWhatsApp"]>>()
|
||||
.fn<
|
||||
(to: string, text: string, opts?: unknown) => Promise<{ messageId: string; toJid: string }>
|
||||
>()
|
||||
.mockResolvedValueOnce({ messageId: "w1", toJid: "jid" })
|
||||
.mockResolvedValueOnce({ messageId: "w2", toJid: "jid" });
|
||||
const cfg: OpenClawConfig = {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import {
|
|||
appendAssistantMessageToSessionTranscript,
|
||||
resolveMirroredTranscriptText,
|
||||
} from "../../config/sessions.js";
|
||||
import type { sendMessageDiscord } from "../../discord/send.js";
|
||||
import { fireAndForgetHook } from "../../hooks/fire-and-forget.js";
|
||||
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
||||
import {
|
||||
|
|
@ -26,15 +25,11 @@ import {
|
|||
toPluginMessageContext,
|
||||
toPluginMessageSentEvent,
|
||||
} from "../../hooks/message-hook-mappers.js";
|
||||
import type { sendMessageIMessage } from "../../imessage/send.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { markdownToSignalTextChunks, type SignalTextStyleRange } from "../../signal/format.js";
|
||||
import { sendMessageSignal } from "../../signal/send.js";
|
||||
import type { sendMessageSlack } from "../../slack/send.js";
|
||||
import type { sendMessageTelegram } from "../../telegram/send.js";
|
||||
import type { sendMessageWhatsApp } from "../../web/outbound.js";
|
||||
import { throwIfAborted } from "./abort.js";
|
||||
import { ackDelivery, enqueueDelivery, failDelivery } from "./delivery-queue.js";
|
||||
import type { OutboundIdentity } from "./identity.js";
|
||||
|
|
@ -51,33 +46,48 @@ export { normalizeOutboundPayloads } from "./payloads.js";
|
|||
const log = createSubsystemLogger("outbound/deliver");
|
||||
const TELEGRAM_TEXT_LIMIT = 4096;
|
||||
|
||||
type SendMatrixMessage = (
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: {
|
||||
cfg?: OpenClawConfig;
|
||||
mediaUrl?: string;
|
||||
replyToId?: string;
|
||||
threadId?: string;
|
||||
timeoutMs?: number;
|
||||
},
|
||||
) => Promise<{ messageId: string; roomId: string }>;
|
||||
|
||||
export type OutboundSendDeps = {
|
||||
sendWhatsApp?: typeof sendMessageWhatsApp;
|
||||
sendTelegram?: typeof sendMessageTelegram;
|
||||
sendDiscord?: typeof sendMessageDiscord;
|
||||
sendSlack?: typeof sendMessageSlack;
|
||||
sendSignal?: typeof sendMessageSignal;
|
||||
sendIMessage?: typeof sendMessageIMessage;
|
||||
sendMatrix?: SendMatrixMessage;
|
||||
sendMSTeams?: (
|
||||
to: string,
|
||||
text: string,
|
||||
opts?: { mediaUrl?: string; mediaLocalRoots?: readonly string[] },
|
||||
) => Promise<{ messageId: string; conversationId: string }>;
|
||||
type LegacyOutboundSendDeps = {
|
||||
sendWhatsApp?: unknown;
|
||||
sendTelegram?: unknown;
|
||||
sendDiscord?: unknown;
|
||||
sendSlack?: unknown;
|
||||
sendSignal?: unknown;
|
||||
sendIMessage?: unknown;
|
||||
sendMatrix?: unknown;
|
||||
sendMSTeams?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamic bag of per-channel send functions, keyed by channel ID.
|
||||
* Each outbound adapter resolves its own function from this record and
|
||||
* falls back to a direct import when the key is absent.
|
||||
*/
|
||||
export type OutboundSendDeps = LegacyOutboundSendDeps & { [channelId: string]: unknown };
|
||||
|
||||
const LEGACY_SEND_DEP_KEYS = {
|
||||
whatsapp: "sendWhatsApp",
|
||||
telegram: "sendTelegram",
|
||||
discord: "sendDiscord",
|
||||
slack: "sendSlack",
|
||||
signal: "sendSignal",
|
||||
imessage: "sendIMessage",
|
||||
matrix: "sendMatrix",
|
||||
msteams: "sendMSTeams",
|
||||
} as const satisfies Record<string, keyof LegacyOutboundSendDeps>;
|
||||
|
||||
export function resolveOutboundSendDep<T>(
|
||||
deps: OutboundSendDeps | null | undefined,
|
||||
channelId: keyof typeof LEGACY_SEND_DEP_KEYS,
|
||||
): T | undefined {
|
||||
const dynamic = deps?.[channelId];
|
||||
if (dynamic !== undefined) {
|
||||
return dynamic as T;
|
||||
}
|
||||
const legacyKey = LEGACY_SEND_DEP_KEYS[channelId];
|
||||
const legacy = deps?.[legacyKey];
|
||||
return legacy as T | undefined;
|
||||
}
|
||||
|
||||
export type OutboundDeliveryResult = {
|
||||
channel: Exclude<OutboundChannel, "none">;
|
||||
messageId: string;
|
||||
|
|
@ -527,7 +537,8 @@ async function deliverOutboundPayloadsCore(
|
|||
const accountId = params.accountId;
|
||||
const deps = params.deps;
|
||||
const abortSignal = params.abortSignal;
|
||||
const sendSignal = params.deps?.sendSignal ?? sendMessageSignal;
|
||||
const sendSignal =
|
||||
resolveOutboundSendDep<typeof sendMessageSignal>(params.deps, "signal") ?? sendMessageSignal;
|
||||
const mediaLocalRoots = getAgentScopedMediaLocalRoots(
|
||||
cfg,
|
||||
params.session?.agentId ?? params.mirror?.agentId,
|
||||
|
|
|
|||
|
|
@ -304,7 +304,9 @@ const emptyRegistry = createTestRegistry([]);
|
|||
const createMSTeamsOutbound = (opts?: { includePoll?: boolean }): ChannelOutboundAdapter => ({
|
||||
deliveryMode: "direct",
|
||||
sendText: async ({ deps, to, text }) => {
|
||||
const send = deps?.sendMSTeams;
|
||||
const send = deps?.sendMSTeams as
|
||||
| ((to: string, text: string, opts?: unknown) => Promise<{ messageId: string }>)
|
||||
| undefined;
|
||||
if (!send) {
|
||||
throw new Error("sendMSTeams missing");
|
||||
}
|
||||
|
|
@ -312,7 +314,9 @@ const createMSTeamsOutbound = (opts?: { includePoll?: boolean }): ChannelOutboun
|
|||
return { channel: "msteams", ...result };
|
||||
},
|
||||
sendMedia: async ({ deps, to, text, mediaUrl }) => {
|
||||
const send = deps?.sendMSTeams;
|
||||
const send = deps?.sendMSTeams as
|
||||
| ((to: string, text: string, opts?: unknown) => Promise<{ messageId: string }>)
|
||||
| undefined;
|
||||
if (!send) {
|
||||
throw new Error("sendMSTeams missing");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,13 @@ async function readRuntimeSourceFiles(
|
|||
if (!absolutePath) {
|
||||
continue;
|
||||
}
|
||||
const source = await fs.readFile(absolutePath, "utf8");
|
||||
let source: string;
|
||||
try {
|
||||
source = await fs.readFile(absolutePath, "utf8");
|
||||
} catch {
|
||||
// File tracked by git but deleted on disk (e.g. pending deletion).
|
||||
continue;
|
||||
}
|
||||
output[index] = {
|
||||
relativePath: path.relative(repoRoot, absolutePath),
|
||||
source,
|
||||
|
|
|
|||
|
|
@ -48,22 +48,7 @@ const [
|
|||
installProcessWarningFilter();
|
||||
|
||||
const pickSendFn = (id: ChannelId, deps?: OutboundSendDeps) => {
|
||||
switch (id) {
|
||||
case "discord":
|
||||
return deps?.sendDiscord;
|
||||
case "slack":
|
||||
return deps?.sendSlack;
|
||||
case "telegram":
|
||||
return deps?.sendTelegram;
|
||||
case "whatsapp":
|
||||
return deps?.sendWhatsApp;
|
||||
case "signal":
|
||||
return deps?.sendSignal;
|
||||
case "imessage":
|
||||
return deps?.sendIMessage;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
return deps?.[id] as ((...args: unknown[]) => Promise<unknown>) | undefined;
|
||||
};
|
||||
|
||||
const createStubOutbound = (
|
||||
|
|
@ -75,7 +60,9 @@ const createStubOutbound = (
|
|||
const send = pickSendFn(id, deps);
|
||||
if (send) {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
const result = await send(to, text, { verbose: false } as any);
|
||||
const result = (await send(to, text, { verbose: false } as any)) as {
|
||||
messageId: string;
|
||||
};
|
||||
return { channel: id, ...result };
|
||||
}
|
||||
return { channel: id, messageId: "test" };
|
||||
|
|
@ -84,7 +71,9 @@ const createStubOutbound = (
|
|||
const send = pickSendFn(id, deps);
|
||||
if (send) {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
const result = await send(to, text, { verbose: false, mediaUrl } as any);
|
||||
const result = (await send(to, text, { verbose: false, mediaUrl } as any)) as {
|
||||
messageId: string;
|
||||
};
|
||||
return { channel: id, ...result };
|
||||
}
|
||||
return { channel: id, messageId: "test" };
|
||||
|
|
|
|||
Loading…
Reference in New Issue