From 2054cb9431b6eed97e91b8b03e2ba3f2fd6b71e5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Mar 2026 23:47:22 -0700 Subject: [PATCH] refactor: move remaining channel seams into plugins --- extensions/discord/src/channel.ts | 24 +- extensions/imessage/src/channel.ts | 13 +- extensions/signal/src/channel.ts | 14 +- extensions/slack/src/channel.ts | 24 +- extensions/telegram/src/channel.ts | 14 +- extensions/whatsapp/src/channel.ts | 13 +- src/agents/channel-tools.ts | 4 +- src/agents/pi-tools.policy.ts | 10 +- src/auto-reply/command-auth.ts | 63 +++-- src/auto-reply/commands-registry.data.ts | 26 +- src/auto-reply/reply/agent-runner-utils.ts | 10 +- src/auto-reply/reply/block-streaming.ts | 7 +- src/auto-reply/reply/commands-allowlist.ts | 222 ++------------- .../reply/get-reply-inline-actions.ts | 4 +- src/auto-reply/reply/groups.ts | 7 +- src/auto-reply/reply/mentions.ts | 5 +- src/auto-reply/reply/reply-elevated.ts | 19 +- src/auto-reply/reply/reply-threading.ts | 9 +- src/auto-reply/reply/route-reply.test.ts | 14 +- src/channels/plugins/outbound/discord.ts | 2 - .../plugins/outbound/imessage.test.ts | 2 +- src/channels/plugins/outbound/imessage.ts | 35 --- src/channels/plugins/outbound/signal.test.ts | 2 +- src/channels/plugins/outbound/signal.ts | 125 --------- .../outbound/slack.sendpayload.test.ts | 2 +- src/channels/plugins/outbound/slack.test.ts | 2 +- src/channels/plugins/outbound/slack.ts | 255 ------------------ src/channels/plugins/outbound/telegram.ts | 1 - src/channels/plugins/outbound/whatsapp.ts | 2 - src/channels/plugins/plugins-channel.test.ts | 3 +- src/channels/plugins/types.adapters.ts | 40 ++- src/config/sessions/metadata.ts | 5 +- ...gent.direct-delivery-core-channels.test.ts | 14 +- src/cron/isolated-agent.test-setup.ts | 3 +- src/infra/exec-approval-forwarder.test.ts | 12 +- ...tbeat-runner.returns-default-unset.test.ts | 2 +- src/infra/outbound/deliver.test-helpers.ts | 8 +- src/infra/outbound/deliver.test.ts | 8 +- src/infra/outbound/targets.test.ts | 3 +- src/plugin-sdk/index.ts | 1 + src/plugin-sdk/telegram.ts | 2 +- src/test-utils/imessage-test-plugin.ts | 2 +- 42 files changed, 246 insertions(+), 787 deletions(-) delete mode 100644 src/channels/plugins/outbound/discord.ts delete mode 100644 src/channels/plugins/outbound/imessage.ts delete mode 100644 src/channels/plugins/outbound/signal.ts delete mode 100644 src/channels/plugins/outbound/slack.ts delete mode 100644 src/channels/plugins/outbound/telegram.ts delete mode 100644 src/channels/plugins/outbound/whatsapp.ts diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index a16574bfb70..3c0da68a06a 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -1,6 +1,7 @@ import { Separator, TextDisplay } from "@buape/carbon"; import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat"; import { + buildAccountScopedAllowlistConfigEditor, buildAccountScopedDmSecurityPolicy, collectOpenProviderGroupPolicyWarnings, collectOpenGroupPolicyConfiguredRouteWarnings, @@ -262,16 +263,19 @@ export const discordPlugin: ChannelPlugin = { readDiscordAllowlistConfig(resolveDiscordAccount({ cfg, accountId })), resolveNames: async ({ cfg, accountId, entries }) => await resolveDiscordAllowlistNames({ cfg, accountId, entries }), - resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) => - scope === "dm" - ? { - pathPrefix, - writeTarget, - readPaths: [["allowFrom"], ["dm", "allowFrom"]], - writePath: ["allowFrom"], - cleanupPaths: [["dm", "allowFrom"]], - } - : null, + applyConfigEdit: buildAccountScopedAllowlistConfigEditor({ + channelId: "discord", + normalize: ({ cfg, accountId, values }) => + discordConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }), + resolvePaths: (scope) => + scope === "dm" + ? { + readPaths: [["allowFrom"], ["dm", "allowFrom"]], + writePath: ["allowFrom"], + cleanupPaths: [["dm", "allowFrom"]], + } + : null, + }), }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/imessage/src/channel.ts b/extensions/imessage/src/channel.ts index aec66694ef8..295f16970ad 100644 --- a/extensions/imessage/src/channel.ts +++ b/extensions/imessage/src/channel.ts @@ -1,4 +1,5 @@ import { + buildAccountScopedAllowlistConfigEditor, buildAccountScopedDmSecurityPolicy, collectAllowlistProviderRestrictSendersWarnings, } from "openclaw/plugin-sdk/compat"; @@ -135,11 +136,13 @@ export const imessagePlugin: ChannelPlugin = { groupPolicy: account.config.groupPolicy, }; }, - resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) => ({ - pathPrefix, - writeTarget, - readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]], - writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"], + applyConfigEdit: buildAccountScopedAllowlistConfigEditor({ + channelId: "imessage", + normalize: ({ values }) => formatTrimmedAllowFromEntries(values), + resolvePaths: (scope) => ({ + readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]], + writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"], + }), }), }, security: { diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index 80291872143..7567d68d4fa 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -1,4 +1,5 @@ import { + buildAccountScopedAllowlistConfigEditor, buildAccountScopedDmSecurityPolicy, createScopedAccountConfigAccessors, collectAllowlistProviderRestrictSendersWarnings, @@ -283,11 +284,14 @@ export const signalPlugin: ChannelPlugin = { groupPolicy: account.config.groupPolicy, }; }, - resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) => ({ - pathPrefix, - writeTarget, - readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]], - writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"], + applyConfigEdit: buildAccountScopedAllowlistConfigEditor({ + channelId: "signal", + normalize: ({ cfg, accountId, values }) => + signalConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }), + resolvePaths: (scope) => ({ + readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]], + writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"], + }), }), }, security: { diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 2a8849b1671..33322732236 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -1,5 +1,6 @@ import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat"; import { + buildAccountScopedAllowlistConfigEditor, buildAccountScopedDmSecurityPolicy, collectOpenProviderGroupPolicyWarnings, collectOpenGroupPolicyConfiguredRouteWarnings, @@ -279,16 +280,19 @@ export const slackPlugin: ChannelPlugin = { readSlackAllowlistConfig(resolveSlackAccount({ cfg, accountId })), resolveNames: async ({ cfg, accountId, entries }) => await resolveSlackAllowlistNames({ cfg, accountId, entries }), - resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) => - scope === "dm" - ? { - pathPrefix, - writeTarget, - readPaths: [["allowFrom"], ["dm", "allowFrom"]], - writePath: ["allowFrom"], - cleanupPaths: [["dm", "allowFrom"]], - } - : null, + applyConfigEdit: buildAccountScopedAllowlistConfigEditor({ + channelId: "slack", + normalize: ({ cfg, accountId, values }) => + slackConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }), + resolvePaths: (scope) => + scope === "dm" + ? { + readPaths: [["allowFrom"], ["dm", "allowFrom"]], + writePath: ["allowFrom"], + cleanupPaths: [["dm", "allowFrom"]], + } + : null, + }), }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index be09a186baf..dda83e3f521 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -1,5 +1,6 @@ import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat"; import { + buildAccountScopedAllowlistConfigEditor, collectAllowlistProviderGroupPolicyWarnings, collectOpenGroupPolicyRouteAllowlistWarnings, createScopedAccountConfigAccessors, @@ -358,11 +359,14 @@ export const telegramPlugin: ChannelPlugin scope === "dm" || scope === "group" || scope === "all", readConfig: ({ cfg, accountId }) => readTelegramAllowlistConfig(resolveTelegramAccount({ cfg, accountId })), - resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) => ({ - pathPrefix, - writeTarget, - readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]], - writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"], + applyConfigEdit: buildAccountScopedAllowlistConfigEditor({ + channelId: "telegram", + normalize: ({ cfg, accountId, values }) => + telegramConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }), + resolvePaths: (scope) => ({ + readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]], + writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"], + }), }), }, acpBindings: { diff --git a/extensions/whatsapp/src/channel.ts b/extensions/whatsapp/src/channel.ts index cf506e6912b..d7f437d3204 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -1,3 +1,4 @@ +import { buildAccountScopedAllowlistConfigEditor } from "openclaw/plugin-sdk/compat"; import { buildChannelConfigSchema, buildAccountScopedDmSecurityPolicy, @@ -195,11 +196,13 @@ export const whatsappPlugin: ChannelPlugin = { groupPolicy: account.groupPolicy, }; }, - resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) => ({ - pathPrefix, - writeTarget, - readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]], - writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"], + applyConfigEdit: buildAccountScopedAllowlistConfigEditor({ + channelId: "whatsapp", + normalize: ({ values }) => formatWhatsAppConfigAllowFromEntries(values), + resolvePaths: (scope) => ({ + readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]], + writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"], + }), }), }, security: { diff --git a/src/agents/channel-tools.ts b/src/agents/channel-tools.ts index e49a090f509..242cce868c1 100644 --- a/src/agents/channel-tools.ts +++ b/src/agents/channel-tools.ts @@ -1,4 +1,3 @@ -import { getChannelDock } from "../channels/dock.js"; import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js"; import type { ChannelAgentTool, @@ -73,8 +72,7 @@ export function resolveChannelMessageToolHints(params: { if (!channelId) { return []; } - const dock = getChannelDock(channelId); - const resolve = dock?.agentPrompt?.messageToolHints; + const resolve = getChannelPlugin(channelId)?.agentPrompt?.messageToolHints; if (!resolve) { return []; } diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index a6f8651f72d..4e7cea7c94e 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -1,4 +1,4 @@ -import { getChannelDock } from "../channels/dock.js"; +import { getChannelPlugin } from "../channels/plugins/index.js"; import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js"; @@ -315,14 +315,14 @@ export function resolveGroupToolPolicy(params: { if (!channel) { return undefined; } - let dock; + let plugin; try { - dock = getChannelDock(channel); + plugin = getChannelPlugin(channel); } catch { - dock = undefined; + plugin = undefined; } const toolsConfig = - dock?.groups?.resolveToolPolicy?.({ + plugin?.groups?.resolveToolPolicy?.({ cfg: params.config, groupId, groupChannel: params.groupChannel, diff --git a/src/auto-reply/command-auth.ts b/src/auto-reply/command-auth.ts index ead6e6e0312..956c132d773 100644 --- a/src/auto-reply/command-auth.ts +++ b/src/auto-reply/command-auth.ts @@ -1,6 +1,5 @@ -import type { ChannelDock } from "../channels/dock.js"; -import { getChannelDock, listChannelDocks } from "../channels/dock.js"; -import type { ChannelId } from "../channels/plugins/types.js"; +import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js"; +import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.js"; import { normalizeAnyChannelId } from "../channels/registry.js"; import type { OpenClawConfig } from "../config/config.js"; import { normalizeStringEntries } from "../shared/string-normalization.js"; @@ -52,19 +51,19 @@ function resolveProviderFromContext(ctx: MsgContext, cfg: OpenClawConfig): Chann return normalized; } } - const configured = listChannelDocks() - .map((dock) => { - if (!dock.config?.resolveAllowFrom) { + const configured = listChannelPlugins() + .map((plugin) => { + if (!plugin.config?.resolveAllowFrom) { return null; } - const allowFrom = dock.config.resolveAllowFrom({ + const allowFrom = plugin.config.resolveAllowFrom({ cfg, accountId: ctx.AccountId, }); if (!Array.isArray(allowFrom) || allowFrom.length === 0) { return null; } - return dock.id; + return plugin.id; }) .filter((value): value is ChannelId => Boolean(value)); if (configured.length === 1) { @@ -74,29 +73,29 @@ function resolveProviderFromContext(ctx: MsgContext, cfg: OpenClawConfig): Chann } function formatAllowFromList(params: { - dock?: ChannelDock; + plugin?: ChannelPlugin; cfg: OpenClawConfig; accountId?: string | null; allowFrom: Array; }): string[] { - const { dock, cfg, accountId, allowFrom } = params; + const { plugin, cfg, accountId, allowFrom } = params; if (!allowFrom || allowFrom.length === 0) { return []; } - if (dock?.config?.formatAllowFrom) { - return dock.config.formatAllowFrom({ cfg, accountId, allowFrom }); + if (plugin?.config?.formatAllowFrom) { + return plugin.config.formatAllowFrom({ cfg, accountId, allowFrom }); } return normalizeStringEntries(allowFrom); } function normalizeAllowFromEntry(params: { - dock?: ChannelDock; + plugin?: ChannelPlugin; cfg: OpenClawConfig; accountId?: string | null; value: string; }): string[] { const normalized = formatAllowFromList({ - dock: params.dock, + plugin: params.plugin, cfg: params.cfg, accountId: params.accountId, allowFrom: [params.value], @@ -105,7 +104,7 @@ function normalizeAllowFromEntry(params: { } function resolveOwnerAllowFromList(params: { - dock?: ChannelDock; + plugin?: ChannelPlugin; cfg: OpenClawConfig; accountId?: string | null; providerId?: ChannelId; @@ -139,7 +138,7 @@ function resolveOwnerAllowFromList(params: { filtered.push(trimmed); } return formatAllowFromList({ - dock: params.dock, + plugin: params.plugin, cfg: params.cfg, accountId: params.accountId, allowFrom: filtered, @@ -152,12 +151,12 @@ function resolveOwnerAllowFromList(params: { * Returns null if commands.allowFrom is not configured at all (fall back to channel allowFrom). */ function resolveCommandsAllowFromList(params: { - dock?: ChannelDock; + plugin?: ChannelPlugin; cfg: OpenClawConfig; accountId?: string | null; providerId?: ChannelId; }): string[] | null { - const { dock, cfg, accountId, providerId } = params; + const { plugin, cfg, accountId, providerId } = params; const commandsAllowFrom = cfg.commands?.allowFrom; if (!commandsAllowFrom || typeof commandsAllowFrom !== "object") { return null; // Not configured, fall back to channel allowFrom @@ -174,7 +173,7 @@ function resolveCommandsAllowFromList(params: { } return formatAllowFromList({ - dock, + plugin, cfg, accountId, allowFrom: rawList, @@ -211,7 +210,7 @@ function shouldUseFromAsSenderFallback(params: { } function resolveSenderCandidates(params: { - dock?: ChannelDock; + plugin?: ChannelPlugin; providerId?: ChannelId; cfg: OpenClawConfig; accountId?: string | null; @@ -220,7 +219,7 @@ function resolveSenderCandidates(params: { from?: string | null; chatType?: string | null; }): string[] { - const { dock, cfg, accountId } = params; + const { plugin, cfg, accountId } = params; const candidates: string[] = []; const pushCandidate = (value?: string | null) => { const trimmed = (value ?? "").trim(); @@ -245,7 +244,7 @@ function resolveSenderCandidates(params: { const normalized: string[] = []; for (const sender of candidates) { - const entries = normalizeAllowFromEntry({ dock, cfg, accountId, value: sender }); + const entries = normalizeAllowFromEntry({ plugin, cfg, accountId, value: sender }); for (const entry of entries) { if (!normalized.includes(entry)) { normalized.push(entry); @@ -262,36 +261,36 @@ export function resolveCommandAuthorization(params: { }): CommandAuthorization { const { ctx, cfg, commandAuthorized } = params; const providerId = resolveProviderFromContext(ctx, cfg); - const dock = providerId ? getChannelDock(providerId) : undefined; + const plugin = providerId ? getChannelPlugin(providerId) : undefined; const from = (ctx.From ?? "").trim(); const to = (ctx.To ?? "").trim(); // Check if commands.allowFrom is configured (separate command authorization) const commandsAllowFromList = resolveCommandsAllowFromList({ - dock, + plugin, cfg, accountId: ctx.AccountId, providerId, }); - const allowFromRaw = dock?.config?.resolveAllowFrom - ? dock.config.resolveAllowFrom({ cfg, accountId: ctx.AccountId }) + const allowFromRaw = plugin?.config?.resolveAllowFrom + ? plugin.config.resolveAllowFrom({ cfg, accountId: ctx.AccountId }) : []; const allowFromList = formatAllowFromList({ - dock, + plugin, cfg, accountId: ctx.AccountId, allowFrom: Array.isArray(allowFromRaw) ? allowFromRaw : [], }); const configOwnerAllowFromList = resolveOwnerAllowFromList({ - dock, + plugin, cfg, accountId: ctx.AccountId, providerId, allowFrom: cfg.commands?.ownerAllowFrom, }); const contextOwnerAllowFromList = resolveOwnerAllowFromList({ - dock, + plugin, cfg, accountId: ctx.AccountId, providerId, @@ -303,7 +302,7 @@ export function resolveCommandAuthorization(params: { const ownerCandidatesForCommands = allowAll ? [] : allowFromList.filter((entry) => entry !== "*"); if (!allowAll && ownerCandidatesForCommands.length === 0 && to) { const normalizedTo = normalizeAllowFromEntry({ - dock, + plugin, cfg, accountId: ctx.AccountId, value: to, @@ -328,7 +327,7 @@ export function resolveCommandAuthorization(params: { ); const senderCandidates = resolveSenderCandidates({ - dock, + plugin, providerId, cfg, accountId: ctx.AccountId, @@ -345,7 +344,7 @@ export function resolveCommandAuthorization(params: { : undefined; const senderId = matchedSender ?? senderCandidates[0]; - const enforceOwner = Boolean(dock?.commands?.enforceOwnerForCommands); + const enforceOwner = Boolean(plugin?.commands?.enforceOwnerForCommands); const senderIsOwnerByIdentity = Boolean(matchedSender); const senderIsOwnerByScope = isInternalMessageChannel(ctx.Provider) && diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index 80f8d4bd73f..58064473543 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -1,4 +1,4 @@ -import { listChannelDocks } from "../channels/dock.js"; +import { listChannelPlugins } from "../channels/plugins/index.js"; import { getActivePluginRegistry } from "../plugins/runtime.js"; import { COMMAND_ARG_FORMATTERS } from "./commands-args.js"; import type { @@ -46,14 +46,14 @@ function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefiniti }; } -type ChannelDock = ReturnType[number]; +type ChannelPlugin = ReturnType[number]; -function defineDockCommand(dock: ChannelDock): ChatCommandDefinition { +function defineDockCommand(plugin: ChannelPlugin): ChatCommandDefinition { return defineChatCommand({ - key: `dock:${dock.id}`, - nativeName: `dock_${dock.id}`, - description: `Switch to ${dock.id} for replies.`, - textAliases: [`/dock-${dock.id}`, `/dock_${dock.id}`], + key: `dock:${plugin.id}`, + nativeName: `dock_${plugin.id}`, + description: `Switch to ${plugin.id} for replies.`, + textAliases: [`/dock-${plugin.id}`, `/dock_${plugin.id}`], category: "docks", }); } @@ -758,9 +758,9 @@ function buildChatCommands(): ChatCommandDefinition[] { }, ], }), - ...listChannelDocks() - .filter((dock) => dock.capabilities.nativeCommands) - .map((dock) => defineDockCommand(dock)), + ...listChannelPlugins() + .filter((plugin) => plugin.capabilities.nativeCommands) + .map((plugin) => defineDockCommand(plugin)), ]; registerAlias(commands, "whoami", "/id"); @@ -792,9 +792,9 @@ export function getNativeCommandSurfaces(): Set { return cachedNativeCommandSurfaces; } cachedNativeCommandSurfaces = new Set( - listChannelDocks() - .filter((dock) => dock.capabilities.nativeCommands) - .map((dock) => dock.id), + listChannelPlugins() + .filter((plugin) => plugin.capabilities.nativeCommands) + .map((plugin) => plugin.id), ); cachedNativeRegistry = registry; return cachedNativeCommandSurfaces; diff --git a/src/auto-reply/reply/agent-runner-utils.ts b/src/auto-reply/reply/agent-runner-utils.ts index c6e71a9bab0..abf6322a287 100644 --- a/src/auto-reply/reply/agent-runner-utils.ts +++ b/src/auto-reply/reply/agent-runner-utils.ts @@ -1,6 +1,6 @@ import { resolveRunModelFallbacksOverride } from "../../agents/agent-scope.js"; import type { NormalizedUsage } from "../../agents/usage.js"; -import { getChannelDock } from "../../channels/dock.js"; +import { getChannelPlugin } from "../../channels/plugins/index.js"; import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js"; import { normalizeAnyChannelId, normalizeChannelId } from "../../channels/registry.js"; import type { OpenClawConfig } from "../../config/config.js"; @@ -44,8 +44,8 @@ export function buildThreadingToolContext(params: { } const provider = normalizeChannelId(rawProvider) ?? normalizeAnyChannelId(rawProvider); // Fallback for unrecognized/plugin channels (e.g., BlueBubbles before plugin registry init) - const dock = provider ? getChannelDock(provider) : undefined; - if (!dock?.threading?.buildToolContext) { + const threading = provider ? getChannelPlugin(provider)?.threading : undefined; + if (!threading?.buildToolContext) { return { currentChannelId: originTo?.trim() || undefined, currentChannelProvider: provider ?? (rawProvider as ChannelId), @@ -54,7 +54,7 @@ export function buildThreadingToolContext(params: { }; } const context = - dock.threading.buildToolContext({ + threading.buildToolContext({ cfg: config, accountId: sessionCtx.AccountId, context: { @@ -72,7 +72,7 @@ export function buildThreadingToolContext(params: { }) ?? {}; return { ...context, - currentChannelProvider: provider!, // guaranteed non-null since dock exists + currentChannelProvider: provider!, // guaranteed non-null since threading exists currentMessageId: context.currentMessageId ?? currentMessageId, }; } diff --git a/src/auto-reply/reply/block-streaming.ts b/src/auto-reply/reply/block-streaming.ts index b24ee8cac1a..9149f7c8562 100644 --- a/src/auto-reply/reply/block-streaming.ts +++ b/src/auto-reply/reply/block-streaming.ts @@ -1,5 +1,4 @@ -import { getChannelDock } from "../../channels/dock.js"; -import { normalizeChannelId } from "../../channels/plugins/index.js"; +import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { BlockStreamingCoalesceConfig } from "../../config/types.js"; import { resolveAccountEntry } from "../../routing/account-lookup.js"; @@ -34,7 +33,7 @@ function resolveProviderChunkContext( const providerKey = normalizeChunkProvider(provider); const providerId = providerKey ? normalizeChannelId(providerKey) : null; const providerChunkLimit = providerId - ? getChannelDock(providerId)?.outbound?.textChunkLimit + ? getChannelPlugin(providerId)?.outbound?.textChunkLimit : undefined; const textLimit = resolveTextChunkLimit(cfg, providerKey, accountId, { fallbackLimit: providerChunkLimit, @@ -209,7 +208,7 @@ export function resolveBlockStreamingCoalescing( // when chunkMode="newline", matching the delivery-time splitting behavior. const chunkMode = opts?.chunkMode ?? resolveChunkMode(cfg, providerKey, accountId); const providerDefaults = providerId - ? getChannelDock(providerId)?.streaming?.blockStreamingCoalesceDefaults + ? getChannelPlugin(providerId)?.streaming?.blockStreamingCoalesceDefaults : undefined; const providerCfg = resolveProviderBlockStreamingCoalesce({ cfg, diff --git a/src/auto-reply/reply/commands-allowlist.ts b/src/auto-reply/reply/commands-allowlist.ts index f371fcd0b62..7360fa20252 100644 --- a/src/auto-reply/reply/commands-allowlist.ts +++ b/src/auto-reply/reply/commands-allowlist.ts @@ -1,5 +1,4 @@ import { getChannelPlugin } from "../../channels/plugins/index.js"; -import { listPairingChannels } from "../../channels/plugins/pairing.js"; import type { ChannelId } from "../../channels/plugins/types.js"; import { normalizeChannelId } from "../../channels/registry.js"; import type { OpenClawConfig } from "../../config/config.js"; @@ -8,7 +7,6 @@ import { validateConfigObjectWithPlugins, writeConfigFile, } from "../../config/config.js"; -import { isBlockedObjectKey } from "../../infra/prototype-keys.js"; import { addChannelAllowFromStoreEntry, readChannelAllowFromStore, @@ -198,104 +196,6 @@ async function updatePairingStoreAllowlist(params: { } } -function resolveAccountTarget( - parsed: Record, - channelId: ChannelId, - accountId?: string | null, -) { - const channels = (parsed.channels ??= {}) as Record; - const channel = (channels[channelId] ??= {}) as Record; - const normalizedAccountId = normalizeAccountId(accountId); - if (isBlockedObjectKey(normalizedAccountId)) { - return { - target: channel, - pathPrefix: `channels.${channelId}`, - accountId: DEFAULT_ACCOUNT_ID, - writeTarget: { kind: "channel", scope: { channelId } } as const, - }; - } - const hasAccounts = Boolean(channel.accounts && typeof channel.accounts === "object"); - const useAccount = normalizedAccountId !== DEFAULT_ACCOUNT_ID || hasAccounts; - if (!useAccount) { - return { - target: channel, - pathPrefix: `channels.${channelId}`, - accountId: normalizedAccountId, - writeTarget: { kind: "channel", scope: { channelId } } as const, - }; - } - const accounts = (channel.accounts ??= {}) as Record; - const existingAccount = Object.hasOwn(accounts, normalizedAccountId) - ? accounts[normalizedAccountId] - : undefined; - if (!existingAccount || typeof existingAccount !== "object") { - accounts[normalizedAccountId] = {}; - } - const account = accounts[normalizedAccountId] as Record; - return { - target: account, - pathPrefix: `channels.${channelId}.accounts.${normalizedAccountId}`, - accountId: normalizedAccountId, - writeTarget: { - kind: "account", - scope: { channelId, accountId: normalizedAccountId }, - } as const, - }; -} - -function getNestedValue(root: Record, path: string[]): unknown { - let current: unknown = root; - for (const key of path) { - if (!current || typeof current !== "object") { - return undefined; - } - current = (current as Record)[key]; - } - return current; -} - -function ensureNestedObject( - root: Record, - path: string[], -): Record { - let current = root; - for (const key of path) { - const existing = current[key]; - if (!existing || typeof existing !== "object") { - current[key] = {}; - } - current = current[key] as Record; - } - return current; -} - -function setNestedValue(root: Record, path: string[], value: unknown) { - if (path.length === 0) { - return; - } - if (path.length === 1) { - root[path[0]] = value; - return; - } - const parent = ensureNestedObject(root, path.slice(0, -1)); - parent[path[path.length - 1]] = value; -} - -function deleteNestedValue(root: Record, path: string[]) { - if (path.length === 0) { - return; - } - if (path.length === 1) { - delete root[path[0]]; - return; - } - const parent = getNestedValue(root, path.slice(0, -1)); - if (!parent || typeof parent !== "object") { - return; - } - delete (parent as Record)[path[path.length - 1]]; -} - function mapResolvedAllowlistNames(entries: ResolvedAllowlistName[]): Map { const map = new Map(); for (const entry of entries) { @@ -375,7 +275,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo const plugin = getChannelPlugin(channelId); if (parsed.action === "list") { - const supportsStore = listPairingChannels().includes(channelId); + const supportsStore = Boolean(plugin?.pairing); if (!plugin?.allowlist?.readConfig && !supportsStore) { return { shouldContinue: false, @@ -493,7 +393,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo } const shouldUpdateConfig = parsed.target !== "store"; - const shouldTouchStore = parsed.target !== "config" && listPairingChannels().includes(channelId); + const shouldTouchStore = parsed.target !== "config" && Boolean(plugin?.pairing); if (shouldUpdateConfig) { if (parsed.scope === "all") { @@ -502,19 +402,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo reply: { text: "⚠️ /allowlist add|remove requires scope dm or group." }, }; } - const { - target, - pathPrefix, - accountId: normalizedAccountId, - writeTarget, - } = resolveAccountTarget(structuredClone({ channels: {} }), channelId, accountId); - void target; - const editSpec = plugin?.allowlist?.resolveConfigEdit?.({ - scope: parsed.scope, - pathPrefix, - writeTarget, - }); - if (!editSpec) { + if (!plugin?.allowlist?.applyConfigEdit) { return { shouldContinue: false, reply: { @@ -531,14 +419,35 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo }; } const parsedConfig = structuredClone(snapshot.parsed as Record); - const resolvedTarget = resolveAccountTarget(parsedConfig, channelId, accountId); + const editResult = await plugin.allowlist.applyConfigEdit({ + cfg: params.cfg, + parsedConfig, + accountId, + scope: parsed.scope, + action: parsed.action, + entry: parsed.entry, + }); + if (!editResult) { + return { + shouldContinue: false, + reply: { + text: `⚠️ ${channelId} does not support ${parsed.scope} allowlist edits via /allowlist.`, + }, + }; + } + if (editResult.kind === "invalid-entry") { + return { + shouldContinue: false, + reply: { text: "⚠️ Invalid allowlist entry." }, + }; + } const deniedText = resolveConfigWriteDeniedText({ cfg: params.cfg, channel: params.command.channel, channelId, accountId: params.ctx.AccountId, gatewayClientScopes: params.ctx.GatewayClientScopes, - target: editSpec.writeTarget, + target: editResult.writeTarget, }); if (deniedText) { return { @@ -548,82 +457,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo }, }; } - - const existing: string[] = []; - for (const path of editSpec.readPaths) { - const existingRaw = getNestedValue(resolvedTarget.target, path); - if (!Array.isArray(existingRaw)) { - continue; - } - for (const entry of existingRaw) { - const value = String(entry).trim(); - if (!value || existing.includes(value)) { - continue; - } - existing.push(value); - } - } - - const normalizedEntry = normalizeAllowFrom({ - cfg: params.cfg, - channelId, - accountId: normalizedAccountId, - values: [parsed.entry], - }); - if (normalizedEntry.length === 0) { - return { - shouldContinue: false, - reply: { text: "⚠️ Invalid allowlist entry." }, - }; - } - - const existingNormalized = normalizeAllowFrom({ - cfg: params.cfg, - channelId, - accountId: normalizedAccountId, - values: existing, - }); - - const shouldMatch = (value: string) => normalizedEntry.includes(value); - - let configChanged = false; - let next = existing; - const configHasEntry = existingNormalized.some((value) => shouldMatch(value)); - if (parsed.action === "add") { - if (!configHasEntry) { - next = [...existing, parsed.entry.trim()]; - configChanged = true; - } - } - - if (parsed.action === "remove") { - const keep: string[] = []; - for (const entry of existing) { - const normalized = normalizeAllowFrom({ - cfg: params.cfg, - channelId, - accountId: normalizedAccountId, - values: [entry], - }); - if (normalized.some((value) => shouldMatch(value))) { - configChanged = true; - continue; - } - keep.push(entry); - } - next = keep; - } - - if (configChanged) { - if (next.length === 0) { - deleteNestedValue(resolvedTarget.target, editSpec.writePath); - } else { - setNestedValue(resolvedTarget.target, editSpec.writePath, next); - } - for (const path of editSpec.cleanupPaths ?? []) { - deleteNestedValue(resolvedTarget.target, path); - } - } + const configChanged = editResult.changed; if (configChanged) { const validated = validateConfigObjectWithPlugins(parsedConfig); @@ -655,7 +489,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo const scopeLabel = parsed.scope === "dm" ? "DM" : "group"; const locations: string[] = []; if (configChanged) { - locations.push(`${resolvedTarget.pathPrefix}.${editSpec.writePath.join(".")}`); + locations.push(editResult.pathLabel); } if (shouldTouchStore) { locations.push("pairing store"); diff --git a/src/auto-reply/reply/get-reply-inline-actions.ts b/src/auto-reply/reply/get-reply-inline-actions.ts index b4f921672f8..73983cfdc49 100644 --- a/src/auto-reply/reply/get-reply-inline-actions.ts +++ b/src/auto-reply/reply/get-reply-inline-actions.ts @@ -3,7 +3,7 @@ import { createOpenClawTools } from "../../agents/openclaw-tools.js"; import type { BlockReplyChunking } from "../../agents/pi-embedded-block-chunker.js"; import type { SkillCommandSpec } from "../../agents/skills.js"; import { applyOwnerOnlyToolPolicy } from "../../agents/tool-policy.js"; -import { getChannelDock } from "../../channels/dock.js"; +import { getChannelPlugin } from "../../channels/plugins/index.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; import { logVerbose } from "../../globals.js"; @@ -402,7 +402,7 @@ export async function handleInlineActions(params: { const isEmptyConfig = Object.keys(cfg).length === 0; const skipWhenConfigEmpty = command.channelId - ? Boolean(getChannelDock(command.channelId)?.commands?.skipWhenConfigEmpty) + ? Boolean(getChannelPlugin(command.channelId)?.commands?.skipWhenConfigEmpty) : false; if ( skipWhenConfigEmpty && diff --git a/src/auto-reply/reply/groups.ts b/src/auto-reply/reply/groups.ts index dcf398d5a4b..acdbbe67faf 100644 --- a/src/auto-reply/reply/groups.ts +++ b/src/auto-reply/reply/groups.ts @@ -1,4 +1,3 @@ -import { getChannelDock } from "../../channels/dock.js"; import { getChannelPlugin, normalizeChannelId as normalizePluginChannelId, @@ -39,7 +38,7 @@ function resolveDockChannelId(raw?: string | null): ChannelId | null { return null; } try { - if (getChannelDock(normalized as ChannelId)) { + if (getChannelPlugin(normalized as ChannelId)) { return normalized as ChannelId; } } catch { @@ -68,7 +67,7 @@ export function resolveGroupRequireMention(params: { const groupSpace = ctx.GroupSpace?.trim(); let requireMention: boolean | undefined; try { - requireMention = getChannelDock(channel)?.groups?.resolveRequireMention?.({ + requireMention = getChannelPlugin(channel)?.groups?.resolveRequireMention?.({ cfg, groupId, groupChannel, @@ -158,7 +157,7 @@ export function buildGroupIntro(params: { params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(); const groupSpace = params.sessionCtx.GroupSpace?.trim(); const providerIdsLine = providerId - ? getChannelDock(providerId)?.groups?.resolveGroupIntroHint?.({ + ? getChannelPlugin(providerId)?.groups?.resolveGroupIntroHint?.({ cfg: params.cfg, groupId, groupChannel, diff --git a/src/auto-reply/reply/mentions.ts b/src/auto-reply/reply/mentions.ts index 714e599e38a..5b60cf6688f 100644 --- a/src/auto-reply/reply/mentions.ts +++ b/src/auto-reply/reply/mentions.ts @@ -1,6 +1,5 @@ import { resolveAgentConfig } from "../../agents/agent-scope.js"; -import { getChannelDock } from "../../channels/dock.js"; -import { normalizeChannelId } from "../../channels/plugins/index.js"; +import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import type { OpenClawConfig } from "../../config/config.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { compileConfigRegexes, type ConfigRegexRejectReason } from "../../security/config-regex.js"; @@ -199,7 +198,7 @@ export function stripMentions( ): string { let result = text; const providerId = ctx.Provider ? normalizeChannelId(ctx.Provider) : null; - const providerMentions = providerId ? getChannelDock(providerId)?.mentions : undefined; + const providerMentions = providerId ? getChannelPlugin(providerId)?.mentions : undefined; const configRegexes = compileMentionPatternsCached({ patterns: normalizeMentionPatterns(resolveMentionPatterns(cfg, agentId)), flags: "gi", diff --git a/src/auto-reply/reply/reply-elevated.ts b/src/auto-reply/reply/reply-elevated.ts index 17da0058dd6..9a6e5093bda 100644 --- a/src/auto-reply/reply/reply-elevated.ts +++ b/src/auto-reply/reply/reply-elevated.ts @@ -1,6 +1,5 @@ import { resolveAgentConfig } from "../../agents/agent-scope.js"; -import { getChannelDock } from "../../channels/dock.js"; -import { normalizeChannelId } from "../../channels/plugins/index.js"; +import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import type { AgentElevatedAllowFromConfig, OpenClawConfig } from "../../config/config.js"; import { normalizeStringEntries } from "../../shared/string-normalization.js"; import type { MsgContext } from "../templating.js"; @@ -34,8 +33,9 @@ function resolveAllowFromFormatter(params: { accountId?: string; }): AllowFromFormatter { const normalizedProvider = normalizeChannelId(params.provider); - const dock = normalizedProvider ? getChannelDock(normalizedProvider) : undefined; - const formatAllowFrom = dock?.config?.formatAllowFrom; + const formatAllowFrom = normalizedProvider + ? getChannelPlugin(normalizedProvider)?.config?.formatAllowFrom + : undefined; if (!formatAllowFrom) { return (values) => normalizeStringEntries(values); } @@ -192,11 +192,12 @@ export function resolveElevatedPermissions(params: { } const normalizedProvider = normalizeChannelId(params.provider); - const dock = normalizedProvider ? getChannelDock(normalizedProvider) : undefined; - const fallbackAllowFrom = dock?.elevated?.allowFromFallback?.({ - cfg: params.cfg, - accountId: params.ctx.AccountId, - }); + const fallbackAllowFrom = normalizedProvider + ? getChannelPlugin(normalizedProvider)?.elevated?.allowFromFallback?.({ + cfg: params.cfg, + accountId: params.ctx.AccountId, + }) + : undefined; const formatAllowFrom = resolveAllowFromFormatter({ cfg: params.cfg, provider: params.provider, diff --git a/src/auto-reply/reply/reply-threading.ts b/src/auto-reply/reply/reply-threading.ts index 5db377bbd00..66871f226b7 100644 --- a/src/auto-reply/reply/reply-threading.ts +++ b/src/auto-reply/reply/reply-threading.ts @@ -1,5 +1,4 @@ -import { getChannelDock } from "../../channels/dock.js"; -import { normalizeChannelId } from "../../channels/plugins/index.js"; +import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { ReplyToMode } from "../../config/types.js"; import type { OriginatingChannelType } from "../templating.js"; @@ -15,7 +14,7 @@ export function resolveReplyToMode( if (!provider) { return "all"; } - const resolved = getChannelDock(provider)?.threading?.resolveReplyToMode?.({ + const resolved = getChannelPlugin(provider)?.threading?.resolveReplyToMode?.({ cfg, accountId, chatType, @@ -59,9 +58,9 @@ export function createReplyToModeFilterForChannel( const isWebchat = normalized === "webchat"; // Default: allow explicit reply tags/directives even when replyToMode is "off". // Unknown channels fail closed; internal webchat stays allowed. - const dock = provider ? getChannelDock(provider) : undefined; + const threading = provider ? getChannelPlugin(provider)?.threading : undefined; const allowExplicitReplyTagsWhenOff = provider - ? (dock?.threading?.allowExplicitReplyTagsWhenOff ?? dock?.threading?.allowTagsWhenOff ?? true) + ? (threading?.allowExplicitReplyTagsWhenOff ?? threading?.allowTagsWhenOff ?? true) : isWebchat; return createReplyToModeFilter(mode, { allowExplicitReplyTagsWhenOff, diff --git a/src/auto-reply/reply/route-reply.test.ts b/src/auto-reply/reply/route-reply.test.ts index c0023ae1c37..b7b6cd31e9f 100644 --- a/src/auto-reply/reply/route-reply.test.ts +++ b/src/auto-reply/reply/route-reply.test.ts @@ -1,12 +1,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { mattermostPlugin } from "../../../extensions/mattermost/src/channel.js"; import { slackPlugin } from "../../../extensions/slack/src/channel.js"; -import { discordOutbound } from "../../channels/plugins/outbound/discord.js"; -import { imessageOutbound } from "../../channels/plugins/outbound/imessage.js"; -import { signalOutbound } from "../../channels/plugins/outbound/signal.js"; -import { slackOutbound } from "../../channels/plugins/outbound/slack.js"; -import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js"; -import { whatsappOutbound } from "../../channels/plugins/outbound/whatsapp.js"; +import { + discordOutbound, + imessageOutbound, + signalOutbound, + slackOutbound, + telegramOutbound, + whatsappOutbound, +} from "../../../test/channel-outbounds.js"; import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { PluginRegistry } from "../../plugins/registry.js"; diff --git a/src/channels/plugins/outbound/discord.ts b/src/channels/plugins/outbound/discord.ts deleted file mode 100644 index 5b2126b8fcc..00000000000 --- a/src/channels/plugins/outbound/discord.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Shim: re-exports from extension -export * from "../../../../extensions/discord/src/outbound-adapter.js"; diff --git a/src/channels/plugins/outbound/imessage.test.ts b/src/channels/plugins/outbound/imessage.test.ts index b42b5a954c8..04c68a94f82 100644 --- a/src/channels/plugins/outbound/imessage.test.ts +++ b/src/channels/plugins/outbound/imessage.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; +import { imessageOutbound } from "../../../../test/channel-outbounds.js"; import type { OpenClawConfig } from "../../../config/config.js"; -import { imessageOutbound } from "./imessage.js"; describe("imessageOutbound", () => { const cfg: OpenClawConfig = { diff --git a/src/channels/plugins/outbound/imessage.ts b/src/channels/plugins/outbound/imessage.ts deleted file mode 100644 index b916c1e37df..00000000000 --- a/src/channels/plugins/outbound/imessage.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { sendMessageIMessage } from "../../../../extensions/imessage/src/send.js"; -import { - resolveOutboundSendDep, - type OutboundSendDeps, -} from "../../../infra/outbound/send-deps.js"; -import { - createScopedChannelMediaMaxBytesResolver, - createDirectTextMediaOutbound, -} from "./direct-text-media.js"; - -function resolveIMessageSender(deps: OutboundSendDeps | undefined) { - return ( - resolveOutboundSendDep(deps, "imessage") ?? sendMessageIMessage - ); -} - -export const imessageOutbound = createDirectTextMediaOutbound({ - channel: "imessage", - resolveSender: resolveIMessageSender, - resolveMaxBytes: createScopedChannelMediaMaxBytesResolver("imessage"), - buildTextOptions: ({ cfg, maxBytes, accountId, replyToId }) => ({ - config: cfg, - maxBytes, - accountId: accountId ?? undefined, - replyToId: replyToId ?? undefined, - }), - buildMediaOptions: ({ cfg, mediaUrl, maxBytes, accountId, replyToId, mediaLocalRoots }) => ({ - config: cfg, - mediaUrl, - maxBytes, - accountId: accountId ?? undefined, - replyToId: replyToId ?? undefined, - mediaLocalRoots, - }), -}); diff --git a/src/channels/plugins/outbound/signal.test.ts b/src/channels/plugins/outbound/signal.test.ts index 9848c558965..5d28e4aefaf 100644 --- a/src/channels/plugins/outbound/signal.test.ts +++ b/src/channels/plugins/outbound/signal.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; +import { signalOutbound } from "../../../../test/channel-outbounds.js"; import type { OpenClawConfig } from "../../../config/config.js"; -import { signalOutbound } from "./signal.js"; describe("signalOutbound", () => { const cfg: OpenClawConfig = { diff --git a/src/channels/plugins/outbound/signal.ts b/src/channels/plugins/outbound/signal.ts deleted file mode 100644 index 9de4e6f0fa7..00000000000 --- a/src/channels/plugins/outbound/signal.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { markdownToSignalTextChunks } from "../../../../extensions/signal/src/format.js"; -import { sendMessageSignal } from "../../../../extensions/signal/src/send.js"; -import { resolveTextChunkLimit } from "../../../auto-reply/chunk.js"; -import { resolveMarkdownTableMode } from "../../../config/markdown-tables.js"; -import { - resolveOutboundSendDep, - type OutboundSendDeps, -} from "../../../infra/outbound/send-deps.js"; -import type { ChannelOutboundAdapter } from "../types.js"; -import { createScopedChannelMediaMaxBytesResolver } from "./direct-text-media.js"; - -function resolveSignalSender(deps: OutboundSendDeps | undefined) { - return resolveOutboundSendDep(deps, "signal") ?? sendMessageSignal; -} - -const resolveSignalMaxBytes = createScopedChannelMediaMaxBytesResolver("signal"); -type SignalSendOpts = NonNullable[2]>; - -function inferSignalTableMode(params: { cfg: SignalSendOpts["cfg"]; accountId?: string | null }) { - return resolveMarkdownTableMode({ - cfg: params.cfg, - channel: "signal", - accountId: params.accountId ?? undefined, - }); -} - -export const signalOutbound: ChannelOutboundAdapter = { - deliveryMode: "direct", - chunker: (text, _limit) => text.split(/\n{2,}/).flatMap((chunk) => (chunk ? [chunk] : [])), - chunkerMode: "text", - textChunkLimit: 4000, - sendFormattedText: async ({ cfg, to, text, accountId, deps, abortSignal }) => { - const send = resolveSignalSender(deps); - const maxBytes = resolveSignalMaxBytes({ - cfg, - accountId: accountId ?? undefined, - }); - const limit = resolveTextChunkLimit(cfg, "signal", accountId ?? undefined, { - fallbackLimit: 4000, - }); - const tableMode = inferSignalTableMode({ cfg, accountId }); - let chunks = - limit === undefined - ? markdownToSignalTextChunks(text, Number.POSITIVE_INFINITY, { tableMode }) - : markdownToSignalTextChunks(text, limit, { tableMode }); - if (chunks.length === 0 && text) { - chunks = [{ text, styles: [] }]; - } - const results = []; - for (const chunk of chunks) { - abortSignal?.throwIfAborted(); - const result = await send(to, chunk.text, { - cfg, - maxBytes, - accountId: accountId ?? undefined, - textMode: "plain", - textStyles: chunk.styles, - }); - results.push({ channel: "signal" as const, ...result }); - } - return results; - }, - sendFormattedMedia: async ({ - cfg, - to, - text, - mediaUrl, - mediaLocalRoots, - accountId, - deps, - abortSignal, - }) => { - abortSignal?.throwIfAborted(); - const send = resolveSignalSender(deps); - const maxBytes = resolveSignalMaxBytes({ - cfg, - accountId: accountId ?? undefined, - }); - const tableMode = inferSignalTableMode({ cfg, accountId }); - const formatted = markdownToSignalTextChunks(text, Number.POSITIVE_INFINITY, { - tableMode, - })[0] ?? { - text, - styles: [], - }; - const result = await send(to, formatted.text, { - cfg, - mediaUrl, - maxBytes, - accountId: accountId ?? undefined, - textMode: "plain", - textStyles: formatted.styles, - mediaLocalRoots, - }); - return { channel: "signal", ...result }; - }, - sendText: async ({ cfg, to, text, accountId, deps }) => { - const send = resolveSignalSender(deps); - const maxBytes = resolveSignalMaxBytes({ - cfg, - accountId: accountId ?? undefined, - }); - const result = await send(to, text, { - cfg, - maxBytes, - accountId: accountId ?? undefined, - }); - return { channel: "signal", ...result }; - }, - sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId, deps }) => { - const send = resolveSignalSender(deps); - const maxBytes = resolveSignalMaxBytes({ - cfg, - accountId: accountId ?? undefined, - }); - const result = await send(to, text, { - cfg, - mediaUrl, - maxBytes, - accountId: accountId ?? undefined, - mediaLocalRoots, - }); - return { channel: "signal", ...result }; - }, -}; diff --git a/src/channels/plugins/outbound/slack.sendpayload.test.ts b/src/channels/plugins/outbound/slack.sendpayload.test.ts index 0bb551d0395..e1175023858 100644 --- a/src/channels/plugins/outbound/slack.sendpayload.test.ts +++ b/src/channels/plugins/outbound/slack.sendpayload.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from "vitest"; +import { slackOutbound } from "../../../../test/channel-outbounds.js"; import type { ReplyPayload } from "../../../auto-reply/types.js"; import { installSendPayloadContractSuite, primeSendMock, } from "../../../test-utils/send-payload-contract.js"; -import { slackOutbound } from "./slack.js"; function createHarness(params: { payload: ReplyPayload; diff --git a/src/channels/plugins/outbound/slack.test.ts b/src/channels/plugins/outbound/slack.test.ts index 9b5c1843ce2..90c6f5e55ad 100644 --- a/src/channels/plugins/outbound/slack.test.ts +++ b/src/channels/plugins/outbound/slack.test.ts @@ -10,8 +10,8 @@ vi.mock("../../../plugins/hook-runner-global.js", () => ({ })); import { sendMessageSlack } from "../../../../extensions/slack/src/send.js"; +import { slackOutbound } from "../../../../test/channel-outbounds.js"; import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js"; -import { slackOutbound } from "./slack.js"; type SlackSendTextCtx = { to: string; diff --git a/src/channels/plugins/outbound/slack.ts b/src/channels/plugins/outbound/slack.ts deleted file mode 100644 index 13729acb2ee..00000000000 --- a/src/channels/plugins/outbound/slack.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { parseSlackBlocksInput } from "../../../../extensions/slack/src/blocks-input.js"; -import { - buildSlackInteractiveBlocks, - type SlackBlock, -} from "../../../../extensions/slack/src/blocks-render.js"; -import { sendMessageSlack, type SlackSendIdentity } from "../../../../extensions/slack/src/send.js"; -import type { OutboundIdentity } from "../../../infra/outbound/identity.js"; -import { resolveOutboundSendDep } from "../../../infra/outbound/send-deps.js"; -import { - resolveInteractiveTextFallback, - type InteractiveReply, -} from "../../../interactive/payload.js"; -import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js"; -import type { ChannelOutboundAdapter } from "../types.js"; -import { - resolvePayloadMediaUrls, - sendPayloadMediaSequence, - sendTextMediaPayload, -} from "./direct-text-media.js"; - -const SLACK_MAX_BLOCKS = 50; - -function resolveRenderedInteractiveBlocks( - interactive?: InteractiveReply, -): SlackBlock[] | undefined { - if (!interactive) { - return undefined; - } - const blocks = buildSlackInteractiveBlocks(interactive); - return blocks.length > 0 ? blocks : undefined; -} - -function resolveSlackSendIdentity(identity?: OutboundIdentity): SlackSendIdentity | undefined { - if (!identity) { - return undefined; - } - const username = identity.name?.trim() || undefined; - const iconUrl = identity.avatarUrl?.trim() || undefined; - const rawEmoji = identity.emoji?.trim(); - const iconEmoji = !iconUrl && rawEmoji && /^:[^:\s]+:$/.test(rawEmoji) ? rawEmoji : undefined; - if (!username && !iconUrl && !iconEmoji) { - return undefined; - } - return { username, iconUrl, iconEmoji }; -} - -async function applySlackMessageSendingHooks(params: { - to: string; - text: string; - threadTs?: string; - accountId?: string; - mediaUrl?: string; -}): Promise<{ cancelled: boolean; text: string }> { - const hookRunner = getGlobalHookRunner(); - if (!hookRunner?.hasHooks("message_sending")) { - return { cancelled: false, text: params.text }; - } - const hookResult = await hookRunner.runMessageSending( - { - to: params.to, - content: params.text, - metadata: { - threadTs: params.threadTs, - channelId: params.to, - ...(params.mediaUrl ? { mediaUrl: params.mediaUrl } : {}), - }, - }, - { channelId: "slack", accountId: params.accountId ?? undefined }, - ); - if (hookResult?.cancel) { - return { cancelled: true, text: params.text }; - } - return { cancelled: false, text: hookResult?.content ?? params.text }; -} - -async function sendSlackOutboundMessage(params: { - cfg: NonNullable[2]>["cfg"]; - to: string; - text: string; - mediaUrl?: string; - mediaLocalRoots?: readonly string[]; - blocks?: NonNullable[2]>["blocks"]; - accountId?: string | null; - deps?: { [channelId: string]: unknown } | null; - replyToId?: string | null; - threadId?: string | number | null; - identity?: OutboundIdentity; -}) { - const send = - resolveOutboundSendDep(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); - const hookResult = await applySlackMessageSendingHooks({ - to: params.to, - text: params.text, - threadTs, - mediaUrl: params.mediaUrl, - accountId: params.accountId ?? undefined, - }); - if (hookResult.cancelled) { - return { - channel: "slack" as const, - messageId: "cancelled-by-hook", - channelId: params.to, - meta: { cancelled: true }, - }; - } - - const slackIdentity = resolveSlackSendIdentity(params.identity); - const result = await send(params.to, hookResult.text, { - cfg: params.cfg, - threadTs, - accountId: params.accountId ?? undefined, - ...(params.mediaUrl - ? { mediaUrl: params.mediaUrl, mediaLocalRoots: params.mediaLocalRoots } - : {}), - ...(params.blocks ? { blocks: params.blocks } : {}), - ...(slackIdentity ? { identity: slackIdentity } : {}), - }); - return { channel: "slack" as const, ...result }; -} - -function resolveSlackBlocks(payload: { - channelData?: Record; - interactive?: InteractiveReply; -}) { - const slackData = payload.channelData?.slack; - const renderedInteractive = resolveRenderedInteractiveBlocks(payload.interactive); - if (!slackData || typeof slackData !== "object" || Array.isArray(slackData)) { - return renderedInteractive; - } - let existingBlocks: SlackBlock[] | undefined; - existingBlocks = parseSlackBlocksInput((slackData as { blocks?: unknown }).blocks) as - | SlackBlock[] - | undefined; - const mergedBlocks = [...(existingBlocks ?? []), ...(renderedInteractive ?? [])]; - if (mergedBlocks.length === 0) { - return undefined; - } - if (mergedBlocks.length > SLACK_MAX_BLOCKS) { - throw new Error( - `Slack blocks cannot exceed ${SLACK_MAX_BLOCKS} items after interactive render`, - ); - } - return mergedBlocks; -} - -export const slackOutbound: ChannelOutboundAdapter = { - deliveryMode: "direct", - chunker: null, - textChunkLimit: 4000, - sendPayload: async (ctx) => { - const payload = { - ...ctx.payload, - text: - resolveInteractiveTextFallback({ - text: ctx.payload.text, - interactive: ctx.payload.interactive, - }) ?? "", - }; - const blocks = resolveSlackBlocks(payload); - if (!blocks) { - return await sendTextMediaPayload({ - channel: "slack", - ctx: { - ...ctx, - payload, - }, - adapter: slackOutbound, - }); - } - const mediaUrls = resolvePayloadMediaUrls(payload); - if (mediaUrls.length === 0) { - return await sendSlackOutboundMessage({ - cfg: ctx.cfg, - to: ctx.to, - text: payload.text ?? "", - mediaLocalRoots: ctx.mediaLocalRoots, - blocks, - accountId: ctx.accountId, - deps: ctx.deps, - replyToId: ctx.replyToId, - threadId: ctx.threadId, - identity: ctx.identity, - }); - } - await sendPayloadMediaSequence({ - text: "", - mediaUrls, - send: async ({ text, mediaUrl }) => - await sendSlackOutboundMessage({ - cfg: ctx.cfg, - to: ctx.to, - text, - mediaUrl, - mediaLocalRoots: ctx.mediaLocalRoots, - accountId: ctx.accountId, - deps: ctx.deps, - replyToId: ctx.replyToId, - threadId: ctx.threadId, - identity: ctx.identity, - }), - }); - return await sendSlackOutboundMessage({ - cfg: ctx.cfg, - to: ctx.to, - text: payload.text ?? "", - mediaLocalRoots: ctx.mediaLocalRoots, - blocks, - accountId: ctx.accountId, - deps: ctx.deps, - replyToId: ctx.replyToId, - threadId: ctx.threadId, - identity: ctx.identity, - }); - }, - sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, identity }) => { - return await sendSlackOutboundMessage({ - cfg, - to, - text, - accountId, - deps, - replyToId, - threadId, - identity, - }); - }, - sendMedia: async ({ - cfg, - to, - text, - mediaUrl, - mediaLocalRoots, - accountId, - deps, - replyToId, - threadId, - identity, - }) => { - return await sendSlackOutboundMessage({ - cfg, - to, - text, - mediaUrl, - mediaLocalRoots, - accountId, - deps, - replyToId, - threadId, - identity, - }); - }, -}; diff --git a/src/channels/plugins/outbound/telegram.ts b/src/channels/plugins/outbound/telegram.ts deleted file mode 100644 index 685ddb6ef31..00000000000 --- a/src/channels/plugins/outbound/telegram.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../../extensions/telegram/src/outbound-adapter.js"; diff --git a/src/channels/plugins/outbound/whatsapp.ts b/src/channels/plugins/outbound/whatsapp.ts deleted file mode 100644 index 112ff4ccf91..00000000000 --- a/src/channels/plugins/outbound/whatsapp.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Shim: re-exports from extensions/whatsapp/src/outbound-adapter.ts -export * from "../../../../extensions/whatsapp/src/outbound-adapter.js"; diff --git a/src/channels/plugins/plugins-channel.test.ts b/src/channels/plugins/plugins-channel.test.ts index 01a9d29169a..bfd2c4ff556 100644 --- a/src/channels/plugins/plugins-channel.test.ts +++ b/src/channels/plugins/plugins-channel.test.ts @@ -1,10 +1,9 @@ import { describe, expect, it, vi } from "vitest"; import { normalizeSignalAccountInput } from "../../../extensions/signal/src/setup-surface.js"; +import { telegramOutbound, whatsappOutbound } from "../../../test/channel-outbounds.js"; import type { OpenClawConfig } from "../../config/config.js"; import { normalizeIMessageMessagingTarget } from "./normalize/imessage.js"; import { looksLikeSignalTargetId, normalizeSignalMessagingTarget } from "./normalize/signal.js"; -import { telegramOutbound } from "./outbound/telegram.js"; -import { whatsappOutbound } from "./outbound/whatsapp.js"; function expectWhatsAppTargetResolutionError(result: unknown) { expect(result).toEqual({ diff --git a/src/channels/plugins/types.adapters.ts b/src/channels/plugins/types.adapters.ts index c66fa0d463e..9f9e279bdc1 100644 --- a/src/channels/plugins/types.adapters.ts +++ b/src/channels/plugins/types.adapters.ts @@ -481,6 +481,35 @@ export type ChannelExecApprovalAdapter = { }; export type ChannelAllowlistAdapter = { + applyConfigEdit?: (params: { + cfg: OpenClawConfig; + parsedConfig: Record; + accountId?: string | null; + scope: "dm" | "group"; + action: "add" | "remove"; + entry: string; + }) => + | { + kind: "ok"; + changed: boolean; + pathLabel: string; + writeTarget: ConfigWriteTarget; + } + | { + kind: "invalid-entry"; + } + | Promise< + | { + kind: "ok"; + changed: boolean; + pathLabel: string; + writeTarget: ConfigWriteTarget; + } + | { + kind: "invalid-entry"; + } + > + | null; readConfig?: (params: { cfg: OpenClawConfig; accountId?: string | null }) => | { dmAllowFrom?: Array; @@ -504,17 +533,6 @@ export type ChannelAllowlistAdapter = { }) => | Array<{ input: string; resolved: boolean; name?: string | null }> | Promise>; - resolveConfigEdit?: (params: { - scope: "dm" | "group"; - pathPrefix: string; - writeTarget: ConfigWriteTarget; - }) => { - pathPrefix: string; - writeTarget: ConfigWriteTarget; - readPaths: string[][]; - writePath: string[]; - cleanupPaths?: string[][]; - } | null; supportsScope?: (params: { scope: "dm" | "group" | "all" }) => boolean; }; diff --git a/src/config/sessions/metadata.ts b/src/config/sessions/metadata.ts index c438fd60f2b..b93cfcb5372 100644 --- a/src/config/sessions/metadata.ts +++ b/src/config/sessions/metadata.ts @@ -1,8 +1,7 @@ import type { MsgContext } from "../../auto-reply/templating.js"; import { normalizeChatType } from "../../channels/chat-type.js"; import { resolveConversationLabel } from "../../channels/conversation-label.js"; -import { getChannelDock } from "../../channels/dock.js"; -import { normalizeChannelId } from "../../channels/plugins/index.js"; +import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import { normalizeMessageChannel } from "../../utils/message-channel.js"; import { buildGroupDisplayName, resolveGroupSessionKey } from "./group.js"; import type { GroupKeyResolution, SessionEntry, SessionOrigin } from "./types.js"; @@ -111,7 +110,7 @@ export function deriveGroupSessionPatch(params: { const normalizedChannel = normalizeChannelId(channel); const isChannelProvider = Boolean( normalizedChannel && - getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes("channel"), + getChannelPlugin(normalizedChannel)?.capabilities.chatTypes.includes("channel"), ); const nextGroupChannel = explicitChannel ?? diff --git a/src/cron/isolated-agent.direct-delivery-core-channels.test.ts b/src/cron/isolated-agent.direct-delivery-core-channels.test.ts index 1950e361068..c477ded7f7d 100644 --- a/src/cron/isolated-agent.direct-delivery-core-channels.test.ts +++ b/src/cron/isolated-agent.direct-delivery-core-channels.test.ts @@ -1,12 +1,14 @@ import "./isolated-agent.mocks.js"; import { beforeEach, describe, expect, it } from "vitest"; +import { + discordOutbound, + imessageOutbound, + signalOutbound, + slackOutbound, + telegramOutbound, + whatsappOutbound, +} from "../../test/channel-outbounds.js"; import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js"; -import { discordOutbound } from "../channels/plugins/outbound/discord.js"; -import { imessageOutbound } from "../channels/plugins/outbound/imessage.js"; -import { signalOutbound } from "../channels/plugins/outbound/signal.js"; -import { slackOutbound } from "../channels/plugins/outbound/slack.js"; -import { telegramOutbound } from "../channels/plugins/outbound/telegram.js"; -import { whatsappOutbound } from "../channels/plugins/outbound/whatsapp.js"; import type { CliDeps } from "../cli/deps.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js"; diff --git a/src/cron/isolated-agent.test-setup.ts b/src/cron/isolated-agent.test-setup.ts index e6357531ad3..bdeb71fbaf4 100644 --- a/src/cron/isolated-agent.test-setup.ts +++ b/src/cron/isolated-agent.test-setup.ts @@ -1,9 +1,8 @@ import { vi } from "vitest"; +import { signalOutbound, telegramOutbound } from "../../test/channel-outbounds.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js"; -import { signalOutbound } from "../channels/plugins/outbound/signal.js"; -import { telegramOutbound } from "../channels/plugins/outbound/telegram.js"; import { callGateway } from "../gateway/call.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js"; diff --git a/src/infra/exec-approval-forwarder.test.ts b/src/infra/exec-approval-forwarder.test.ts index d29856c3088..2dfc1c97dbd 100644 --- a/src/infra/exec-approval-forwarder.test.ts +++ b/src/infra/exec-approval-forwarder.test.ts @@ -1,8 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { telegramOutbound } from "../channels/plugins/outbound/telegram.js"; +import { discordPlugin } from "../../extensions/discord/src/channel.js"; +import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; import type { OpenClawConfig } from "../config/config.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; -import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js"; +import { createTestRegistry } from "../test-utils/channel-plugins.js"; import { createExecApprovalForwarder } from "./exec-approval-forwarder.js"; const baseRequest = { @@ -25,7 +26,12 @@ const emptyRegistry = createTestRegistry([]); const defaultRegistry = createTestRegistry([ { pluginId: "telegram", - plugin: createOutboundTestPlugin({ id: "telegram", outbound: telegramOutbound }), + plugin: telegramPlugin, + source: "test", + }, + { + pluginId: "discord", + plugin: discordPlugin, source: "test", }, ]); diff --git a/src/infra/heartbeat-runner.returns-default-unset.test.ts b/src/infra/heartbeat-runner.returns-default-unset.test.ts index a39914016f1..8bca1ca1de7 100644 --- a/src/infra/heartbeat-runner.returns-default-unset.test.ts +++ b/src/infra/heartbeat-runner.returns-default-unset.test.ts @@ -3,9 +3,9 @@ import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { parseTelegramTarget } from "../../extensions/telegram/src/targets.js"; +import { whatsappOutbound } from "../../test/channel-outbounds.js"; import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js"; import * as replyModule from "../auto-reply/reply.js"; -import { whatsappOutbound } from "../channels/plugins/outbound/whatsapp.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveAgentIdFromSessionKey, diff --git a/src/infra/outbound/deliver.test-helpers.ts b/src/infra/outbound/deliver.test-helpers.ts index bc70c456dc5..77054dff7f3 100644 --- a/src/infra/outbound/deliver.test-helpers.ts +++ b/src/infra/outbound/deliver.test-helpers.ts @@ -1,7 +1,9 @@ import { vi } from "vitest"; -import { signalOutbound } from "../../channels/plugins/outbound/signal.js"; -import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js"; -import { whatsappOutbound } from "../../channels/plugins/outbound/whatsapp.js"; +import { + signalOutbound, + telegramOutbound, + whatsappOutbound, +} from "../../../test/channel-outbounds.js"; import type { OpenClawConfig } from "../../config/config.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index 075752df083..5323dd83e27 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -1,9 +1,11 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { markdownToSignalTextChunks } from "../../../extensions/signal/src/format.js"; -import { signalOutbound } from "../../channels/plugins/outbound/signal.js"; -import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js"; -import { whatsappOutbound } from "../../channels/plugins/outbound/whatsapp.js"; +import { + signalOutbound, + telegramOutbound, + whatsappOutbound, +} from "../../../test/channel-outbounds.js"; import type { OpenClawConfig } from "../../config/config.js"; import { STATE_DIR } from "../../config/paths.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; diff --git a/src/infra/outbound/targets.test.ts b/src/infra/outbound/targets.test.ts index 4d9645dc130..76bb9a2b3b5 100644 --- a/src/infra/outbound/targets.test.ts +++ b/src/infra/outbound/targets.test.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it } from "vitest"; import { parseTelegramTarget } from "../../../extensions/telegram/src/targets.js"; -import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js"; -import { whatsappOutbound } from "../../channels/plugins/outbound/whatsapp.js"; +import { telegramOutbound, whatsappOutbound } from "../../../test/channel-outbounds.js"; import type { ChannelOutboundAdapter } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions/types.js"; diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 6e70c8b7c19..5f1ccd91bbe 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -241,6 +241,7 @@ export { buildChannelSendResult } from "./channel-send-result.js"; export type { ChannelSendRawResult } from "./channel-send-result.js"; export { createPluginRuntimeStore } from "./runtime-store.js"; export { createScopedChannelConfigBase } from "./channel-config-helpers.js"; +export { buildAccountScopedAllowlistConfigEditor } from "./allowlist-config-edit.js"; export { AllowFromEntrySchema, AllowFromListSchema, diff --git a/src/plugin-sdk/telegram.ts b/src/plugin-sdk/telegram.ts index 7504994f70a..3e6d1df1257 100644 --- a/src/plugin-sdk/telegram.ts +++ b/src/plugin-sdk/telegram.ts @@ -54,7 +54,7 @@ export { parseTelegramThreadId, } from "../../extensions/telegram/src/outbound-params.js"; export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js"; -export { sendTelegramPayloadMessages } from "../channels/plugins/outbound/telegram.js"; +export { sendTelegramPayloadMessages } from "../../extensions/telegram/src/outbound-adapter.js"; export { resolveAllowlistProviderRuntimeGroupPolicy, diff --git a/src/test-utils/imessage-test-plugin.ts b/src/test-utils/imessage-test-plugin.ts index 962a1f7c33e..201ad3f9897 100644 --- a/src/test-utils/imessage-test-plugin.ts +++ b/src/test-utils/imessage-test-plugin.ts @@ -1,5 +1,5 @@ import { normalizeIMessageHandle } from "../../extensions/imessage/src/targets.js"; -import { imessageOutbound } from "../channels/plugins/outbound/imessage.js"; +import { imessageOutbound } from "../../test/channel-outbounds.js"; import type { ChannelOutboundAdapter, ChannelPlugin } from "../channels/plugins/types.js"; import { collectStatusIssuesFromLastError } from "../plugin-sdk/status-helpers.js";