Skip to content

Commit 7764f71

Browse files
authored
refactor: make OutboundSendDeps dynamic with channel-ID keys (openclaw#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
1 parent 0c926a2 commit 7764f71

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+399
-278
lines changed

extensions/discord/src/channel.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@ import {
3737
type ChannelPlugin,
3838
type ResolvedDiscordAccount,
3939
} from "openclaw/plugin-sdk/discord";
40+
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
4041
import { getDiscordRuntime } from "./runtime.js";
4142

43+
type DiscordSendFn = ReturnType<
44+
typeof getDiscordRuntime
45+
>["channel"]["discord"]["sendMessageDiscord"];
46+
4247
const meta = getChatChannelMeta("discord");
4348

4449
const discordMessageActions: ChannelMessageActionAdapter = {
@@ -300,7 +305,9 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
300305
pollMaxOptions: 10,
301306
resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to),
302307
sendText: async ({ cfg, to, text, accountId, deps, replyToId, silent }) => {
303-
const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
308+
const send =
309+
resolveOutboundSendDep<DiscordSendFn>(deps, "discord") ??
310+
getDiscordRuntime().channel.discord.sendMessageDiscord;
304311
const result = await send(to, text, {
305312
verbose: false,
306313
cfg,
@@ -321,7 +328,9 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
321328
replyToId,
322329
silent,
323330
}) => {
324-
const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
331+
const send =
332+
resolveOutboundSendDep<DiscordSendFn>(deps, "discord") ??
333+
getDiscordRuntime().channel.discord.sendMessageDiscord;
325334
const result = await send(to, text, {
326335
verbose: false,
327336
cfg,

extensions/imessage/src/channel.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
type ChannelPlugin,
3030
type ResolvedIMessageAccount,
3131
} from "openclaw/plugin-sdk/imessage";
32+
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
3233
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
3334
import { getIMessageRuntime } from "./runtime.js";
3435

@@ -59,11 +60,12 @@ async function sendIMessageOutbound(params: {
5960
mediaUrl?: string;
6061
mediaLocalRoots?: readonly string[];
6162
accountId?: string;
62-
deps?: { sendIMessage?: IMessageSendFn };
63+
deps?: { [channelId: string]: unknown };
6364
replyToId?: string;
6465
}) {
6566
const send =
66-
params.deps?.sendIMessage ?? getIMessageRuntime().channel.imessage.sendMessageIMessage;
67+
resolveOutboundSendDep<IMessageSendFn>(params.deps, "imessage") ??
68+
getIMessageRuntime().channel.imessage.sendMessageIMessage;
6769
const maxBytes = resolveChannelMediaMaxBytes({
6870
cfg: params.cfg,
6971
resolveChannelLimitMb: ({ cfg, accountId }) =>

extensions/matrix/src/outbound.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,15 @@ describe("matrixOutbound cfg threading", () => {
8888
);
8989
});
9090

91-
it("passes resolved cfg through injected deps.sendMatrix", async () => {
91+
it("passes resolved cfg through injected deps.matrix", async () => {
9292
const cfg = {
9393
channels: {
9494
matrix: {
9595
accessToken: "resolved-token",
9696
},
9797
},
9898
} as OpenClawConfig;
99-
const sendMatrix = vi.fn(async () => ({
99+
const matrix = vi.fn(async () => ({
100100
messageId: "evt-injected",
101101
roomId: "!room:example",
102102
}));
@@ -105,13 +105,13 @@ describe("matrixOutbound cfg threading", () => {
105105
cfg,
106106
to: "room:!room:example",
107107
text: "hello via deps",
108-
deps: { sendMatrix },
108+
deps: { matrix },
109109
accountId: "default",
110110
threadId: "$thread",
111111
replyToId: "$reply",
112112
});
113113

114-
expect(sendMatrix).toHaveBeenCalledWith(
114+
expect(matrix).toHaveBeenCalledWith(
115115
"room:!room:example",
116116
"hello via deps",
117117
expect.objectContaining({

extensions/matrix/src/outbound.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/matrix";
2+
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
23
import { sendMessageMatrix, sendPollMatrix } from "./matrix/send.js";
34
import { getMatrixRuntime } from "./runtime.js";
45

@@ -8,7 +9,8 @@ export const matrixOutbound: ChannelOutboundAdapter = {
89
chunkerMode: "markdown",
910
textChunkLimit: 4000,
1011
sendText: async ({ cfg, to, text, deps, replyToId, threadId, accountId }) => {
11-
const send = deps?.sendMatrix ?? sendMessageMatrix;
12+
const send =
13+
resolveOutboundSendDep<typeof sendMessageMatrix>(deps, "matrix") ?? sendMessageMatrix;
1214
const resolvedThreadId =
1315
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
1416
const result = await send(to, text, {
@@ -24,7 +26,8 @@ export const matrixOutbound: ChannelOutboundAdapter = {
2426
};
2527
},
2628
sendMedia: async ({ cfg, to, text, mediaUrl, deps, replyToId, threadId, accountId }) => {
27-
const send = deps?.sendMatrix ?? sendMessageMatrix;
29+
const send =
30+
resolveOutboundSendDep<typeof sendMessageMatrix>(deps, "matrix") ?? sendMessageMatrix;
2831
const resolvedThreadId =
2932
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
3033
const result = await send(to, text, {

extensions/msteams/src/outbound.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/msteams";
2+
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
23
import { createMSTeamsPollStoreFs } from "./polls.js";
34
import { getMSTeamsRuntime } from "./runtime.js";
45
import { sendMessageMSTeams, sendPollMSTeams } from "./send.js";
@@ -10,13 +11,24 @@ export const msteamsOutbound: ChannelOutboundAdapter = {
1011
textChunkLimit: 4000,
1112
pollMaxOptions: 12,
1213
sendText: async ({ cfg, to, text, deps }) => {
13-
const send = deps?.sendMSTeams ?? ((to, text) => sendMessageMSTeams({ cfg, to, text }));
14+
type SendFn = (
15+
to: string,
16+
text: string,
17+
) => Promise<{ messageId: string; conversationId: string }>;
18+
const send =
19+
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
20+
((to, text) => sendMessageMSTeams({ cfg, to, text }));
1421
const result = await send(to, text);
1522
return { channel: "msteams", ...result };
1623
},
1724
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, deps }) => {
25+
type SendFn = (
26+
to: string,
27+
text: string,
28+
opts?: { mediaUrl?: string; mediaLocalRoots?: readonly string[] },
29+
) => Promise<{ messageId: string; conversationId: string }>;
1830
const send =
19-
deps?.sendMSTeams ??
31+
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
2032
((to, text, opts) =>
2133
sendMessageMSTeams({
2234
cfg,

extensions/signal/src/channel.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
type ChannelPlugin,
3131
type ResolvedSignalAccount,
3232
} from "openclaw/plugin-sdk/signal";
33+
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
3334
import { getSignalRuntime } from "./runtime.js";
3435

3536
const signalMessageActions: ChannelMessageActionAdapter = {
@@ -84,9 +85,11 @@ async function sendSignalOutbound(params: {
8485
mediaUrl?: string;
8586
mediaLocalRoots?: readonly string[];
8687
accountId?: string;
87-
deps?: { sendSignal?: SignalSendFn };
88+
deps?: { [channelId: string]: unknown };
8889
}) {
89-
const send = params.deps?.sendSignal ?? getSignalRuntime().channel.signal.sendMessageSignal;
90+
const send =
91+
resolveOutboundSendDep<SignalSendFn>(params.deps, "signal") ??
92+
getSignalRuntime().channel.signal.sendMessageSignal;
9093
const maxBytes = resolveChannelMediaMaxBytes({
9194
cfg: params.cfg,
9295
resolveChannelLimitMb: ({ cfg, accountId }) =>

extensions/slack/src/channel.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
type ChannelPlugin,
3939
type ResolvedSlackAccount,
4040
} from "openclaw/plugin-sdk/slack";
41+
import { resolveOutboundSendDep } from "../../../src/infra/outbound/deliver.js";
4142
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
4243
import { getSlackRuntime } from "./runtime.js";
4344

@@ -77,11 +78,13 @@ type SlackSendFn = ReturnType<typeof getSlackRuntime>["channel"]["slack"]["sendM
7778
function resolveSlackSendContext(params: {
7879
cfg: Parameters<typeof resolveSlackAccount>[0]["cfg"];
7980
accountId?: string;
80-
deps?: { sendSlack?: SlackSendFn };
81+
deps?: { [channelId: string]: unknown };
8182
replyToId?: string | number | null;
8283
threadId?: string | number | null;
8384
}) {
84-
const send = params.deps?.sendSlack ?? getSlackRuntime().channel.slack.sendMessageSlack;
85+
const send =
86+
resolveOutboundSendDep<SlackSendFn>(params.deps, "slack") ??
87+
getSlackRuntime().channel.slack.sendMessageSlack;
8588
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
8689
const token = getTokenForOperation(account, "write");
8790
const botToken = account.botToken?.trim();

extensions/telegram/src/channel.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,16 @@ import {
4040
type ResolvedTelegramAccount,
4141
type TelegramProbe,
4242
} from "openclaw/plugin-sdk/telegram";
43+
import {
44+
type OutboundSendDeps,
45+
resolveOutboundSendDep,
46+
} from "../../../src/infra/outbound/deliver.js";
4347
import { getTelegramRuntime } from "./runtime.js";
4448

49+
type TelegramSendFn = ReturnType<
50+
typeof getTelegramRuntime
51+
>["channel"]["telegram"]["sendMessageTelegram"];
52+
4553
const meta = getChatChannelMeta("telegram");
4654

4755
function findTelegramTokenOwnerAccountId(params: {
@@ -78,9 +86,6 @@ function formatDuplicateTelegramTokenReason(params: {
7886
);
7987
}
8088

81-
type TelegramSendFn = ReturnType<
82-
typeof getTelegramRuntime
83-
>["channel"]["telegram"]["sendMessageTelegram"];
8489
type TelegramSendOptions = NonNullable<Parameters<TelegramSendFn>[2]>;
8590

8691
function buildTelegramSendOptions(params: {
@@ -111,13 +116,14 @@ async function sendTelegramOutbound(params: {
111116
mediaUrl?: string | null;
112117
mediaLocalRoots?: readonly string[] | null;
113118
accountId?: string | null;
114-
deps?: { sendTelegram?: TelegramSendFn };
119+
deps?: OutboundSendDeps;
115120
replyToId?: string | null;
116121
threadId?: string | number | null;
117122
silent?: boolean | null;
118123
}) {
119124
const send =
120-
params.deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram;
125+
resolveOutboundSendDep<TelegramSendFn>(params.deps, "telegram") ??
126+
getTelegramRuntime().channel.telegram.sendMessageTelegram;
121127
return await send(
122128
params.to,
123129
params.text,
@@ -381,7 +387,9 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
381387
threadId,
382388
silent,
383389
}) => {
384-
const send = deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram;
390+
const send =
391+
resolveOutboundSendDep<TelegramSendFn>(deps, "telegram") ??
392+
getTelegramRuntime().channel.telegram.sendMessageTelegram;
385393
const result = await sendTelegramPayloadMessages({
386394
send,
387395
to,

src/channels/plugins/outbound/discord.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
sendPollDiscord,
99
sendWebhookMessageDiscord,
1010
} from "../../../discord/send.js";
11+
import { resolveOutboundSendDep } from "../../../infra/outbound/deliver.js";
1112
import type { OutboundIdentity } from "../../../infra/outbound/identity.js";
1213
import { normalizeDiscordOutboundTarget } from "../normalize/discord.js";
1314
import type { ChannelOutboundAdapter } from "../types.js";
@@ -100,7 +101,8 @@ export const discordOutbound: ChannelOutboundAdapter = {
100101
return { channel: "discord", ...webhookResult };
101102
}
102103
}
103-
const send = deps?.sendDiscord ?? sendMessageDiscord;
104+
const send =
105+
resolveOutboundSendDep<typeof sendMessageDiscord>(deps, "discord") ?? sendMessageDiscord;
104106
const target = resolveDiscordOutboundTarget({ to, threadId });
105107
const result = await send(target, text, {
106108
verbose: false,
@@ -123,7 +125,8 @@ export const discordOutbound: ChannelOutboundAdapter = {
123125
threadId,
124126
silent,
125127
}) => {
126-
const send = deps?.sendDiscord ?? sendMessageDiscord;
128+
const send =
129+
resolveOutboundSendDep<typeof sendMessageDiscord>(deps, "discord") ?? sendMessageDiscord;
127130
const target = resolveDiscordOutboundTarget({ to, threadId });
128131
const result = await send(target, text, {
129132
verbose: false,

src/channels/plugins/outbound/imessage.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe("imessageOutbound", () => {
2222
text: "hello",
2323
accountId: "default",
2424
replyToId: "msg-123",
25-
deps: { sendIMessage },
25+
deps: { imessage: sendIMessage },
2626
});
2727

2828
expect(sendIMessage).toHaveBeenCalledWith(
@@ -50,7 +50,7 @@ describe("imessageOutbound", () => {
5050
mediaLocalRoots: ["/tmp"],
5151
accountId: "acct-1",
5252
replyToId: "msg-456",
53-
deps: { sendIMessage },
53+
deps: { imessage: sendIMessage },
5454
});
5555

5656
expect(sendIMessage).toHaveBeenCalledWith(

0 commit comments

Comments
 (0)