refactor: tighten plugin sdk channel seams

This commit is contained in:
Peter Steinberger
2026-03-16 01:05:18 -07:00
parent 7a09255361
commit f11589b311
72 changed files with 319 additions and 125 deletions

View File

@@ -1,2 +1,2 @@
// Shim: re-exports from extension
// Public entrypoint for the Discord channel action adapter.
export * from "../../../../extensions/discord/src/channel-actions.js";

View File

@@ -1 +1,2 @@
// Public entrypoint for the Telegram channel action adapter.
export * from "../../../../extensions/telegram/src/channel-actions.js";

View File

@@ -0,0 +1,88 @@
import { bluebubblesPlugin } from "../../../extensions/bluebubbles/src/channel.js";
import { discordPlugin } from "../../../extensions/discord/src/channel.js";
import { discordSetupPlugin } from "../../../extensions/discord/src/channel.setup.js";
import { setDiscordRuntime } from "../../../extensions/discord/src/runtime.js";
import { feishuPlugin } from "../../../extensions/feishu/src/channel.js";
import { googlechatPlugin } from "../../../extensions/googlechat/src/channel.js";
import { imessagePlugin } from "../../../extensions/imessage/src/channel.js";
import { imessageSetupPlugin } from "../../../extensions/imessage/src/channel.setup.js";
import { ircPlugin } from "../../../extensions/irc/src/channel.js";
import { linePlugin } from "../../../extensions/line/src/channel.js";
import { lineSetupPlugin } from "../../../extensions/line/src/channel.setup.js";
import { setLineRuntime } from "../../../extensions/line/src/runtime.js";
import { matrixPlugin } from "../../../extensions/matrix/src/channel.js";
import { mattermostPlugin } from "../../../extensions/mattermost/src/channel.js";
import { msteamsPlugin } from "../../../extensions/msteams/src/channel.js";
import { nextcloudTalkPlugin } from "../../../extensions/nextcloud-talk/src/channel.js";
import { nostrPlugin } from "../../../extensions/nostr/src/channel.js";
import { signalPlugin } from "../../../extensions/signal/src/channel.js";
import { signalSetupPlugin } from "../../../extensions/signal/src/channel.setup.js";
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
import { slackSetupPlugin } from "../../../extensions/slack/src/channel.setup.js";
import { synologyChatPlugin } from "../../../extensions/synology-chat/src/channel.js";
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
import { telegramSetupPlugin } from "../../../extensions/telegram/src/channel.setup.js";
import { setTelegramRuntime } from "../../../extensions/telegram/src/runtime.js";
import { tlonPlugin } from "../../../extensions/tlon/src/channel.js";
import { whatsappPlugin } from "../../../extensions/whatsapp/src/channel.js";
import { whatsappSetupPlugin } from "../../../extensions/whatsapp/src/channel.setup.js";
import { zaloPlugin } from "../../../extensions/zalo/src/channel.js";
import { zalouserPlugin } from "../../../extensions/zalouser/src/channel.js";
import type { ChannelId, ChannelPlugin } from "./types.js";
export const bundledChannelPlugins = [
bluebubblesPlugin,
discordPlugin,
feishuPlugin,
googlechatPlugin,
imessagePlugin,
ircPlugin,
linePlugin,
matrixPlugin,
mattermostPlugin,
msteamsPlugin,
nextcloudTalkPlugin,
nostrPlugin,
signalPlugin,
slackPlugin,
synologyChatPlugin,
telegramPlugin,
tlonPlugin,
whatsappPlugin,
zaloPlugin,
zalouserPlugin,
] as ChannelPlugin[];
export const bundledChannelSetupPlugins = [
telegramSetupPlugin,
whatsappSetupPlugin,
discordSetupPlugin,
ircPlugin,
googlechatPlugin,
slackSetupPlugin,
signalSetupPlugin,
imessageSetupPlugin,
lineSetupPlugin,
] as ChannelPlugin[];
const bundledChannelPluginsById = new Map(
bundledChannelPlugins.map((plugin) => [plugin.id, plugin] as const),
);
export function getBundledChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
return bundledChannelPluginsById.get(id);
}
export function requireBundledChannelPlugin(id: ChannelId): ChannelPlugin {
const plugin = getBundledChannelPlugin(id);
if (!plugin) {
throw new Error(`missing bundled channel plugin: ${id}`);
}
return plugin;
}
export const bundledChannelRuntimeSetters = {
setDiscordRuntime,
setLineRuntime,
setTelegramRuntime,
};

View File

@@ -1,33 +1,11 @@
import { expect, vi } from "vitest";
import { bluebubblesPlugin } from "../../../../extensions/bluebubbles/src/channel.js";
import { discordPlugin } from "../../../../extensions/discord/src/channel.js";
import { setDiscordRuntime } from "../../../../extensions/discord/src/runtime.js";
import { feishuPlugin } from "../../../../extensions/feishu/src/channel.js";
import { googlechatPlugin } from "../../../../extensions/googlechat/src/channel.js";
import { imessagePlugin } from "../../../../extensions/imessage/src/channel.js";
import { ircPlugin } from "../../../../extensions/irc/src/channel.js";
import { linePlugin } from "../../../../extensions/line/src/channel.js";
import { setLineRuntime } from "../../../../extensions/line/src/runtime.js";
import { matrixPlugin } from "../../../../extensions/matrix/src/channel.js";
import { mattermostPlugin } from "../../../../extensions/mattermost/src/channel.js";
import { msteamsPlugin } from "../../../../extensions/msteams/src/channel.js";
import { nextcloudTalkPlugin } from "../../../../extensions/nextcloud-talk/src/channel.js";
import { nostrPlugin } from "../../../../extensions/nostr/src/channel.js";
import { signalPlugin } from "../../../../extensions/signal/src/channel.js";
import { slackPlugin } from "../../../../extensions/slack/src/channel.js";
import { synologyChatPlugin } from "../../../../extensions/synology-chat/src/channel.js";
import { telegramPlugin } from "../../../../extensions/telegram/src/channel.js";
import { setTelegramRuntime } from "../../../../extensions/telegram/src/runtime.js";
import { tlonPlugin } from "../../../../extensions/tlon/src/channel.js";
import { whatsappPlugin } from "../../../../extensions/whatsapp/src/channel.js";
import { zaloPlugin } from "../../../../extensions/zalo/src/channel.js";
import { zalouserPlugin } from "../../../../extensions/zalouser/src/channel.js";
import type { OpenClawConfig } from "../../../config/config.js";
import {
resolveDefaultLineAccountId,
resolveLineAccount,
listLineAccountIds,
} from "../../../line/accounts.js";
import { bundledChannelRuntimeSetters, requireBundledChannelPlugin } from "../bundled.js";
import type { ChannelPlugin } from "../types.js";
type PluginContractEntry = {
@@ -84,7 +62,7 @@ const telegramGetCapabilitiesMock = vi.fn();
const discordListActionsMock = vi.fn();
const discordGetCapabilitiesMock = vi.fn();
setTelegramRuntime({
bundledChannelRuntimeSetters.setTelegramRuntime({
channel: {
telegram: {
messageActions: {
@@ -95,7 +73,7 @@ setTelegramRuntime({
},
} as never);
setDiscordRuntime({
bundledChannelRuntimeSetters.setDiscordRuntime({
channel: {
discord: {
messageActions: {
@@ -106,7 +84,7 @@ setDiscordRuntime({
},
} as never);
setLineRuntime({
bundledChannelRuntimeSetters.setLineRuntime({
channel: {
line: {
listLineAccountIds,
@@ -118,32 +96,32 @@ setLineRuntime({
} as never);
export const pluginContractRegistry: PluginContractEntry[] = [
{ id: "bluebubbles", plugin: bluebubblesPlugin },
{ id: "discord", plugin: discordPlugin },
{ id: "feishu", plugin: feishuPlugin },
{ id: "googlechat", plugin: googlechatPlugin },
{ id: "imessage", plugin: imessagePlugin },
{ id: "irc", plugin: ircPlugin },
{ id: "line", plugin: linePlugin },
{ id: "matrix", plugin: matrixPlugin },
{ id: "mattermost", plugin: mattermostPlugin },
{ id: "msteams", plugin: msteamsPlugin },
{ id: "nextcloud-talk", plugin: nextcloudTalkPlugin },
{ id: "nostr", plugin: nostrPlugin },
{ id: "signal", plugin: signalPlugin },
{ id: "slack", plugin: slackPlugin },
{ id: "synology-chat", plugin: synologyChatPlugin },
{ id: "telegram", plugin: telegramPlugin },
{ id: "tlon", plugin: tlonPlugin },
{ id: "whatsapp", plugin: whatsappPlugin },
{ id: "zalo", plugin: zaloPlugin },
{ id: "zalouser", plugin: zalouserPlugin },
{ id: "bluebubbles", plugin: requireBundledChannelPlugin("bluebubbles") },
{ id: "discord", plugin: requireBundledChannelPlugin("discord") },
{ id: "feishu", plugin: requireBundledChannelPlugin("feishu") },
{ id: "googlechat", plugin: requireBundledChannelPlugin("googlechat") },
{ id: "imessage", plugin: requireBundledChannelPlugin("imessage") },
{ id: "irc", plugin: requireBundledChannelPlugin("irc") },
{ id: "line", plugin: requireBundledChannelPlugin("line") },
{ id: "matrix", plugin: requireBundledChannelPlugin("matrix") },
{ id: "mattermost", plugin: requireBundledChannelPlugin("mattermost") },
{ id: "msteams", plugin: requireBundledChannelPlugin("msteams") },
{ id: "nextcloud-talk", plugin: requireBundledChannelPlugin("nextcloud-talk") },
{ id: "nostr", plugin: requireBundledChannelPlugin("nostr") },
{ id: "signal", plugin: requireBundledChannelPlugin("signal") },
{ id: "slack", plugin: requireBundledChannelPlugin("slack") },
{ id: "synology-chat", plugin: requireBundledChannelPlugin("synology-chat") },
{ id: "telegram", plugin: requireBundledChannelPlugin("telegram") },
{ id: "tlon", plugin: requireBundledChannelPlugin("tlon") },
{ id: "whatsapp", plugin: requireBundledChannelPlugin("whatsapp") },
{ id: "zalo", plugin: requireBundledChannelPlugin("zalo") },
{ id: "zalouser", plugin: requireBundledChannelPlugin("zalouser") },
];
export const actionContractRegistry: ActionsContractEntry[] = [
{
id: "slack",
plugin: slackPlugin,
plugin: requireBundledChannelPlugin("slack"),
unsupportedAction: "poll",
cases: [
{
@@ -217,7 +195,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
},
{
id: "mattermost",
plugin: mattermostPlugin,
plugin: requireBundledChannelPlugin("mattermost"),
unsupportedAction: "poll",
cases: [
{
@@ -265,7 +243,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
},
{
id: "telegram",
plugin: telegramPlugin,
plugin: requireBundledChannelPlugin("telegram"),
cases: [
{
name: "forwards runtime-backed Telegram actions and capabilities",
@@ -283,7 +261,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
},
{
id: "discord",
plugin: discordPlugin,
plugin: requireBundledChannelPlugin("discord"),
cases: [
{
name: "forwards runtime-backed Discord actions and capabilities",
@@ -304,7 +282,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
export const setupContractRegistry: SetupContractEntry[] = [
{
id: "slack",
plugin: slackPlugin,
plugin: requireBundledChannelPlugin("slack"),
cases: [
{
name: "default account stores tokens and enables the channel",
@@ -334,7 +312,7 @@ export const setupContractRegistry: SetupContractEntry[] = [
},
{
id: "mattermost",
plugin: mattermostPlugin,
plugin: requireBundledChannelPlugin("mattermost"),
cases: [
{
name: "default account stores token and normalized base URL",
@@ -363,7 +341,7 @@ export const setupContractRegistry: SetupContractEntry[] = [
},
{
id: "line",
plugin: linePlugin,
plugin: requireBundledChannelPlugin("line"),
cases: [
{
name: "default account stores token and secret",
@@ -396,7 +374,7 @@ export const setupContractRegistry: SetupContractEntry[] = [
export const statusContractRegistry: StatusContractEntry[] = [
{
id: "slack",
plugin: slackPlugin,
plugin: requireBundledChannelPlugin("slack"),
cases: [
{
name: "configured account produces a configured status snapshot",
@@ -424,7 +402,7 @@ export const statusContractRegistry: StatusContractEntry[] = [
},
{
id: "mattermost",
plugin: mattermostPlugin,
plugin: requireBundledChannelPlugin("mattermost"),
cases: [
{
name: "configured account preserves connectivity details in the snapshot",
@@ -455,7 +433,7 @@ export const statusContractRegistry: StatusContractEntry[] = [
},
{
id: "line",
plugin: linePlugin,
plugin: requireBundledChannelPlugin("line"),
cases: [
{
name: "configured account produces a webhook status snapshot",

View File

@@ -1,2 +0,0 @@
// Shim: re-exports from extension
export * from "../../../../extensions/discord/src/normalize.js";

View File

@@ -1 +0,0 @@
export * from "../../../../extensions/telegram/src/normalize.js";

View File

@@ -1,2 +1,25 @@
// Shim: re-exports from extensions/whatsapp/src/normalize.ts
export * from "../../../../extensions/whatsapp/src/normalize.js";
import { normalizeWhatsAppTarget } from "../../../whatsapp/normalize.js";
import { looksLikeHandleOrPhoneTarget, trimMessagingTarget } from "./shared.js";
export function normalizeWhatsAppMessagingTarget(raw: string): string | undefined {
const trimmed = trimMessagingTarget(raw);
if (!trimmed) {
return undefined;
}
return normalizeWhatsAppTarget(trimmed) ?? undefined;
}
export function normalizeWhatsAppAllowFromEntries(allowFrom: Array<string | number>): string[] {
return allowFrom
.map((entry) => String(entry).trim())
.filter((entry): entry is string => Boolean(entry))
.map((entry) => (entry === "*" ? entry : normalizeWhatsAppTarget(entry)))
.filter((entry): entry is string => Boolean(entry));
}
export function looksLikeWhatsAppTargetId(raw: string): boolean {
return looksLikeHandleOrPhoneTarget({
raw,
prefixPattern: /^whatsapp:/i,
});
}

View File

@@ -1,17 +1,9 @@
import { discordSetupPlugin } from "../../../extensions/discord/src/channel.setup.js";
import { googlechatPlugin } from "../../../extensions/googlechat/src/channel.js";
import { imessageSetupPlugin } from "../../../extensions/imessage/src/channel.setup.js";
import { ircPlugin } from "../../../extensions/irc/src/channel.js";
import { lineSetupPlugin } from "../../../extensions/line/src/channel.setup.js";
import { signalSetupPlugin } from "../../../extensions/signal/src/channel.setup.js";
import { slackSetupPlugin } from "../../../extensions/slack/src/channel.setup.js";
import { telegramSetupPlugin } from "../../../extensions/telegram/src/channel.setup.js";
import { whatsappSetupPlugin } from "../../../extensions/whatsapp/src/channel.setup.js";
import {
getActivePluginRegistryVersion,
requireActivePluginRegistry,
} from "../../plugins/runtime.js";
import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../registry.js";
import { bundledChannelSetupPlugins } from "./bundled.js";
import type { ChannelId, ChannelPlugin } from "./types.js";
type CachedChannelSetupPlugins = {
@@ -28,18 +20,6 @@ const EMPTY_CHANNEL_SETUP_CACHE: CachedChannelSetupPlugins = {
let cachedChannelSetupPlugins = EMPTY_CHANNEL_SETUP_CACHE;
const BUNDLED_CHANNEL_SETUP_PLUGINS = [
telegramSetupPlugin,
whatsappSetupPlugin,
discordSetupPlugin,
ircPlugin,
googlechatPlugin,
slackSetupPlugin,
signalSetupPlugin,
imessageSetupPlugin,
lineSetupPlugin,
] as ChannelPlugin[];
function dedupeSetupPlugins(plugins: ChannelPlugin[]): ChannelPlugin[] {
const seen = new Set<string>();
const resolved: ChannelPlugin[] = [];
@@ -77,7 +57,7 @@ function resolveCachedChannelSetupPlugins(): CachedChannelSetupPlugins {
const registryPlugins = (registry.channelSetups ?? []).map((entry) => entry.plugin);
const sorted = sortChannelSetupPlugins(
registryPlugins.length > 0 ? registryPlugins : BUNDLED_CHANNEL_SETUP_PLUGINS,
registryPlugins.length > 0 ? registryPlugins : bundledChannelSetupPlugins,
);
const byId = new Map<string, ChannelPlugin>();
for (const plugin of sorted) {

View File

@@ -1,2 +0,0 @@
// Shim: re-exports from extension
export * from "../../../../extensions/discord/src/status-issues.js";

View File

@@ -1 +0,0 @@
export * from "../../../../extensions/telegram/src/status-issues.js";

View File

@@ -1,2 +0,0 @@
// Shim: re-exports from extensions/whatsapp/src/status-issues.ts
export * from "../../../../extensions/whatsapp/src/status-issues.js";