refactor: migrate messaging plugins to sdk
This commit is contained in:
@@ -1,12 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { CHANNEL_IDS } from "../registry.js";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import { listChannelPlugins } from "./index.js";
|
||||
|
||||
describe("channel plugin registry", () => {
|
||||
it("includes the built-in channel ids", () => {
|
||||
const emptyRegistry = createTestRegistry([]);
|
||||
|
||||
const createPlugin = (id: string): ChannelPlugin => ({
|
||||
id,
|
||||
meta: {
|
||||
id,
|
||||
label: id,
|
||||
selectionLabel: id,
|
||||
docsPath: `/channels/${id}`,
|
||||
blurb: "test",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
it("sorts channel plugins by configured order", () => {
|
||||
const registry = createTestRegistry(
|
||||
["slack", "telegram", "signal"].map((id) => ({
|
||||
pluginId: id,
|
||||
plugin: createPlugin(id),
|
||||
source: "test",
|
||||
})),
|
||||
);
|
||||
setActivePluginRegistry(registry);
|
||||
const pluginIds = listChannelPlugins().map((plugin) => plugin.id);
|
||||
for (const id of CHANNEL_IDS) {
|
||||
expect(pluginIds).toContain(id);
|
||||
}
|
||||
expect(pluginIds).toEqual(["telegram", "slack", "signal"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { CHAT_CHANNEL_ORDER, type ChatChannelId, normalizeChatChannelId } from "../registry.js";
|
||||
import { discordPlugin } from "./discord.js";
|
||||
import { imessagePlugin } from "./imessage.js";
|
||||
import { signalPlugin } from "./signal.js";
|
||||
import { slackPlugin } from "./slack.js";
|
||||
import { telegramPlugin } from "./telegram.js";
|
||||
import type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
import { whatsappPlugin } from "./whatsapp.js";
|
||||
import { getActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
|
||||
// Channel plugins registry (runtime).
|
||||
@@ -14,14 +8,7 @@ import { getActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
// Shared code paths (reply flow, command auth, sandbox explain) should depend on `src/channels/dock.ts`
|
||||
// instead, and only call `getChannelPlugin()` at execution boundaries.
|
||||
//
|
||||
// Adding a channel:
|
||||
// - add `<id>Plugin` import + entry in `resolveChannels()`
|
||||
// - add an entry to `src/channels/dock.ts` for shared behavior (capabilities, allowFrom, threading, …)
|
||||
// - add ids/aliases in `src/channels/registry.ts`
|
||||
function resolveCoreChannels(): ChannelPlugin[] {
|
||||
return [telegramPlugin, whatsappPlugin, discordPlugin, slackPlugin, signalPlugin, imessagePlugin];
|
||||
}
|
||||
|
||||
// Channel plugins are registered by the plugin loader (extensions/ or configured paths).
|
||||
function listPluginChannels(): ChannelPlugin[] {
|
||||
const registry = getActivePluginRegistry();
|
||||
if (!registry) return [];
|
||||
@@ -41,7 +28,7 @@ function dedupeChannels(channels: ChannelPlugin[]): ChannelPlugin[] {
|
||||
}
|
||||
|
||||
export function listChannelPlugins(): ChannelPlugin[] {
|
||||
const combined = dedupeChannels([...resolveCoreChannels(), ...listPluginChannels()]);
|
||||
const combined = dedupeChannels(listPluginChannels());
|
||||
return combined.sort((a, b) => {
|
||||
const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id as ChatChannelId);
|
||||
const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id as ChatChannelId);
|
||||
@@ -72,8 +59,6 @@ export function normalizeChannelId(raw?: string | null): ChannelId | null {
|
||||
});
|
||||
return plugin?.id ?? null;
|
||||
}
|
||||
|
||||
export { discordPlugin, imessagePlugin, signalPlugin, slackPlugin, telegramPlugin, whatsappPlugin };
|
||||
export {
|
||||
listDiscordDirectoryGroupsFromConfig,
|
||||
listDiscordDirectoryPeersFromConfig,
|
||||
|
||||
@@ -1,36 +1,25 @@
|
||||
import type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
import type { ChatChannelId } from "../registry.js";
|
||||
import type { PluginRegistry } from "../../plugins/registry.js";
|
||||
import { getActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
|
||||
type PluginLoader = () => Promise<ChannelPlugin>;
|
||||
|
||||
// Channel docking: load *one* plugin on-demand.
|
||||
//
|
||||
// This avoids importing `src/channels/plugins/index.ts` (intentionally heavy)
|
||||
// from shared flows like outbound delivery / followup routing.
|
||||
const LOADERS: Record<ChatChannelId, PluginLoader> = {
|
||||
telegram: async () => (await import("./telegram.js")).telegramPlugin,
|
||||
whatsapp: async () => (await import("./whatsapp.js")).whatsappPlugin,
|
||||
discord: async () => (await import("./discord.js")).discordPlugin,
|
||||
slack: async () => (await import("./slack.js")).slackPlugin,
|
||||
signal: async () => (await import("./signal.js")).signalPlugin,
|
||||
imessage: async () => (await import("./imessage.js")).imessagePlugin,
|
||||
};
|
||||
|
||||
const cache = new Map<ChannelId, ChannelPlugin>();
|
||||
let lastRegistry: PluginRegistry | null = null;
|
||||
|
||||
function ensureCacheForRegistry(registry: PluginRegistry | null) {
|
||||
if (registry === lastRegistry) return;
|
||||
cache.clear();
|
||||
lastRegistry = registry;
|
||||
}
|
||||
|
||||
export async function loadChannelPlugin(id: ChannelId): Promise<ChannelPlugin | undefined> {
|
||||
const registry = getActivePluginRegistry();
|
||||
ensureCacheForRegistry(registry);
|
||||
const cached = cache.get(id);
|
||||
if (cached) return cached;
|
||||
const registry = getActivePluginRegistry();
|
||||
const pluginEntry = registry?.channels.find((entry) => entry.plugin.id === id);
|
||||
if (pluginEntry) {
|
||||
cache.set(id, pluginEntry.plugin);
|
||||
return pluginEntry.plugin;
|
||||
}
|
||||
const loader = LOADERS[id as ChatChannelId];
|
||||
if (!loader) return undefined;
|
||||
const plugin = await loader();
|
||||
cache.set(id, plugin);
|
||||
return plugin;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,40 +1,33 @@
|
||||
import type { ChannelId, ChannelOutboundAdapter } from "../types.js";
|
||||
import type { ChatChannelId } from "../../registry.js";
|
||||
import type { PluginRegistry } from "../../../plugins/registry.js";
|
||||
import { getActivePluginRegistry } from "../../../plugins/runtime.js";
|
||||
|
||||
type OutboundLoader = () => Promise<ChannelOutboundAdapter>;
|
||||
|
||||
// Channel docking: outbound sends should stay cheap to import.
|
||||
//
|
||||
// The full channel plugins (src/channels/plugins/*.ts) pull in status,
|
||||
// onboarding, gateway monitors, etc. Outbound delivery only needs chunking +
|
||||
// send primitives, so we keep a dedicated, lightweight loader here.
|
||||
const LOADERS: Record<ChatChannelId, OutboundLoader> = {
|
||||
telegram: async () => (await import("./telegram.js")).telegramOutbound,
|
||||
whatsapp: async () => (await import("./whatsapp.js")).whatsappOutbound,
|
||||
discord: async () => (await import("./discord.js")).discordOutbound,
|
||||
slack: async () => (await import("./slack.js")).slackOutbound,
|
||||
signal: async () => (await import("./signal.js")).signalOutbound,
|
||||
imessage: async () => (await import("./imessage.js")).imessageOutbound,
|
||||
};
|
||||
|
||||
const cache = new Map<ChannelId, ChannelOutboundAdapter>();
|
||||
let lastRegistry: PluginRegistry | null = null;
|
||||
|
||||
function ensureCacheForRegistry(registry: PluginRegistry | null) {
|
||||
if (registry === lastRegistry) return;
|
||||
cache.clear();
|
||||
lastRegistry = registry;
|
||||
}
|
||||
|
||||
export async function loadChannelOutboundAdapter(
|
||||
id: ChannelId,
|
||||
): Promise<ChannelOutboundAdapter | undefined> {
|
||||
const registry = getActivePluginRegistry();
|
||||
ensureCacheForRegistry(registry);
|
||||
const cached = cache.get(id);
|
||||
if (cached) return cached;
|
||||
const registry = getActivePluginRegistry();
|
||||
const pluginEntry = registry?.channels.find((entry) => entry.plugin.id === id);
|
||||
const outbound = pluginEntry?.plugin.outbound;
|
||||
if (outbound) {
|
||||
cache.set(id, outbound);
|
||||
return outbound;
|
||||
}
|
||||
const loader = LOADERS[id as ChatChannelId];
|
||||
if (!loader) return undefined;
|
||||
const loaded = await loader();
|
||||
cache.set(id, loaded);
|
||||
return loaded;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { chunkText } from "../../../auto-reply/chunk.js";
|
||||
import { shouldLogVerbose } from "../../../globals.js";
|
||||
import { sendMessageWhatsApp, sendPollWhatsApp } from "../../../web/outbound.js";
|
||||
import { sendPollWhatsApp } from "../../../web/outbound.js";
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../../whatsapp/normalize.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
import { missingTargetError } from "../../../infra/outbound/target-errors.js";
|
||||
@@ -57,7 +57,8 @@ export const whatsappOutbound: ChannelOutboundAdapter = {
|
||||
};
|
||||
},
|
||||
sendText: async ({ to, text, accountId, deps, gifPlayback }) => {
|
||||
const send = deps?.sendWhatsApp ?? sendMessageWhatsApp;
|
||||
const send =
|
||||
deps?.sendWhatsApp ?? (await import("../../../web/outbound.js")).sendMessageWhatsApp;
|
||||
const result = await send(to, text, {
|
||||
verbose: false,
|
||||
accountId: accountId ?? undefined,
|
||||
@@ -66,7 +67,8 @@ export const whatsappOutbound: ChannelOutboundAdapter = {
|
||||
return { channel: "whatsapp", ...result };
|
||||
},
|
||||
sendMedia: async ({ to, text, mediaUrl, accountId, deps, gifPlayback }) => {
|
||||
const send = deps?.sendWhatsApp ?? sendMessageWhatsApp;
|
||||
const send =
|
||||
deps?.sendWhatsApp ?? (await import("../../../web/outbound.js")).sendMessageWhatsApp;
|
||||
const result = await send(to, text, {
|
||||
verbose: false,
|
||||
mediaUrl,
|
||||
|
||||
Reference in New Issue
Block a user